├── .gitignore ├── LICENSE ├── README.md ├── elm.json ├── examples ├── DragAndDrop.elm ├── DragAndDropWithImagePreview.elm ├── README.md ├── SelectFiles.elm ├── SelectFilesWithProgress.elm ├── SelectFilesWithProgressAndCancellation.elm └── elm.json └── src ├── Elm └── Kernel │ └── File.js ├── File.elm └── File ├── Download.elm └── Select.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-present, Evan Czaplicki 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Evan Czaplicki nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Files 2 | 3 | Select files. Download files. Work with file content. 4 | 5 | Maybe you generate an SVG floorplan or a PDF legal document? You can use [`File.Download`](https://package.elm-lang.org/packages/elm/file/latest/File-Download) to save those files to disk. Maybe you want people to upload a CSV of microbe data or a JPG of their face? You can use [`File.Select`](https://package.elm-lang.org/packages/elm/file/latest/File-Select) to get those files into the browser. 6 | 7 | **This package does not allow _arbitrary_ access to the file system though.** Browsers restrict access to the file system for security. Otherwise, any website on the internet could go try to read private keys out of `~/.ssh` or whatever else they want! 8 | 9 | 10 | ## Download Example 11 | 12 | Maybe you want people to download the floorplan they just designed as an SVG file? You could use [`File.Download.string`](https://package.elm-lang.org/packages/elm/file/latest/File-Download#string) like this: 13 | 14 | ```elm 15 | import File.Download as Download 16 | 17 | download : String -> Cmd msg 18 | download svgContent = 19 | Download.string "floorplan.svg" "image/svg+xml" svgContent 20 | ``` 21 | 22 | 23 | ## Upload Example 24 | 25 | Maybe you want to help scientists explore and visualize data? Maybe they need to upload [CSV files](https://en.wikipedia.org/wiki/Comma-separated_values) like this: 26 | 27 | ``` 28 | Name,Age,Weight,Height 29 | Tom,22,63,1.8 30 | Sue,55,50,1.6 31 | Bob,35,75,1.85 32 | ``` 33 | 34 | You could use [`File.Select.file`](https://package.elm-lang.org/packages/elm/file/latest/File-Select#file) and [`File.toString`](https://package.elm-lang.org/packages/elm/file/latest/File#toString) to create a program like this: 35 | 36 | ```elm 37 | import Browser 38 | import File exposing (File) 39 | import File.Select as Select 40 | import Html exposing (Html, button, p, text) 41 | import Html.Attributes exposing (style) 42 | import Html.Events exposing (onClick) 43 | import Task 44 | 45 | 46 | 47 | -- MAIN 48 | 49 | 50 | main : Program () Model Msg 51 | main = 52 | Browser.element 53 | { init = init 54 | , view = view 55 | , update = update 56 | , subscriptions = subscriptions 57 | } 58 | 59 | 60 | 61 | -- MODEL 62 | 63 | 64 | type alias Model = 65 | { csv : Maybe String 66 | } 67 | 68 | 69 | init : () -> (Model, Cmd Msg) 70 | init _ = 71 | ( Model Nothing, Cmd.none ) 72 | 73 | 74 | 75 | -- UPDATE 76 | 77 | 78 | type Msg 79 | = CsvRequested 80 | | CsvSelected File 81 | | CsvLoaded String 82 | 83 | 84 | update : Msg -> Model -> (Model, Cmd Msg) 85 | update msg model = 86 | case msg of 87 | CsvRequested -> 88 | ( model 89 | , Select.file ["text/csv"] CsvSelected 90 | ) 91 | 92 | CsvSelected file -> 93 | ( model 94 | , Task.perform CsvLoaded (File.toString file) 95 | ) 96 | 97 | CsvLoaded content -> 98 | ( { model | csv = Just content } 99 | , Cmd.none 100 | ) 101 | 102 | 103 | 104 | -- VIEW 105 | 106 | 107 | view : Model -> Html Msg 108 | view model = 109 | case model.csv of 110 | Nothing -> 111 | button [ onClick CsvRequested ] [ text "Load CSV" ] 112 | 113 | Just content -> 114 | p [ style "white-space" "pre" ] [ text content ] 115 | 116 | 117 | 118 | -- SUBSCRIPTIONS 119 | 120 | 121 | subscriptions : Model -> Sub Msg 122 | subscriptions _ = 123 | Sub.none 124 | 125 | ``` 126 | 127 | From there you could parse the CSV file, start showing scatter plots, etc. -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "elm/file", 4 | "summary": "Select files. Download files. Work with file content.", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.5", 7 | "exposed-modules": [ 8 | "File", 9 | "File.Select", 10 | "File.Download" 11 | ], 12 | "elm-version": "0.19.0 <= v < 0.20.0", 13 | "dependencies": { 14 | "elm/bytes": "1.0.0 <= v < 2.0.0", 15 | "elm/core": "1.0.1 <= v < 2.0.0", 16 | "elm/json": "1.1.0 <= v < 2.0.0", 17 | "elm/time": "1.0.0 <= v < 2.0.0" 18 | }, 19 | "test-dependencies": {} 20 | } -------------------------------------------------------------------------------- /examples/DragAndDrop.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import File exposing (File) 3 | import File.Select as Select 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Json.Decode as D 8 | 9 | 10 | 11 | -- MAIN 12 | 13 | 14 | main = 15 | Browser.element 16 | { init = init 17 | , view = view 18 | , update = update 19 | , subscriptions = subscriptions 20 | } 21 | 22 | 23 | 24 | -- MODEL 25 | 26 | 27 | type alias Model = 28 | { hover : Bool 29 | , files : List File 30 | } 31 | 32 | 33 | init : () -> (Model, Cmd Msg) 34 | init _ = 35 | (Model False [], Cmd.none) 36 | 37 | 38 | 39 | -- UPDATE 40 | 41 | 42 | type Msg 43 | = Pick 44 | | DragEnter 45 | | DragLeave 46 | | GotFiles File (List File) 47 | 48 | 49 | update : Msg -> Model -> (Model, Cmd Msg) 50 | update msg model = 51 | case msg of 52 | Pick -> 53 | ( model 54 | , Select.files ["image/*"] GotFiles 55 | ) 56 | 57 | DragEnter -> 58 | ( { model | hover = True } 59 | , Cmd.none 60 | ) 61 | 62 | DragLeave -> 63 | ( { model | hover = False } 64 | , Cmd.none 65 | ) 66 | 67 | GotFiles file files -> 68 | ( { model 69 | | files = file :: files 70 | , hover = False 71 | } 72 | , Cmd.none 73 | ) 74 | 75 | 76 | 77 | -- SUBSCRIPTIONS 78 | 79 | 80 | subscriptions : Model -> Sub Msg 81 | subscriptions model = 82 | Sub.none 83 | 84 | 85 | 86 | -- VIEW 87 | 88 | 89 | view : Model -> Html Msg 90 | view model = 91 | div 92 | [ style "border" (if model.hover then "6px dashed purple" else "6px dashed #ccc") 93 | , style "border-radius" "20px" 94 | , style "width" "480px" 95 | , style "height" "100px" 96 | , style "margin" "100px auto" 97 | , style "padding" "20px" 98 | , style "display" "flex" 99 | , style "flex-direction" "column" 100 | , style "justify-content" "center" 101 | , style "align-items" "center" 102 | , hijackOn "dragenter" (D.succeed DragEnter) 103 | , hijackOn "dragover" (D.succeed DragEnter) 104 | , hijackOn "dragleave" (D.succeed DragLeave) 105 | , hijackOn "drop" dropDecoder 106 | ] 107 | [ button [ onClick Pick ] [ text "Upload Images" ] 108 | , span [ style "color" "#ccc" ] [ text (Debug.toString model) ] 109 | ] 110 | 111 | 112 | dropDecoder : D.Decoder Msg 113 | dropDecoder = 114 | D.at ["dataTransfer","files"] (D.oneOrMore GotFiles File.decoder) 115 | 116 | 117 | hijackOn : String -> D.Decoder msg -> Attribute msg 118 | hijackOn event decoder = 119 | preventDefaultOn event (D.map hijack decoder) 120 | 121 | 122 | hijack : msg -> (msg, Bool) 123 | hijack msg = 124 | (msg, True) 125 | -------------------------------------------------------------------------------- /examples/DragAndDropWithImagePreview.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import File exposing (File) 3 | import File.Select as Select 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Json.Decode as D 8 | import Task 9 | 10 | 11 | 12 | -- MAIN 13 | 14 | 15 | main = 16 | Browser.element 17 | { init = init 18 | , view = view 19 | , update = update 20 | , subscriptions = subscriptions 21 | } 22 | 23 | 24 | 25 | -- MODEL 26 | 27 | 28 | type alias Model = 29 | { hover : Bool 30 | , previews : List String 31 | } 32 | 33 | 34 | init : () -> (Model, Cmd Msg) 35 | init _ = 36 | (Model False [], Cmd.none) 37 | 38 | 39 | 40 | -- UPDATE 41 | 42 | 43 | type Msg 44 | = Pick 45 | | DragEnter 46 | | DragLeave 47 | | GotFiles File (List File) 48 | | GotPreviews (List String) 49 | 50 | 51 | update : Msg -> Model -> (Model, Cmd Msg) 52 | update msg model = 53 | case msg of 54 | Pick -> 55 | ( model 56 | , Select.files ["image/*"] GotFiles 57 | ) 58 | 59 | DragEnter -> 60 | ( { model | hover = True } 61 | , Cmd.none 62 | ) 63 | 64 | DragLeave -> 65 | ( { model | hover = False } 66 | , Cmd.none 67 | ) 68 | 69 | GotFiles file files -> 70 | ( { model | hover = False } 71 | , Task.perform GotPreviews <| Task.sequence <| 72 | List.map File.toUrl (file :: files) 73 | ) 74 | 75 | GotPreviews urls -> 76 | ( { model | previews = urls } 77 | , Cmd.none 78 | ) 79 | 80 | 81 | 82 | -- SUBSCRIPTIONS 83 | 84 | 85 | subscriptions : Model -> Sub Msg 86 | subscriptions model = 87 | Sub.none 88 | 89 | 90 | 91 | -- VIEW 92 | 93 | 94 | view : Model -> Html Msg 95 | view model = 96 | div 97 | [ style "border" (if model.hover then "6px dashed purple" else "6px dashed #ccc") 98 | , style "border-radius" "20px" 99 | , style "width" "480px" 100 | , style "margin" "100px auto" 101 | , style "padding" "40px" 102 | , style "display" "flex" 103 | , style "flex-direction" "column" 104 | , style "justify-content" "center" 105 | , style "align-items" "center" 106 | , hijackOn "dragenter" (D.succeed DragEnter) 107 | , hijackOn "dragover" (D.succeed DragEnter) 108 | , hijackOn "dragleave" (D.succeed DragLeave) 109 | , hijackOn "drop" dropDecoder 110 | ] 111 | [ button [ onClick Pick ] [ text "Upload Images" ] 112 | , div 113 | [ style "display" "flex" 114 | , style "align-items" "center" 115 | , style "height" "60px" 116 | , style "padding" "20px" 117 | ] 118 | (List.map viewPreview model.previews) 119 | ] 120 | 121 | 122 | viewPreview : String -> Html msg 123 | viewPreview url = 124 | div 125 | [ style "width" "60px" 126 | , style "height" "60px" 127 | , style "background-image" ("url('" ++ url ++ "')") 128 | , style "background-position" "center" 129 | , style "background-repeat" "no-repeat" 130 | , style "background-size" "contain" 131 | ] 132 | [] 133 | 134 | 135 | dropDecoder : D.Decoder Msg 136 | dropDecoder = 137 | D.at ["dataTransfer","files"] (D.oneOrMore GotFiles File.decoder) 138 | 139 | 140 | hijackOn : String -> D.Decoder msg -> Attribute msg 141 | hijackOn event decoder = 142 | preventDefaultOn event (D.map hijack decoder) 143 | 144 | 145 | hijack : msg -> (msg, Bool) 146 | hijack msg = 147 | (msg, True) 148 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | To play with these examples locally, you need to (1) make sure Elm is [installed](https://guide.elm-lang.org/install.html) and then (2) run the following commands: 2 | 3 | ```bash 4 | git clone https://github.com/elm/file.git 5 | cd file/examples 6 | elm reactor 7 | ``` 8 | 9 | You can then play with the working examples at [http://localhost:8000](http://localhost:8000). 10 | 11 | **Note:** You may need to open up the “Network” panel in your browser’s developer tools and slow down the network speed to get the progress examples to animate nicely! -------------------------------------------------------------------------------- /examples/SelectFiles.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import File exposing (File) 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (..) 6 | import Json.Decode as D 7 | 8 | 9 | 10 | -- MAIN 11 | 12 | 13 | main = 14 | Browser.element 15 | { init = init 16 | , view = view 17 | , update = update 18 | , subscriptions = subscriptions 19 | } 20 | 21 | 22 | 23 | -- MODEL 24 | 25 | 26 | type alias Model = List File 27 | 28 | 29 | init : () -> (Model, Cmd Msg) 30 | init _ = 31 | ([], Cmd.none) 32 | 33 | 34 | 35 | -- UPDATE 36 | 37 | 38 | type Msg 39 | = GotFiles (List File) 40 | 41 | 42 | update : Msg -> Model -> (Model, Cmd Msg) 43 | update msg model = 44 | case msg of 45 | GotFiles files -> 46 | (files, Cmd.none) 47 | 48 | 49 | 50 | -- SUBSCRIPTIONS 51 | 52 | 53 | subscriptions : Model -> Sub Msg 54 | subscriptions model = 55 | Sub.none 56 | 57 | 58 | 59 | -- VIEW 60 | 61 | 62 | view : Model -> Html Msg 63 | view model = 64 | div [] 65 | [ input 66 | [ type_ "file" 67 | , multiple True 68 | , on "change" (D.map GotFiles filesDecoder) 69 | ] 70 | [] 71 | , div [] [ text (Debug.toString model) ] 72 | ] 73 | 74 | 75 | filesDecoder : D.Decoder (List File) 76 | filesDecoder = 77 | D.at ["target","files"] (D.list File.decoder) 78 | -------------------------------------------------------------------------------- /examples/SelectFilesWithProgress.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import File exposing (File) 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (..) 6 | import Http 7 | import Json.Decode as D 8 | 9 | 10 | 11 | -- MAIN 12 | 13 | 14 | main = 15 | Browser.element 16 | { init = init 17 | , view = view 18 | , update = update 19 | , subscriptions = subscriptions 20 | } 21 | 22 | 23 | 24 | -- MODEL 25 | 26 | 27 | type Model 28 | = Waiting 29 | | Uploading Float 30 | | Done 31 | | Fail 32 | 33 | 34 | init : () -> (Model, Cmd Msg) 35 | init _ = 36 | ( Waiting 37 | , Cmd.none 38 | ) 39 | 40 | 41 | 42 | -- UPDATE 43 | 44 | 45 | type Msg 46 | = GotFiles (List File) 47 | | GotProgress Http.Progress 48 | | Uploaded (Result Http.Error ()) 49 | 50 | 51 | update : Msg -> Model -> (Model, Cmd Msg) 52 | update msg model = 53 | case msg of 54 | GotFiles files -> 55 | ( Uploading 0 56 | , Http.request 57 | { method = "POST" 58 | , url = "/" 59 | , headers = [] 60 | , body = Http.multipartBody (List.map (Http.filePart "files[]") files) 61 | , expect = Http.expectWhatever Uploaded 62 | , timeout = Nothing 63 | , tracker = Just "upload" 64 | } 65 | ) 66 | 67 | GotProgress progress -> 68 | case progress of 69 | Http.Sending p -> 70 | (Uploading (Http.fractionSent p), Cmd.none) 71 | 72 | Http.Receiving _ -> 73 | (model, Cmd.none) 74 | 75 | Uploaded result -> 76 | case result of 77 | Ok _ -> 78 | (Done, Cmd.none) 79 | 80 | Err _ -> 81 | (Fail, Cmd.none) 82 | 83 | 84 | 85 | -- SUBSCRIPTIONS 86 | 87 | 88 | subscriptions : Model -> Sub Msg 89 | subscriptions model = 90 | Http.track "upload" GotProgress 91 | 92 | 93 | 94 | -- VIEW 95 | 96 | 97 | view : Model -> Html Msg 98 | view model = 99 | case model of 100 | Waiting -> 101 | input 102 | [ type_ "file" 103 | , multiple True 104 | , on "change" (D.map GotFiles filesDecoder) 105 | ] 106 | [] 107 | 108 | Uploading fraction -> 109 | h1 [] [ text (String.fromInt (round (100 * fraction)) ++ "%") ] 110 | 111 | Done -> 112 | h1 [] [ text "DONE" ] 113 | 114 | Fail -> 115 | h1 [] [ text "FAIL" ] 116 | 117 | 118 | filesDecoder : D.Decoder (List File) 119 | filesDecoder = 120 | D.at ["target","files"] (D.list File.decoder) 121 | -------------------------------------------------------------------------------- /examples/SelectFilesWithProgressAndCancellation.elm: -------------------------------------------------------------------------------- 1 | import Browser 2 | import File exposing (File) 3 | import Html exposing (..) 4 | import Html.Attributes as A exposing (..) 5 | import Html.Events exposing (..) 6 | import Http 7 | import Json.Decode as D 8 | 9 | 10 | 11 | -- MAIN 12 | 13 | 14 | main = 15 | Browser.element 16 | { init = init 17 | , view = view 18 | , update = update 19 | , subscriptions = subscriptions 20 | } 21 | 22 | 23 | 24 | -- MODEL 25 | 26 | 27 | type Model 28 | = Waiting 29 | | Uploading Float 30 | | Done 31 | | Fail 32 | 33 | 34 | init : () -> (Model, Cmd Msg) 35 | init _ = 36 | ( Waiting 37 | , Cmd.none 38 | ) 39 | 40 | 41 | 42 | -- UPDATE 43 | 44 | 45 | type Msg 46 | = GotFiles (List File) 47 | | GotProgress Http.Progress 48 | | Uploaded (Result Http.Error ()) 49 | | Cancel 50 | 51 | 52 | update : Msg -> Model -> (Model, Cmd Msg) 53 | update msg model = 54 | case msg of 55 | GotFiles files -> 56 | ( Uploading 0 57 | , Http.request 58 | { method = "POST" 59 | , url = "/" 60 | , headers = [] 61 | , body = Http.multipartBody (List.map (Http.filePart "files[]") files) 62 | , expect = Http.expectWhatever Uploaded 63 | , timeout = Nothing 64 | , tracker = Just "upload" 65 | } 66 | ) 67 | 68 | GotProgress progress -> 69 | case progress of 70 | Http.Sending p -> 71 | (Uploading (Http.fractionSent p), Cmd.none) 72 | 73 | Http.Receiving _ -> 74 | (model, Cmd.none) 75 | 76 | Uploaded result -> 77 | case result of 78 | Ok _ -> 79 | (Done, Cmd.none) 80 | 81 | Err _ -> 82 | (Fail, Cmd.none) 83 | 84 | Cancel -> 85 | (Waiting, Http.cancel "upload") 86 | 87 | 88 | 89 | -- SUBSCRIPTIONS 90 | 91 | 92 | subscriptions : Model -> Sub Msg 93 | subscriptions model = 94 | Http.track "upload" GotProgress 95 | 96 | 97 | 98 | -- VIEW 99 | 100 | 101 | view : Model -> Html Msg 102 | view model = 103 | case model of 104 | Waiting -> 105 | input 106 | [ type_ "file" 107 | , multiple True 108 | , on "change" (D.map GotFiles filesDecoder) 109 | ] 110 | [] 111 | 112 | Uploading fraction -> 113 | div [] 114 | [ progress 115 | [ value (String.fromInt (round (100 * fraction))) 116 | , A.max "100" 117 | , style "display" "block" 118 | ] 119 | [] 120 | , button [ onClick Cancel ] [ text "Cancel" ] 121 | ] 122 | 123 | Done -> 124 | h1 [] [ text "DONE" ] 125 | 126 | Fail -> 127 | h1 [] [ text "FAIL" ] 128 | 129 | 130 | filesDecoder : D.Decoder (List File) 131 | filesDecoder = 132 | D.at ["target","files"] (D.list File.decoder) 133 | -------------------------------------------------------------------------------- /examples/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "." 5 | ], 6 | "elm-version": "0.19.0", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.1", 10 | "elm/core": "1.0.1", 11 | "elm/file": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "2.0.0", 14 | "elm/json": "1.1.0" 15 | }, 16 | "indirect": { 17 | "elm/bytes": "1.0.0", 18 | "elm/time": "1.0.0", 19 | "elm/url": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | }, 23 | "test-dependencies": { 24 | "direct": {}, 25 | "indirect": {} 26 | } 27 | } -------------------------------------------------------------------------------- /src/Elm/Kernel/File.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | import Elm.Kernel.Json exposing (decodePrim, expecting) 4 | import Elm.Kernel.List exposing (fromArray) 5 | import Elm.Kernel.Scheduler exposing (binding, succeed) 6 | import Elm.Kernel.Utils exposing (Tuple0, Tuple2) 7 | import Result exposing (Ok) 8 | import String exposing (join) 9 | import Time exposing (millisToPosix) 10 | 11 | */ 12 | 13 | 14 | // DECODER 15 | 16 | var _File_decoder = __Json_decodePrim(function(value) { 17 | // NOTE: checks if `File` exists in case this is run on node 18 | return (typeof File !== 'undefined' && value instanceof File) 19 | ? __Result_Ok(value) 20 | : __Json_expecting('a FILE', value); 21 | }); 22 | 23 | 24 | // METADATA 25 | 26 | function _File_name(file) { return file.name; } 27 | function _File_mime(file) { return file.type; } 28 | function _File_size(file) { return file.size; } 29 | 30 | function _File_lastModified(file) 31 | { 32 | return __Time_millisToPosix(file.lastModified); 33 | } 34 | 35 | 36 | // DOWNLOAD 37 | 38 | var _File_downloadNode; 39 | 40 | function _File_getDownloadNode() 41 | { 42 | return _File_downloadNode || (_File_downloadNode = document.createElement('a')); 43 | } 44 | 45 | var _File_download = F3(function(name, mime, content) 46 | { 47 | return __Scheduler_binding(function(callback) 48 | { 49 | var blob = new Blob([content], {type: mime}); 50 | 51 | // for IE10+ 52 | if (navigator.msSaveOrOpenBlob) 53 | { 54 | navigator.msSaveOrOpenBlob(blob, name); 55 | return; 56 | } 57 | 58 | // for HTML5 59 | var node = _File_getDownloadNode(); 60 | var objectUrl = URL.createObjectURL(blob); 61 | node.href = objectUrl; 62 | node.download = name; 63 | _File_click(node); 64 | URL.revokeObjectURL(objectUrl); 65 | }); 66 | }); 67 | 68 | function _File_downloadUrl(href) 69 | { 70 | return __Scheduler_binding(function(callback) 71 | { 72 | var node = _File_getDownloadNode(); 73 | node.href = href; 74 | node.download = ''; 75 | node.origin === location.origin || (node.target = '_blank'); 76 | _File_click(node); 77 | }); 78 | } 79 | 80 | 81 | // IE COMPATIBILITY 82 | 83 | function _File_makeBytesSafeForInternetExplorer(bytes) 84 | { 85 | // only needed by IE10 and IE11 to fix https://github.com/elm/file/issues/10 86 | // all other browsers can just run `new Blob([bytes])` directly with no problem 87 | // 88 | return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength); 89 | } 90 | 91 | function _File_click(node) 92 | { 93 | // only needed by IE10 and IE11 to fix https://github.com/elm/file/issues/11 94 | // all other browsers have MouseEvent and do not need this conditional stuff 95 | // 96 | if (typeof MouseEvent === 'function') 97 | { 98 | node.dispatchEvent(new MouseEvent('click')); 99 | } 100 | else 101 | { 102 | var event = document.createEvent('MouseEvents'); 103 | event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 104 | document.body.appendChild(node); 105 | node.dispatchEvent(event); 106 | document.body.removeChild(node); 107 | } 108 | } 109 | 110 | 111 | // UPLOAD 112 | 113 | var _File_node; 114 | 115 | function _File_uploadOne(mimes) 116 | { 117 | return __Scheduler_binding(function(callback) 118 | { 119 | _File_node = document.createElement('input'); 120 | _File_node.type = 'file'; 121 | _File_node.accept = A2(__String_join, ',', mimes); 122 | _File_node.addEventListener('change', function(event) 123 | { 124 | callback(__Scheduler_succeed(event.target.files[0])); 125 | }); 126 | _File_click(_File_node); 127 | }); 128 | } 129 | 130 | function _File_uploadOneOrMore(mimes) 131 | { 132 | return __Scheduler_binding(function(callback) 133 | { 134 | _File_node = document.createElement('input'); 135 | _File_node.type = 'file'; 136 | _File_node.multiple = true; 137 | _File_node.accept = A2(__String_join, ',', mimes); 138 | _File_node.addEventListener('change', function(event) 139 | { 140 | var elmFiles = __List_fromArray(event.target.files); 141 | callback(__Scheduler_succeed(__Utils_Tuple2(elmFiles.a, elmFiles.b))); 142 | }); 143 | _File_click(_File_node); 144 | }); 145 | } 146 | 147 | 148 | // CONTENT 149 | 150 | function _File_toString(blob) 151 | { 152 | return __Scheduler_binding(function(callback) 153 | { 154 | var reader = new FileReader(); 155 | reader.addEventListener('loadend', function() { 156 | callback(__Scheduler_succeed(reader.result)); 157 | }); 158 | reader.readAsText(blob); 159 | return function() { reader.abort(); }; 160 | }); 161 | } 162 | 163 | function _File_toBytes(blob) 164 | { 165 | return __Scheduler_binding(function(callback) 166 | { 167 | var reader = new FileReader(); 168 | reader.addEventListener('loadend', function() { 169 | callback(__Scheduler_succeed(new DataView(reader.result))); 170 | }); 171 | reader.readAsArrayBuffer(blob); 172 | return function() { reader.abort(); }; 173 | }); 174 | } 175 | 176 | function _File_toUrl(blob) 177 | { 178 | return __Scheduler_binding(function(callback) 179 | { 180 | var reader = new FileReader(); 181 | reader.addEventListener('loadend', function() { 182 | callback(__Scheduler_succeed(reader.result)); 183 | }); 184 | reader.readAsDataURL(blob); 185 | return function() { reader.abort(); }; 186 | }); 187 | } 188 | 189 | -------------------------------------------------------------------------------- /src/File.elm: -------------------------------------------------------------------------------- 1 | module File exposing 2 | ( File 3 | , decoder 4 | , toString 5 | , toBytes 6 | , toUrl 7 | , name 8 | , mime 9 | , size 10 | , lastModified 11 | ) 12 | 13 | 14 | {-| 15 | 16 | # Files 17 | @docs File, decoder 18 | 19 | # Extract Content 20 | @docs toString, toBytes, toUrl 21 | 22 | # Read Metadata 23 | @docs name, mime, size, lastModified 24 | 25 | -} 26 | 27 | import Bytes exposing (Bytes) 28 | import Elm.Kernel.File 29 | import Json.Decode as Decode 30 | import Task exposing (Task) 31 | import Time 32 | 33 | 34 | 35 | -- FILE 36 | 37 | 38 | {-| Represents an uploaded file. From there you can read the content, check 39 | the metadata, send it over [`elm/http`](/packages/elm/http/latest), etc. 40 | -} 41 | type File = File 42 | 43 | 44 | {-| Decode `File` values. For example, if you want to create a drag-and-drop 45 | file uploader, you can listen for `drop` events with a decoder like this: 46 | 47 | import File 48 | import Json.Decode exposing (Decoder, field, list) 49 | 50 | files : Decode.Decoder (List File) 51 | files = 52 | field "dataTransfer" (field "files" (list File.decoder)) 53 | 54 | Once you have the files, you can use functions like [`File.toString`](#toString) 55 | to process the content. Or you can send the file along to someone else with the 56 | [`elm/http`](/packages/elm/http/latest) package. 57 | -} 58 | decoder : Decode.Decoder File 59 | decoder = 60 | Elm.Kernel.File.decoder 61 | 62 | 63 | 64 | -- CONTENT 65 | 66 | 67 | {-| Extract the content of a `File` as a `String`. So if you have a `notes.md` 68 | file you could read the content like this: 69 | 70 | import File exposing (File) 71 | import Task 72 | 73 | type Msg 74 | = MarkdownLoaded String 75 | 76 | read : File -> Cmd Msg 77 | read file = 78 | Task.perform MarkdownLoaded (File.toString file) 79 | 80 | Reading the content is asynchronous because browsers want to avoid allocating 81 | the file content into memory if possible. (E.g. if you are just sending files 82 | along to a server with [`elm/http`](/packages/elm/http/latest) there is no 83 | point having their content in memory!) 84 | -} 85 | toString : File -> Task x String 86 | toString = 87 | Elm.Kernel.File.toString 88 | 89 | 90 | {-| Extract the content of a `File` as `Bytes`. So if you have an `archive.zip` 91 | file you could read the content like this: 92 | 93 | import Bytes exposing (Bytes) 94 | import File exposing (File) 95 | import Task 96 | 97 | type Msg 98 | = ZipLoaded Bytes 99 | 100 | read : File -> Cmd Msg 101 | read file = 102 | Task.perform ZipLoaded (File.toBytes file) 103 | 104 | From here you can use the [`elm/bytes`](/packages/elm/bytes/latest) package to 105 | work with the bytes and turn them into whatever you want. 106 | -} 107 | toBytes : File -> Task x Bytes 108 | toBytes = 109 | Elm.Kernel.File.toBytes 110 | 111 | 112 | {-| The `File.toUrl` function will convert files into URLs like this: 113 | 114 | - `data:*/*;base64,V2hvIGF0ZSBhbGwgdGhlIHBpZT8=` 115 | - `data:*/*;base64,SXQgd2FzIG1lLCBXaWxleQ==` 116 | - `data:*/*;base64,SGUgYXRlIGFsbCB0aGUgcGllcywgYm95IQ==` 117 | 118 | This is using a [Base64](https://en.wikipedia.org/wiki/Base64) encoding to 119 | turn arbitrary binary data into ASCII characters that safely fit in strings. 120 | 121 | This is primarily useful when you want to show images that were just uploaded 122 | because **an `` tag expects its `src` attribute to be a URL.** So if you 123 | have a website for selling furniture, using `File.toUrl` could make it easier 124 | to create a screen to preview and reorder images. This way people can make 125 | sure their old table looks great! 126 | -} 127 | toUrl : File -> Task x String 128 | toUrl = 129 | Elm.Kernel.File.toUrl 130 | 131 | 132 | 133 | -- METADATA 134 | 135 | 136 | {-| Get the name of a file. 137 | 138 | name file1 == "README.md" 139 | name file2 == "math.gif" 140 | name file3 == "archive.zip" 141 | -} 142 | name : File -> String 143 | name = 144 | Elm.Kernel.File.name 145 | 146 | {-| Get the MIME type of a file. 147 | 148 | mime file1 == "text/markdown" 149 | mime file2 == "image/gif" 150 | mime file3 == "application/zip" 151 | -} 152 | mime : File -> String 153 | mime = 154 | Elm.Kernel.File.mime 155 | 156 | 157 | {-| Get the size of the file in bytes. 158 | 159 | size file1 == 395 160 | size file2 == 65813 161 | size file3 == 81481 162 | -} 163 | size : File -> Int 164 | size = 165 | Elm.Kernel.File.size 166 | 167 | 168 | {-| Get the time the file was last modified. 169 | 170 | lastModified file1 -- 1536872423 171 | lastModified file2 -- 860581394 172 | lastModified file3 -- 1340375405 173 | 174 | Learn more about how time is represented by reading through the 175 | [`elm/time`](/packages/elm/time/latest) package! 176 | -} 177 | lastModified : File -> Time.Posix 178 | lastModified = 179 | Elm.Kernel.File.lastModified 180 | -------------------------------------------------------------------------------- /src/File/Download.elm: -------------------------------------------------------------------------------- 1 | module File.Download exposing 2 | ( string 3 | , bytes 4 | , url 5 | ) 6 | 7 | 8 | {-| Commands for downloading files. 9 | 10 | **SECURITY NOTE:** Browsers require that all downloads are initiated by a user 11 | event. So rather than allowing malicious sites to put files on your computer 12 | however they please, the user at least have to click a button first. As a 13 | result, the following commands only work when they are triggered by some user 14 | event. 15 | 16 | # Download 17 | @docs string, bytes, url 18 | 19 | -} 20 | 21 | 22 | import Bytes exposing (Bytes) 23 | import Elm.Kernel.File 24 | import Task 25 | 26 | 27 | 28 | -- DOWNLOAD 29 | 30 | 31 | {-| Download a file from a URL on the same origin. So if you have a website 32 | at `https://example.com`, you could download a math GIF like this: 33 | 34 | import File.Download as Download 35 | 36 | downloadMathGif : Cmd msg 37 | downloadMathGif = 38 | Download.url "https://example.com/math.gif" 39 | 40 | The downloaded file will use whatever name the server suggests. So if you want 41 | a different name, have your server add a [`Content-Disposition`][cd] header like 42 | `Content-Disposition: attachment; filename="triangle.gif"` when it serves the 43 | file. 44 | 45 | **Warning:** The implementation uses `` which has 46 | two important consequences: 47 | 48 | 1. **Cross-origin downloads are weird.** If you want a file from a different 49 | domain (like `https://files.example.com` or `https://www.wikipedia.org`) this 50 | function adds a `target="_blank"`, opening the file in a new tab. Otherwise 51 | the link would just take over the current page, replacing your website with a 52 | GIF or whatever. To make cross-origin downloads work differently, you can (1) 53 | make the request same-origin by sending it to your server and then having your 54 | server fetch the file or (2) fetch the file with `elm/http` and then go through 55 | `File.Download.bytes`. 56 | 2. **Same-origin downloads are weird in IE10 and IE11.** These browsers do not 57 | support the `download` attribute, so you should always get the `target="_blank"` 58 | behavior where the URL opens in a new tab. Again, you can fetch the file with 59 | `elm/http` and then use `File.Download.bytes` to get around this. 60 | 61 | Things are quite tricky here between the intentional security constraints and 62 | particularities of browser implementations, so remember that you can always 63 | send the URL out a `port` and do something even more custom in JavaScript! 64 | 65 | [cd]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition 66 | -} 67 | url : String -> Cmd msg 68 | url href = 69 | Task.perform never (Elm.Kernel.File.downloadUrl href) 70 | 71 | 72 | {-| Download a `String` as a file. Maybe you markdown editor in the browser, 73 | and you want to provide a button to download markdown files: 74 | 75 | import File.Download as Download 76 | 77 | save : String -> Cmd msg 78 | save markdown = 79 | Download.string "draft.md" "text/markdown" markdown 80 | 81 | So the arguments are file name, MIME type, and then the file content. In this 82 | case is is markdown, but it could be any string information. 83 | -} 84 | string : String -> String -> String -> Cmd msg 85 | string name mime content = 86 | Task.perform never (Elm.Kernel.File.download name mime content) 87 | 88 | 89 | {-| Download some `Bytes` as a file. Maybe you are creating custom images, 90 | and you want a button to download them as PNG files. After using 91 | [`elm/bytes`][bytes] to generate the file content, you can download it like 92 | this: 93 | 94 | import Bytes exposing (Bytes) 95 | import File.Download as Download 96 | 97 | savePng : Bytes -> Cmd msg 98 | savePng bytes = 99 | Download.bytes "frog.png" "image/png" bytes 100 | 101 | So the arguments are file name, MIME type, and then the file content. With the 102 | ability to build any byte sequence you want with [`elm/bytes`][bytes], you can 103 | create `.zip` files, `.jpg` files, or whatever else you might need! 104 | 105 | [bytes]: /packages/elm/bytes/latest 106 | -} 107 | bytes : String -> String -> Bytes -> Cmd msg 108 | bytes name mime content = 109 | Task.perform never <| Elm.Kernel.File.download name mime <| 110 | Elm.Kernel.File.makeBytesSafeForInternetExplorer content 111 | -------------------------------------------------------------------------------- /src/File/Select.elm: -------------------------------------------------------------------------------- 1 | module File.Select exposing ( file, files ) 2 | 3 | 4 | {-| Ask the user to select some files. 5 | 6 | **SECURITY NOTE:** Browsers will only open a file selector in reaction to a 7 | user event. So rather than allowing malicious sites to ask for files whenever 8 | they please, the user at least have to click a button first. As a result, the 9 | following commands only work when they are triggered by some user event. 10 | 11 | # Select Files 12 | @docs file, files 13 | 14 | # Limitations 15 | 16 | The API here uses commands, but it seems like it could also provide tasks. 17 | The trouble is that a `Task` is guaranteed to succeed or fail. There should 18 | not be any cases where it just does neither. File selection makes this tricky 19 | because there are two limitations in JavaScript as of this writing: 20 | 21 | 1. File selection must be a direct response to a user event. This is intended 22 | to help with security. It is not clear how to reliably detect when these 23 | commands were issued at invalid times, especially across browsers. 24 | 2. The user can always click `Cancel` on the file dialog. It is quite 25 | difficult to reliably detect if someone has clicked this button across 26 | browsers, especially when it is hard to know if the dialog is even open in the 27 | first place. 28 | 29 | I think it would be worth figuring out how to know these two things reliably 30 | before exposing a `Task` API for things. 31 | 32 | -} 33 | 34 | 35 | import Bytes exposing (Bytes) 36 | import Elm.Kernel.File 37 | import File exposing (File) 38 | import Json.Decode as Decode 39 | import Task exposing (Task) 40 | import Time 41 | 42 | 43 | 44 | -- ONE FILE 45 | 46 | 47 | {-| Ask the user to select **one** file. To ask for a single `.zip` file you 48 | could say: 49 | 50 | import File.Select as Select 51 | 52 | type Msg 53 | = ZipRequested 54 | | ZipLoaded File 55 | 56 | requestZip : Cmd Msg 57 | requestZip = 58 | Select.file ["application/zip"] ZipLoaded 59 | 60 | You provide (1) a list of acceptable MIME types and (2) a function to turn the 61 | resulting file into a message for your `update` function. In this case, we only 62 | want files with MIME type `application/zip`. 63 | 64 | **Note:** This only works when the command is the direct result of a user 65 | event, like clicking something. 66 | 67 | **Note:** This command may not resolve, partly because it is unclear how to 68 | reliably detect `Cancel` clicks across browsers. More about that in the 69 | section on [limitations](#limitations) below. 70 | -} 71 | file : List String -> (File -> msg) -> Cmd msg 72 | file mimes toMsg = 73 | Task.perform toMsg (Elm.Kernel.File.uploadOne mimes) 74 | 75 | 76 | 77 | -- ONE OR MORE 78 | 79 | 80 | {-| Ask the user to select **one or more** files. To ask for many image files, 81 | you could say: 82 | 83 | import File.Select as Select 84 | 85 | type Msg 86 | = ImagesRequested 87 | | ImagesLoaded File (List File) 88 | 89 | requestImages : Cmd Msg 90 | requestImages = 91 | Select.files ["image/png","image/jpg"] ImagesLoaded 92 | 93 | In this case, we only want PNG and JPG files. 94 | 95 | Notice that the function that turns the resulting files into a message takes 96 | two arguments: the first file selected and then a list of the other selected 97 | files. This guarantees that one file (or more) is available. This way you do 98 | not have to handle “no files loaded” in your code. That can never happen! 99 | 100 | **Note:** This only works when the command is the direct result of a user 101 | event, like clicking something. 102 | 103 | **Note:** This command may not resolve, partly because it is unclear how to 104 | reliably detect `Cancel` clicks across browsers. More about that in the 105 | section on [limitations](#limitations) below. 106 | -} 107 | files : List String -> (File -> List File -> msg) -> Cmd msg 108 | files mimes toMsg = 109 | Task.perform 110 | (\(f,fs) -> toMsg f fs) 111 | (Elm.Kernel.File.uploadOneOrMore mimes) 112 | --------------------------------------------------------------------------------