├── .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 |
--------------------------------------------------------------------------------