git.iko.soy/typeable/minio-hs
on master help sign in
.github/workflows/ci.ymlnew file+266
since splitsnapshots
unifiedsplit
@@ -0,0 +1,266 @@
1+name: CI
2+
3+# Trigger the workflow on push or pull request, but only for the master branch
4+on:
5+ pull_request:
6+ branches: [master]
7+ push:
8+ branches: [master]
9+
10+# This ensures that previous jobs for the PR are canceled when the PR is
11+# updated.
12+concurrency:
13+ group: ${{ github.workflow }}-${{ github.head_ref }}
14+ cancel-in-progress: true
15+
16+# Env vars for tests
17+env:
18+ MINIO_ACCESS_KEY: minio
19+ MINIO_SECRET_KEY: minio123
20+ MINIO_LOCAL: 1
21+
22+jobs:
23+ ormolu:
24+ runs-on: ubuntu-latest
25+ steps:
26+ - uses: actions/checkout@v4
27+ - uses: haskell-actions/run-ormolu@v15
28+ with:
29+ version: "0.5.0.1"
30+
31+ hlint:
32+ runs-on: ubuntu-latest
33+ steps:
34+ - uses: actions/checkout@v4
35+
36+ - name: "Set up HLint"
37+ uses: haskell-actions/hlint-setup@v2
38+ with:
39+ version: "3.5"
40+
41+ - name: "Run HLint"
42+ uses: haskell-actions/hlint-run@v2
43+ with:
44+ path: '["src/", "test/", "examples"]'
45+ fail-on: warning
46+
47+ cabal:
48+ name: ${{ matrix.os }} / ghc-${{ matrix.ghc }} / cabal-${{ matrix.cabal }}
49+ runs-on: ${{ matrix.os }}
50+ needs: ormolu
51+ strategy:
52+ matrix:
53+ os: [ubuntu-latest, windows-latest, macos-latest]
54+ cabal: ["3.8", "latest"]
55+ ghc:
56+ - "9.8"
57+ - "9.6"
58+ - "9.4"
59+ - "9.2"
60+ - "9.0"
61+ - "8.10"
62+ exclude:
63+ # macos llvm issue for versions less than 9.2
64+ - os: macos-latest
65+ ghc: "8.10"
66+ - os: macos-latest
67+ ghc: "9.0"
68+ # Cabal 3.8 supports GHC < 9.6
69+ - cabal: "3.8"
70+ ghc: "9.6"
71+ - cabal: "3.8"
72+ ghc: "9.8"
73+
74+ steps:
75+ - uses: actions/checkout@v4
76+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
77+
78+ - uses: haskell-actions/setup@v2
79+ id: setup
80+ name: Setup Haskell
81+ with:
82+ ghc-version: ${{ matrix.ghc }}
83+ cabal-version: ${{ matrix.cabal }}
84+ cabal-update: true
85+
86+ - name: Configure
87+ run: |
88+ cabal configure --enable-tests --enable-benchmarks --test-show-details=direct -fexamples -fdev -flive-test
89+ cabal build all --dry-run
90+ # The last step generates dist-newstyle/cache/plan.json for the cache key.
91+
92+ - name: Restore cached dependencies
93+ uses: actions/cache/restore@v4
94+ id: cache
95+ env:
96+ key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
97+ with:
98+ path: ${{ steps.setup.outputs.cabal-store }}
99+ key: ${{ env.key }}-plan-${{ hashFiles('**/plan.json') }}
100+ restore-keys: ${{ env.key }}-
101+
102+ - name: Install dependencies
103+ # If we had an exact cache hit, the dependencies will be up to date.
104+ if: steps.cache.outputs.cache-hit != 'true'
105+ run: cabal build all --only-dependencies
106+
107+ # Cache dependencies already here, so that we do not have to rebuild them should the subsequent steps fail.
108+ - name: Save cached dependencies
109+ uses: actions/cache/save@v4
110+ # If we had an exact cache hit, trying to save the cache would error because of key clash.
111+ if: steps.cache.outputs.cache-hit != 'true'
112+ with:
113+ path: ${{ steps.setup.outputs.cabal-store }}
114+ key: ${{ steps.cache.outputs.cache-primary-key }}
115+
116+ - name: Build
117+ run: |
118+ cabal build all
119+
120+ - name: Setup TLS certs for MinIO for testing (Linux)
121+ if: matrix.os == 'ubuntu-latest'
122+ run: |
123+ mkdir -p /tmp/minio /tmp/minio-config/certs
124+ cp test/cert/* /tmp/minio-config/certs/
125+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
126+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
127+ sudo update-ca-certificates
128+
129+ ## Currently disable TLS setup for MacOS due to issues in trusting it on MacOS.
130+ - name: Setup TLS certs for MinIO for testing (MacOS)
131+ if: matrix.os == 'macos-latest'
132+ run: |
133+ mkdir -p /tmp/minio /tmp/minio-config/certs
134+ cp test/cert/* /tmp/minio-config/certs/
135+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
136+ # sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
137+
138+ - name: Setup MinIO for testing (Windows)
139+ if: matrix.os == 'windows-latest'
140+ run: |
141+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
142+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
143+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
144+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
145+
146+ - name: Test (Linux)
147+ if: matrix.os == 'ubuntu-latest'
148+ env:
149+ MINIO_SECURE: 1
150+ run: |
151+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
152+ ghc --version
153+ cabal --version
154+ cabal test all
155+
156+ - name: Test (MacOS)
157+ if: matrix.os == 'macos-latest'
158+ # # Leave MINIO_SECURE unset to disable TLS in tests.
159+ # env:
160+ # MINIO_SECURE: 1
161+ run: |
162+ /tmp/minio/minio server --quiet data1 data2 data3 data4 2>&1 > minio.log &
163+ ghc --version
164+ cabal --version
165+ cabal test all
166+
167+ - name: Test (Windows)
168+ if: matrix.os == 'windows-latest'
169+ env:
170+ MINIO_SECURE: 1
171+ run: |
172+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
173+ ghc --version
174+ cabal --version
175+ cabal test all
176+
177+ stack:
178+ name: stack / ghc ${{ matrix.ghc }}
179+ runs-on: ${{ matrix.os }}
180+ env:
181+ MINIO_SECURE: 1
182+ strategy:
183+ matrix:
184+ ghc:
185+ - "8.10.7"
186+ - "9.0.2"
187+ - "9.2.8"
188+ - "9.4.8"
189+ - "9.6.5"
190+ - "9.8.2"
191+ os: [ubuntu-latest]
192+
193+ steps:
194+ - uses: actions/checkout@v4
195+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
196+
197+ - uses: haskell-actions/setup@v2
198+ with:
199+ ghc-version: ${{ matrix.ghc }}
200+ enable-stack: true
201+ stack-version: "latest"
202+
203+ - uses: actions/cache@v4
204+ name: Cache ~/.stack
205+ with:
206+ path: ~/.stack
207+ key: ${{ runner.os }}-stack-global-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}
208+ restore-keys: |
209+ ${{ runner.os }}-stack-global-
210+ - uses: actions/cache@v4
211+ name: Cache .stack-work
212+ with:
213+ path: .stack-work
214+ key: ${{ runner.os }}-stack-work-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}-${{ hashFiles('**/*.hs') }}
215+ restore-keys: |
216+ ${{ runner.os }}-stack-work-
217+
218+ - name: Install dependencies
219+ run: |
220+ stack --version
221+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
222+
223+ - name: Build
224+ run: |
225+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --flag minio-hs:examples --flag minio-hs:live-test --flag minio-hs:dev
226+
227+ - name: Setup MinIO for testing (Linux)
228+ if: matrix.os == 'ubuntu-latest'
229+ run: |
230+ mkdir -p /tmp/minio /tmp/minio-config/certs
231+ cp test/cert/* /tmp/minio-config/certs/
232+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
233+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
234+ sudo update-ca-certificates
235+
236+ - name: Setup MinIO for testing (MacOS)
237+ if: matrix.os == 'macos-latest'
238+ run: |
239+ mkdir -p /tmp/minio /tmp/minio-config/certs
240+ cp test/cert/* /tmp/minio-config/certs/
241+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
242+ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
243+
244+ - name: Setup MinIO for testing (Windows)
245+ if: matrix.os == 'windows-latest'
246+ run: |
247+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
248+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
249+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
250+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
251+
252+ - name: Test (Non-Windows)
253+ if: matrix.os != 'windows-latest'
254+ run: |
255+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
256+ ghc --version
257+ stack --version
258+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
259+
260+ - name: Test (Windows)
261+ if: matrix.os == 'windows-latest'
262+ run: |
263+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
264+ ghc --version
265+ cabal --version
266+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
.stylish-haskell.yamldeleted−233
@@ -1,233 +0,0 @@
1−# stylish-haskell configuration file
2−# ==================================
3−
4−# The stylish-haskell tool is mainly configured by specifying steps. These steps
5−# are a list, so they have an order, and one specific step may appear more than
6−# once (if needed). Each file is processed by these steps in the given order.
7−steps:
8− # Convert some ASCII sequences to their Unicode equivalents. This is disabled
9− # by default.
10− # - unicode_syntax:
11− # # In order to make this work, we also need to insert the UnicodeSyntax
12− # # language pragma. If this flag is set to true, we insert it when it's
13− # # not already present. You may want to disable it if you configure
14− # # language extensions using some other method than pragmas. Default:
15− # # true.
16− # add_language_pragma: true
17−
18− # Align the right hand side of some elements. This is quite conservative
19− # and only applies to statements where each element occupies a single
20− # line.
21− - simple_align:
22− cases: true
23− top_level_patterns: true
24− records: true
25−
26− # Import cleanup
27− - imports:
28− # There are different ways we can align names and lists.
29− #
30− # - global: Align the import names and import list throughout the entire
31− # file.
32− #
33− # - file: Like global, but don't add padding when there are no qualified
34− # imports in the file.
35− #
36− # - group: Only align the imports per group (a group is formed by adjacent
37− # import lines).
38− #
39− # - none: Do not perform any alignment.
40− #
41− # Default: global.
42− align: global
43−
44− # The following options affect only import list alignment.
45− #
46− # List align has following options:
47− #
48− # - after_alias: Import list is aligned with end of import including
49− # 'as' and 'hiding' keywords.
50− #
51− # > import qualified Data.List as List (concat, foldl, foldr, head,
52− # > init, last, length)
53− #
54− # - with_alias: Import list is aligned with start of alias or hiding.
55− #
56− # > import qualified Data.List as List (concat, foldl, foldr, head,
57− # > init, last, length)
58− #
59− # - new_line: Import list starts always on new line.
60− #
61− # > import qualified Data.List as List
62− # > (concat, foldl, foldr, head, init, last, length)
63− #
64− # Default: after_alias
65− list_align: after_alias
66−
67− # Right-pad the module names to align imports in a group:
68− #
69− # - true: a little more readable
70− #
71− # > import qualified Data.List as List (concat, foldl, foldr,
72− # > init, last, length)
73− # > import qualified Data.List.Extra as List (concat, foldl, foldr,
74− # > init, last, length)
75− #
76− # - false: diff-safe
77− #
78− # > import qualified Data.List as List (concat, foldl, foldr, init,
79− # > last, length)
80− # > import qualified Data.List.Extra as List (concat, foldl, foldr,
81− # > init, last, length)
82− #
83− # Default: true
84− pad_module_names: true
85−
86− # Long list align style takes effect when import is too long. This is
87− # determined by 'columns' setting.
88− #
89− # - inline: This option will put as much specs on same line as possible.
90− #
91− # - new_line: Import list will start on new line.
92− #
93− # - new_line_multiline: Import list will start on new line when it's
94− # short enough to fit to single line. Otherwise it'll be multiline.
95− #
96− # - multiline: One line per import list entry.
97− # Type with constructor list acts like single import.
98− #
99− # > import qualified Data.Map as M
100− # > ( empty
101− # > , singleton
102− # > , ...
103− # > , delete
104− # > )
105− #
106− # Default: inline
107− long_list_align: inline
108−
109− # Align empty list (importing instances)
110− #
111− # Empty list align has following options
112− #
113− # - inherit: inherit list_align setting
114− #
115− # - right_after: () is right after the module name:
116− #
117− # > import Vector.Instances ()
118− #
119− # Default: inherit
120− empty_list_align: inherit
121−
122− # List padding determines indentation of import list on lines after import.
123− # This option affects 'long_list_align'.
124− #
125− # - <integer>: constant value
126− #
127− # - module_name: align under start of module name.
128− # Useful for 'file' and 'group' align settings.
129− list_padding: 4
130−
131− # Separate lists option affects formatting of import list for type
132− # or class. The only difference is single space between type and list
133− # of constructors, selectors and class functions.
134− #
135− # - true: There is single space between Foldable type and list of it's
136− # functions.
137− #
138− # > import Data.Foldable (Foldable (fold, foldl, foldMap))
139− #
140− # - false: There is no space between Foldable type and list of it's
141− # functions.
142− #
143− # > import Data.Foldable (Foldable(fold, foldl, foldMap))
144− #
145− # Default: true
146− separate_lists: true
147−
148− # Space surround option affects formatting of import lists on a single
149− # line. The only difference is single space after the initial
150− # parenthesis and a single space before the terminal parenthesis.
151− #
152− # - true: There is single space associated with the enclosing
153− # parenthesis.
154− #
155− # > import Data.Foo ( foo )
156− #
157− # - false: There is no space associated with the enclosing parenthesis
158− #
159− # > import Data.Foo (foo)
160− #
161− # Default: false
162− space_surround: false
163−
164− # Language pragmas
165− - language_pragmas:
166− # We can generate different styles of language pragma lists.
167− #
168− # - vertical: Vertical-spaced language pragmas, one per line.
169− #
170− # - compact: A more compact style.
171− #
172− # - compact_line: Similar to compact, but wrap each line with
173− # `{-#LANGUAGE #-}'.
174− #
175− # Default: vertical.
176− style: vertical
177−
178− # Align affects alignment of closing pragma brackets.
179− #
180− # - true: Brackets are aligned in same column.
181− #
182− # - false: Brackets are not aligned together. There is only one space
183− # between actual import and closing bracket.
184− #
185− # Default: true
186− align: true
187−
188− # stylish-haskell can detect redundancy of some language pragmas. If this
189− # is set to true, it will remove those redundant pragmas. Default: true.
190− remove_redundant: true
191−
192− # Replace tabs by spaces. This is disabled by default.
193− # - tabs:
194− # # Number of spaces to use for each tab. Default: 8, as specified by the
195− # # Haskell report.
196− # spaces: 8
197−
198− # Remove trailing whitespace
199− - trailing_whitespace: {}
200−
201−# A common setting is the number of columns (parts of) code will be wrapped
202−# to. Different steps take this into account. Default: 80.
203−columns: 80
204−
205−# By default, line endings are converted according to the OS. You can override
206−# preferred format here.
207−#
208−# - native: Native newline format. CRLF on Windows, LF on other OSes.
209−#
210−# - lf: Convert to LF ("\n").
211−#
212−# - crlf: Convert to CRLF ("\r\n").
213−#
214−# Default: native.
215−newline: native
216−
217−# Sometimes, language extensions are specified in a cabal file or from the
218−# command line instead of using language pragmas in the file. stylish-haskell
219−# needs to be aware of these, so it can parse the file correctly.
220−#
221−# No language extensions are enabled by default.
222−language_extensions:
223− - BangPatterns
224− - FlexibleContexts
225− - FlexibleInstances
226− - MultiParamTypeClasses
227− - MultiWayIf
228− - NoImplicitPrelude
229− - OverloadedStrings
230− - RankNTypes
231− - ScopedTypeVariables
232− - TupleSections
233− - TypeFamilies
.travis.ymldeleted−52
@@ -1,52 +0,0 @@
1−sudo: true
2−language: haskell
3−
4−git:
5− depth: 5
6−
7−cabal: "2.4"
8−
9−cache:
10− directories:
11− - "$HOME/.cabal/store"
12− - "$HOME/.stack"
13− - "$TRAVIS_BUILD_DIR/.stack-work"
14−
15−matrix:
16− include:
17−
18− # Cabal
19− - ghc: 8.2.2
20− - ghc: 8.4.4
21− - ghc: 8.6.5
22−
23− # Stack
24− - ghc: 8.6.5
25− env: STACK_YAML="$TRAVIS_BUILD_DIR/stack.yaml"
26−
27−install:
28− - |
29− if [ -z "$STACK_YAML" ]; then
30− ghc --version
31− cabal --version
32− cabal new-update
33− cabal new-build --enable-tests --enable-benchmarks
34− else
35− # install stack
36− curl -sSL https://get.haskellstack.org/ | sh
37−
38− # build project with stack
39− stack --version
40− stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks
41− fi
42−
43−script:
44− - |
45− if [ -z "$STACK_YAML" ]; then
46− cabal new-test --enable-tests
47− else
48− stack test --system-ghc
49− fi
50−
51−notifications:
52− email: false
CHANGELOG.md+62
@@ -1,6 +1,68 @@
1Changelog
1Changelog
2==========
2==========
3
3
4+## Version 1.7.0 -- Unreleased
5+
6+* Fix data type `EventMessage` to not export partial fields (#179)
7+* Bump up min bound on time dep and fix deprecation warnings (#181)
8+* Add `dev` flag to cabal for building with warnings as errors (#182)
9+* Fix AWS region map (#185)
10+* Fix XML generator tests (#187)
11+* Add support for STS Assume Role API (#188)
12+
13+### Breaking changes in 1.7.0
14+
15+* `Credentials` type has been removed. Use `CredentialValue` instead.
16+* `Provider` type has been replaced with `CredentialLoader`.
17+* `EventMessage` data type is updated.
18+
19+## Version 1.6.0
20+
21+* HLint fixes - some types were changed to newtype (#173)
22+* Fix XML generation test for S3 SELECT (#161)
23+* Use region specific endpoints for AWS S3 in presigned Urls (#164)
24+* Replace protolude with relude and build with GHC 9.0.2 (#168)
25+* Support aeson 2 (#169)
26+* CI updates and code formatting changes with ormolu 0.5.0.0
27+
28+## Version 1.5.3
29+
30+* Fix windows build
31+* Fix support for Yandex Storage (#147)
32+* Fix for HEAD requests to S3/Minio (#155)
33+* Bump up some dependencies, new code formatting, Github CI, example fixes and other minor improvements.
34+
35+## Version 1.5.2
36+
37+* Fix region `us-west-2` for AWS S3 (#139)
38+* Build examples in CI
39+* Disable live-server tests by default, but run them in CI
40+* Drop support for GHC 8.2.x
41+
42+## Version 1.5.1
43+
44+* Add support for GHC 8.8
45+
46+## Version 1.5.0
47+
48+* Switch to faster map data type - all previous usage of
49+ Data.Map.Strict and Data.Set is replaced with Data.HashMap.Strict
50+ and Data.HashSet.
51+* Add `oiUserMetadata` to parse and return user metadata stored with
52+ an object.
53+* Add `GetObjectResponse` data type for the value returned by
54+ `getObject`. It now contains parsed ObjectInfo along with the
55+ conduit of object bytes.
56+
57+## Version 1.4.0
58+
59+* Expose runMinioRes and runMinioResWith (#129)
60+* Improve Haddocks (#127)
61+* Fix list objects APIs to return directory prefixes when run with
62+ recurse set to False (#126)
63+* Use streaming signature for streaming payloads when on an insecure
64+ connection (#123)
65+
4## Version 1.3.1
66## Version 1.3.1
5
67
6* Add TLS helpers to check if server uses TLS, and to disable
68* Add TLS helpers to check if server uses TLS, and to disable
CNAMEnew file+1
@@ -0,0 +1 @@
1+minio-hs.min.io
README.md+98−31
@@ -1,53 +1,65 @@
1−# MinIO Client SDK for Haskell [![Build Status](https://travis-ci.org/minio/minio-hs.svg?branch=master)](https://travis-ci.org/minio/minio-hs)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
1+# MinIO Haskell Client SDK for Amazon S3 Compatible Cloud Storage [![CI](https://github.com/minio/minio-hs/actions/workflows/ci.yml/badge.svg)](https://github.com/minio/minio-hs/actions/workflows/ci.yml)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
2
2
3−The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and Amazon S3 compatible object storage server.
3+The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
4
4
5−## Minimum Requirements
5+This guide assumes that you have a working [Haskell development environment](https://www.haskell.org/downloads/).
6−
7−- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
8
6
9## Installation
7## Installation
10
8
11−```sh
9+### Add to your project
12−git clone https://github.com/minio/minio-hs.git
13
10
14−cd minio-hs/
11+Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
15
12
16−stack install
13+### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
17−```
18
14
19−Tests can be run with:
15+#### For a cabal based environment
20
16
21−```sh
17+Download the library source and change to the extracted directory:
22
18
23−stack test
19+``` sh
20+$ cabal get minio-hs
21+$ cd minio-hs-1.6.0/ # directory name could be different
22+```
24
23
24+Then load the `ghci` REPL environment with the library and browse the available APIs:
25+
26+``` sh
27+$ cabal repl
28+ghci> :browse Network.Minio
25```
29```
26
30
27−A section of the tests use the remote MinIO Play server at
31+#### For a stack based environment
28−`https://play.min.io:9000` by default. For library development,
29−using this remote server maybe slow. To run the tests against a
30−locally running MinIO live server at `http://localhost:9000`, just set
31−the environment `MINIO_LOCAL` to any value (and unset it to switch
32−back to Play).
33
32
34−Documentation can be locally built with:
33+From your home folder or any non-haskell project directory, just run:
35
34
36```sh
35```sh
36+stack install minio-hs
37+```
37
38
38−stack haddock
39+Then start an interpreter session and browse the available APIs with:
39
40
41+```sh
42+$ stack ghci
43+> :browse Network.Minio
40```
44```
41
45
46+## Examples
47+
48+The [examples](https://github.com/minio/minio-hs/tree/master/examples) folder contains many examples that you can try out and use to learn and to help with developing your own projects.
49+
42−## Quick-Start Example - File Uploader
50+### Quick-Start Example - File Uploader
51+
52+This example program connects to a MinIO object storage server, makes a bucket on the server and then uploads a file to the bucket.
53+
54+We will use the MinIO server running at https://play.min.io in this example. Feel free to use this service for testing and development. Access credentials are present in the library and are open to the public.
43
55
44### FileUploader.hs
56### FileUploader.hs
45``` haskell
57``` haskell
46#!/usr/bin/env stack
58#!/usr/bin/env stack
47−-- stack --resolver lts-11.1 runghc --package minio-hs --package optparse-applicative --package filepath
59+-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
48
60
49--
61--
50−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
62+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
51--
63--
52-- Licensed under the Apache License, Version 2.0 (the "License");
64-- Licensed under the Apache License, Version 2.0 (the "License");
53-- you may not use this file except in compliance with the License.
65-- you may not use this file except in compliance with the License.
@@ -76,7 +88,7 @@ import UnliftIO (throwIO, try)
76import Prelude
88import Prelude
77
89
78-- | The following example uses minio's play server at
90-- | The following example uses minio's play server at
79−-- https://play.min.io:9000. The endpoint and associated
91+-- https://play.min.io. The endpoint and associated
80-- credentials are provided via the libary constant,
92-- credentials are provided via the libary constant,
81--
93--
82-- > minioPlayCI :: ConnectInfo
94-- > minioPlayCI :: ConnectInfo
@@ -106,16 +118,19 @@ main = do
106 res <- runMinio minioPlayCI $ do
118 res <- runMinio minioPlayCI $ do
107 -- Make a bucket; catch bucket already exists exception if thrown.
119 -- Make a bucket; catch bucket already exists exception if thrown.
108 bErr <- try $ makeBucket bucket Nothing
120 bErr <- try $ makeBucket bucket Nothing
121+
122+ -- If the bucket already exists, we would get a specific
123+ -- `ServiceErr` exception thrown.
109 case bErr of
124 case bErr of
110− Left (MErrService BucketAlreadyOwnedByYou) -> return ()
125+ Left BucketAlreadyOwnedByYou -> return ()
111− Left e -> throwIO e
126+ Left e -> throwIO e
112− Right _ -> return ()
127+ Right _ -> return ()
113
128
114− -- Upload filepath to bucket; object is derived from filepath.
129+ -- Upload filepath to bucket; object name is derived from filepath.
115− fPutObject bucket object filepath def
130+ fPutObject bucket object filepath defaultPutObjectOptions
116
131
117 case res of
132 case res of
118− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
133+ Left e -> putStrLn $ "file upload failed due to " ++ show e
119 Right () -> putStrLn "file upload succeeded."
134 Right () -> putStrLn "file upload succeeded."
120```
135```
121
136
@@ -129,3 +144,55 @@ main = do
129## Contribute
144## Contribute
130
145
131[Contributors Guide](https://github.com/minio/minio-hs/blob/master/CONTRIBUTING.md)
146[Contributors Guide](https://github.com/minio/minio-hs/blob/master/CONTRIBUTING.md)
147+
148+### Development
149+
150+#### Download the source
151+
152+```sh
153+$ git clone https://github.com/minio/minio-hs.git
154+$ cd minio-hs/
155+```
156+
157+#### Build the package:
158+
159+With `cabal`:
160+
161+```sh
162+$ # Configure cabal for development enabling all optional flags defined by the package.
163+$ cabal configure --enable-tests --test-show-details=direct -fexamples -fdev -flive-test
164+$ cabal build
165+```
166+
167+With `stack`:
168+
169+``` sh
170+$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
171+```
172+#### Running tests:
173+
174+A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000` with the credentials `access_key=minio` and `secret_key=minio123`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
175+
176+With `cabal`:
177+
178+```sh
179+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
180+$ cabal test
181+```
182+
183+With `stack`:
184+
185+``` sh
186+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
187+stack test --flag minio-hs:live-test --flag minio-hs:dev
188+```
189+
190+This will run all the test suites.
191+
192+#### Building documentation:
193+
194+```sh
195+$ cabal haddock
196+$ # OR
197+$ stack haddock
198+```
Setup.hsdeleted−18
@@ -1,18 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−import Distribution.Simple
18−main = defaultMain
docs/API.md+183−182
@@ -20,15 +20,15 @@ awsCI { connectAccesskey = "your-access-key"
20
20
21```
21```
22
22
23−|Bucket operations|Object Operations|Presigned Operations|
23+| Bucket operations | Object Operations | Presigned Operations |
24−|:---|:---|:---|
24+|:--------------------------------------------------|:----------------------------------------------|:--------------------------------------------------|
25−|[`listBuckets`](#listBuckets) |[`getObject`](#getObject)|[`presignedGetObjectUrl`](#presignedGetObjectUrl)|
25+| [`listBuckets`](#listBuckets) | [`getObject`](#getObject) | [`presignedGetObjectUrl`](#presignedGetObjectUrl) |
26−|[`makeBucket`](#makeBucket)|[`putObject`](#putObject)|[`presignedPutObjectUrl`](#presignedPutObjectUrl)|
26+| [`makeBucket`](#makeBucket) | [`putObject`](#putObject) | [`presignedPutObjectUrl`](#presignedPutObjectUrl) |
27−|[`removeBucket`](#removeBucket)|[`fGetObject`](#fGetObject)|[`presignedPostPolicy`](#presignedPostPolicy)|
27+| [`removeBucket`](#removeBucket) | [`fGetObject`](#fGetObject) | [`presignedPostPolicy`](#presignedPostPolicy) |
28−|[`listObjects`](#listObjects)|[`fPutObject`](#fPutObject)||
28+| [`listObjects`](#listObjects) | [`fPutObject`](#fPutObject) | |
29−|[`listObjectsV1`](#listObjectsV1)|[`copyObject`](#copyObject)||
29+| [`listObjectsV1`](#listObjectsV1) | [`copyObject`](#copyObject) | |
30−|[`listIncompleteUploads`](#listIncompleteUploads)|[`removeObject`](#removeObject)||
30+| [`listIncompleteUploads`](#listIncompleteUploads) | [`removeObject`](#removeObject) | |
31−|[`bucketExists`](#bucketExists)|[`selectObjectContent`](#selectObjectContent)||
31+| [`bucketExists`](#bucketExists) | [`selectObjectContent`](#selectObjectContent) | |
32
32
33## 1. Connecting and running operations on the storage service
33## 1. Connecting and running operations on the storage service
34
34
@@ -69,16 +69,16 @@ enable/disable the automatic region discovery behaviour.
69
69
70The parameters in the expression `awsWithRegion region autoDiscover` are:
70The parameters in the expression `awsWithRegion region autoDiscover` are:
71
71
72−|Parameter|Type|Description|
72+| Parameter | Type | Description |
73−|:---|:---|:---|
73+|:---------------|:----------------------------|:---------------------------------------------------------------------------------------------------------------------------|
74−| `region` | _Region_ (alias for `Text`) | The region to connect to by default for all requests. |
74+| `region` | _Region_ (alias for `Text`) | The region to connect to by default for all requests. |
75−| `autoDiscover` | _Bool_ | If `True`, region discovery will be enabled. If `False`, discovery is disabled, and all requests go the given region only.|
75+| `autoDiscover` | _Bool_ | If `True`, region discovery will be enabled. If `False`, discovery is disabled, and all requests go the given region only. |
76
76
77#### minioPlayCI :: ConnectInfo
77#### minioPlayCI :: ConnectInfo
78
78
79This constructor provides connection and authentication information to
79This constructor provides connection and authentication information to
80connect to the public MinIO Play server at
80connect to the public MinIO Play server at
81−`https://play.min.io:9000/`.
81+`https://play.min.io/`.
82
82
83#### minioCI :: Text -> Int -> Bool -> ConnectInfo
83#### minioCI :: Text -> Int -> Bool -> ConnectInfo
84
84
@@ -86,25 +86,25 @@ Use to connect to a MinIO server.
86
86
87The parameters in the expression `minioCI host port isSecure` are:
87The parameters in the expression `minioCI host port isSecure` are:
88
88
89−|Parameter|Type|Description|
89+| Parameter | Type | Description |
90−|:---|:---|:---|
90+|:-----------|:-------|:--------------------------------------------------------|
91−| `host` | _Text_ | Hostname of the MinIO or other S3-API compatible server |
91+| `host` | _Text_ | Hostname of the MinIO or other S3-API compatible server |
92−| `port` | _Int_ | Port number to connect to|
92+| `port` | _Int_ | Port number to connect to |
93−| `isSecure` | _Bool_ | Does the server use HTTPS? |
93+| `isSecure` | _Bool_ | Does the server use HTTPS? |
94
94
95#### The ConnectInfo fields and Default instance
95#### The ConnectInfo fields and Default instance
96
96
97The following table shows the fields in the `ConnectInfo` record-type:
97The following table shows the fields in the `ConnectInfo` record-type:
98
98
99−| Field | Type | Description |
99+| Field | Type | Description |
100−|:---|:---|:---|
100+|:----------------------------|:----------------------------|:--------------------------------------------------------------------------------------------------|
101−| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
101+| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
102−| `connectPort` | _Int_ | Port number on which the server listens. Defaults to `9000`. |
102+| `connectPort` | _Int_ | Port number on which the server listens. Defaults to `9000`. |
103−| `connectAccessKey` | _Text_ | Access key to use in authentication. Defaults to `minio`. |
103+| `connectAccessKey` | _Text_ | Access key to use in authentication. Defaults to `minio`. |
104−| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
104+| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
105−| `connectIsSecure` | _Bool_ | Specifies if the server used TLS. Defaults to `False` |
105+| `connectIsSecure` | _Bool_ | Specifies if the server used TLS. Defaults to `False` |
106−| `connectRegion` | _Region_ (alias for `Text`) | Specifies the region to use. Defaults to 'us-east-1' |
106+| `connectRegion` | _Region_ (alias for `Text`) | Specifies the region to use. Defaults to 'us-east-1' |
107−| `connectAutoDiscoverRegion` | _Bool_ | Specifies if the library should automatically discover the region of a bucket. Defaults to `True`|
107+| `connectAutoDiscoverRegion` | _Bool_ | Specifies if the library should automatically discover the region of a bucket. Defaults to `True` |
108
108
109The `def` value of type `ConnectInfo` has all the above default
109The `def` value of type `ConnectInfo` has all the above default
110values.
110values.
@@ -148,17 +148,17 @@ Lists buckets.
148
148
149__Return Value__
149__Return Value__
150
150
151−|Return type |Description |
151+| Return type | Description |
152−|:---|:---|
152+|:---------------------|:----------------|
153−| _Minio [BucketInfo]_| List of buckets |
153+| _Minio [BucketInfo]_ | List of buckets |
154
154
155
155
156__BucketInfo record type__
156__BucketInfo record type__
157
157
158−|Field |Type |Description |
158+| Field | Type | Description |
159−|:---|:---| :---|
159+|:-----------------|:---------------------------|:----------------------------|
160−| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
160+| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
161−| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
161+| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
162
162
163
163
164<a name="makeBucket"></a>
164<a name="makeBucket"></a>
@@ -171,10 +171,10 @@ __Parameters__
171
171
172In the expression `makeBucket bucketName region` the arguments are:
172In the expression `makeBucket bucketName region` the arguments are:
173
173
174−| Param | Type | Description |
174+| Param | Type | Description |
175−|---|---|---|
175+|--------------|-----------------------------|-----------------------------------------------------------------------------------------------------|
176−|`bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
176+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
177−| `region` | _Maybe Region_ | Region where the bucket is to be created. If not specified, default to the region in `ConnectInfo`.|
177+| `region` | _Maybe Region_ | Region where the bucket is to be created. If not specified, default to the region in `ConnectInfo`. |
178
178
179__Example__
179__Example__
180
180
@@ -200,9 +200,9 @@ __Parameters__
200
200
201In the expression `removeBucket bucketName` the arguments are:
201In the expression `removeBucket bucketName` the arguments are:
202
202
203−| Param | Type | Description |
203+| Param | Type | Description |
204−|---|---|---|
204+|--------------|-----------------------------|--------------------|
205−|`bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
205+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
206
206
207
207
208__Example__
208__Example__
@@ -233,26 +233,27 @@ __Parameters__
233In the expression `listObjects bucketName prefix recursive` the
233In the expression `listObjects bucketName prefix recursive` the
234arguments are:
234arguments are:
235
235
236−|Param |Type |Description |
236+| Param | Type | Description |
237−|:---|:---| :---|
237+|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
238−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
238+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
239−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
239+| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
240−| `recursive` | _Bool_ |`True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
240+| `recursive` | _Bool_ | `True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
241
241
242__Return Value__
242__Return Value__
243
243
244−|Return type |Description |
244+| Return type | Description |
245−|:---|:---|
245+|:------------------------------------|:------------------------------------------------------------------------|
246−| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
246+| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
247
247
248__ObjectInfo record type__
248__ObjectInfo record type__
249
249
250−|Field |Type |Description |
250+| Field | Type | Description |
251−|:---|:---| :---|
251+|:-------------|:----------------------------|:-------------------------------------|
252−|`oiObject` | _Object_ (alias for `Text`) | Name of object |
252+| `oiObject` | _Object_ (alias for `Text`) | Name of object |
253−|`oiModTime` | _UTCTime_ | Last modified time of the object |
253+| `oiModTime` | _UTCTime_ | Last modified time of the object |
254−|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
254+| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
255−|`oiSize` | _Int64_ | Size of the object in bytes |
255+| `oiSize` | _Int64_ | Size of the object in bytes |
256+| `oiMetadata` | _HashMap Text Text_ | Map of key-value user-metadata pairs |
256
257
257__Example__
258__Example__
258
259
@@ -265,7 +266,7 @@ import Prelude
265
266
266
267
267-- | The following example uses MinIO play server at
268-- | The following example uses MinIO play server at
268−-- https://play.min.io:9000. The endpoint and associated
269+-- https://play.min.io. The endpoint and associated
269-- credentials are provided via the libary constant,
270-- credentials are provided via the libary constant,
270--
271--
271-- > minioPlayCI :: ConnectInfo
272-- > minioPlayCI :: ConnectInfo
@@ -294,26 +295,26 @@ __Parameters__
294In the expression `listObjectsV1 bucketName prefix recursive` the
295In the expression `listObjectsV1 bucketName prefix recursive` the
295arguments are:
296arguments are:
296
297
297−|Param |Type |Description |
298+| Param | Type | Description |
298−|:---|:---| :---|
299+|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
299−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
300+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
300−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
301+| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
301−| `recursive` | _Bool_ |`True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
302+| `recursive` | _Bool_ | `True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
302
303
303__Return Value__
304__Return Value__
304
305
305−|Return type |Description |
306+| Return type | Description |
306−|:---|:---|
307+|:------------------------------------|:------------------------------------------------------------------------|
307−| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
308+| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
308
309
309__ObjectInfo record type__
310__ObjectInfo record type__
310
311
311−|Field |Type |Description |
312+| Field | Type | Description |
312−|:---|:---| :---|
313+|:------------|:----------------------------|:---------------------------------|
313−|`oiObject` | _Object_ (alias for `Text`) | Name of object |
314+| `oiObject` | _Object_ (alias for `Text`) | Name of object |
314−|`oiModTime` | _UTCTime_ | Last modified time of the object |
315+| `oiModTime` | _UTCTime_ | Last modified time of the object |
315−|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
316+| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
316−|`oiSize` | _Int64_ | Size of the object in bytes |
317+| `oiSize` | _Int64_ | Size of the object in bytes |
317
318
318__Example__
319__Example__
319
320
@@ -326,7 +327,7 @@ import Prelude
326
327
327
328
328-- | The following example uses MinIO play server at
329-- | The following example uses MinIO play server at
329−-- https://play.min.io:9000. The endpoint and associated
330+-- https://play.min.io. The endpoint and associated
330-- credentials are provided via the libary constant,
331-- credentials are provided via the libary constant,
331--
332--
332-- > minioPlayCI :: ConnectInfo
333-- > minioPlayCI :: ConnectInfo
@@ -354,11 +355,11 @@ __Parameters__
354In the expression `listIncompleteUploads bucketName prefix recursive`
355In the expression `listIncompleteUploads bucketName prefix recursive`
355the parameters are:
356the parameters are:
356
357
357−|Param |Type |Description |
358+| Param | Type | Description |
358−|:---|:---| :---|
359+|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
359−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
360+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
360−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have. |
361+| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have. |
361−| `recursive` | _Bool_ |`True` indicates recursive style listing and `Talse` indicates directory style listing delimited by '/'. |
362+| `recursive` | _Bool_ | `True` indicates recursive style listing and `Talse` indicates directory style listing delimited by '/'. |
362
363
363__Return Value__
364__Return Value__
364
365
@@ -368,11 +369,11 @@ __Return Value__
368
369
369__UploadInfo record type__
370__UploadInfo record type__
370
371
371−|Field |Type |Description |
372+| Field | Type | Description |
372−|:---|:---| :---|
373+|:-------------|:---------|:------------------------------------------|
373−|`uiKey` | _Object_ |Name of incompletely uploaded object |
374+| `uiKey` | _Object_ | Name of incompletely uploaded object |
374−|`uiUploadId` | _String_ |Upload ID of incompletely uploaded object |
375+| `uiUploadId` | _String_ | Upload ID of incompletely uploaded object |
375−|`uiSize` | _Int64_ |Size of incompletely uploaded object |
376+| `uiSize` | _Int64_ | Size of incompletely uploaded object |
376
377
377__Example__
378__Example__
378
379
@@ -384,7 +385,7 @@ import Conduit
384import Prelude
385import Prelude
385
386
386-- | The following example uses MinIO play server at
387-- | The following example uses MinIO play server at
387−-- https://play.min.io:9000. The endpoint and associated
388+-- https://play.min.io. The endpoint and associated
388-- credentials are provided via the libary constant,
389-- credentials are provided via the libary constant,
389--
390--
390-- > minioPlayCI :: ConnectInfo
391-- > minioPlayCI :: ConnectInfo
@@ -415,29 +416,29 @@ __Parameters__
415In the expression `getObject bucketName objectName opts` the parameters
416In the expression `getObject bucketName objectName opts` the parameters
416are:
417are:
417
418
418−|Param |Type |Description |
419+| Param | Type | Description |
419−|:---|:---| :---|
420+|:-------------|:----------------------------|:----------------------------------------------------------------------------|
420−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
421+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
421−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
422+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
422−| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
423+| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
423
424
424__GetObjectOptions record type__
425__GetObjectOptions record type__
425
426
426−|Field |Type |Description |
427+| Field | Type | Description |
427−|:---|:---| :---|
428+|:-----------------------|:--------------------------------|:------------------------------------------------------------------------------------------------------|
428−| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object|
429+| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object |
429−| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
430+| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
430−| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
431+| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
431−| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
432+| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
432−| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
433+| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
433
434
434__Return Value__
435__Return Value__
435
436
436The return value can be incrementally read to process the contents of
437The return value can be incrementally read to process the contents of
437the object.
438the object.
438−|Return type |Description |
439+| Return type | Description |
439−|:---|:---|
440+|:--------------------------------------------|:-----------------------------------------|
440−| _Minio (C.ConduitM () ByteString Minio ())_ | A Conduit source of `ByteString` values. |
441+| _Minio (C.ConduitM () ByteString Minio ())_ | A Conduit source of `ByteString` values. |
441
442
442__Example__
443__Example__
443
444
@@ -451,7 +452,7 @@ import qualified Data.Conduit.Binary as CB
451import Prelude
452import Prelude
452
453
453-- | The following example uses MinIO play server at
454-- | The following example uses MinIO play server at
454−-- https://play.min.io:9000. The endpoint and associated
455+-- https://play.min.io. The endpoint and associated
455-- credentials are provided via the libary constant,
456-- credentials are provided via the libary constant,
456--
457--
457-- > minioPlayCI :: ConnectInfo
458-- > minioPlayCI :: ConnectInfo
@@ -482,13 +483,13 @@ __Parameters__
482In the expression `putObject bucketName objectName inputSrc` the parameters
483In the expression `putObject bucketName objectName inputSrc` the parameters
483are:
484are:
484
485
485−|Param |Type |Description |
486+| Param | Type | Description |
486−|:---|:---| :---|
487+|:-------------|:------------------------------------|:------------------------------------------------------------------|
487−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
488+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
488−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
489+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
489−| `inputSrc` | _C.ConduitM () ByteString Minio ()_ | A Conduit producer of `ByteString` values |
490+| `inputSrc` | _C.ConduitM () ByteString Minio ()_ | A Conduit producer of `ByteString` values |
490−| `size` | _Int64_ | Provide stream size (optional) |
491+| `size` | _Int64_ | Provide stream size (optional) |
491−| `opts` | _PutObjectOptions_ | Optional parameters to provide additional metadata for the object |
492+| `opts` | _PutObjectOptions_ | Optional parameters to provide additional metadata for the object |
492
493
493__Example__
494__Example__
494
495
@@ -501,7 +502,7 @@ import qualified Data.Conduit.Combinators as CC
501import Prelude
502import Prelude
502
503
503-- | The following example uses MinIO play server at
504-- | The following example uses MinIO play server at
504−-- https://play.min.io:9000. The endpoint and associated
505+-- https://play.min.io. The endpoint and associated
505-- credentials are provided via the libary constant,
506-- credentials are provided via the libary constant,
506--
507--
507-- > minioPlayCI :: ConnectInfo
508-- > minioPlayCI :: ConnectInfo
@@ -533,23 +534,23 @@ __Parameters__
533In the expression `fGetObject bucketName objectName inputFile` the parameters
534In the expression `fGetObject bucketName objectName inputFile` the parameters
534are:
535are:
535
536
536−|Param |Type |Description |
537+| Param | Type | Description |
537−|:---|:---| :---|
538+|:-------------|:----------------------------|:----------------------------------------------------------------------------|
538−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
539+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
539−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
540+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
540−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
541+| `inputFile` | _FilePath_ | Path to the file to be uploaded |
541−| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
542+| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
542
543
543
544
544__GetObjectOptions record type__
545__GetObjectOptions record type__
545
546
546−|Field |Type |Description |
547+| Field | Type | Description |
547−|:---|:---| :---|
548+|:-----------------------|:--------------------------------|:------------------------------------------------------------------------------------------------------|
548−| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object|
549+| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object |
549−| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
550+| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
550−| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
551+| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
551−| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
552+| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
552−| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
553+| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
553
554
554``` haskell
555``` haskell
555
556
@@ -561,7 +562,7 @@ import Data.Conduit.Binary (sinkLbs)
561import Prelude
562import Prelude
562
563
563-- | The following example uses MinIO play server at
564-- | The following example uses MinIO play server at
564−-- https://play.min.io:9000. The endpoint and associated
565+-- https://play.min.io. The endpoint and associated
565-- credentials are provided via the libary constant,
566-- credentials are provided via the libary constant,
566--
567--
567-- > minioPlayCI :: ConnectInfo
568-- > minioPlayCI :: ConnectInfo
@@ -592,11 +593,11 @@ __Parameters__
592In the expression `fPutObject bucketName objectName inputFile` the parameters
593In the expression `fPutObject bucketName objectName inputFile` the parameters
593are:
594are:
594
595
595−|Param |Type |Description |
596+| Param | Type | Description |
596−|:---|:---| :---|
597+|:-------------|:----------------------------|:--------------------------------|
597−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
598+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
598−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
599+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
599−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
600+| `inputFile` | _FilePath_ | Path to the file to be uploaded |
600
601
601__Example__
602__Example__
602
603
@@ -629,28 +630,28 @@ __Parameters__
629In the expression `copyObject dstInfo srcInfo` the parameters
630In the expression `copyObject dstInfo srcInfo` the parameters
630are:
631are:
631
632
632−|Param |Type |Description |
633+| Param | Type | Description |
633−|:---|:---| :---|
634+|:----------|:------------------|:----------------------------------------------------------|
634| `dstInfo` | _DestinationInfo_ | A value representing properties of the destination object |
635| `dstInfo` | _DestinationInfo_ | A value representing properties of the destination object |
635−| `srcInfo` | _SourceInfo_ | A value representing properties of the source object |
636+| `srcInfo` | _SourceInfo_ | A value representing properties of the source object |
636
637
637
638
638__SourceInfo record type__
639__SourceInfo record type__
639
640
640−|Field |Type |Description |
641+| Field | Type | Description |
641−|:---|:---| :---|
642+|:-----------------------|:-----------------------|:----------------------------------------------------------------------------------------------------------|
642−| `srcBucket` | `Bucket` | Name of source bucket |
643+| `srcBucket` | `Bucket` | Name of source bucket |
643−| `srcObject` | `Object` | Name of source object |
644+| `srcObject` | `Object` | Name of source object |
644−| `srcRange` | `Maybe (Int64, Int64)` | (Optional) Represents the byte range of source object. (0, 9) represents first ten bytes of source object|
645+| `srcRange` | `Maybe (Int64, Int64)` | (Optional) Represents the byte range of source object. (0, 9) represents first ten bytes of source object |
645−| `srcIfMatch` | `Maybe Text` | (Optional) ETag source object should match |
646+| `srcIfMatch` | `Maybe Text` | (Optional) ETag source object should match |
646−| `srcIfNoneMatch` | `Maybe Text` | (Optional) ETag source object shouldn't match |
647+| `srcIfNoneMatch` | `Maybe Text` | (Optional) ETag source object shouldn't match |
647−| `srcIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since source object wasn't modified |
648+| `srcIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since source object wasn't modified |
648−| `srcIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since source object was modified |
649+| `srcIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since source object was modified |
649
650
650__Destination record type__
651__Destination record type__
651
652
652−|Field |Type |Description |
653+| Field | Type | Description |
653−|:---|:---| :---|
654+|:------------|:---------|:-----------------------------------------------------|
654| `dstBucket` | `Bucket` | Name of destination bucket in server-side copyObject |
655| `dstBucket` | `Bucket` | Name of destination bucket in server-side copyObject |
655| `dstObject` | `Object` | Name of destination object in server-side copyObject |
656| `dstObject` | `Object` | Name of destination object in server-side copyObject |
656
657
@@ -684,10 +685,10 @@ __Parameters__
684In the expression `removeObject bucketName objectName` the parameters
685In the expression `removeObject bucketName objectName` the parameters
685are:
686are:
686
687
687−|Param |Type |Description |
688+| Param | Type | Description |
688−|:---|:---| :---|
689+|:-------------|:----------------------------|:-------------------|
689−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
690+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
690−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
691+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
691
692
692__Example__
693__Example__
693
694
@@ -718,10 +719,10 @@ __Parameters__
718In the expression `removeIncompleteUpload bucketName objectName` the parameters
719In the expression `removeIncompleteUpload bucketName objectName` the parameters
719are:
720are:
720
721
721−|Param |Type |Description |
722+| Param | Type | Description |
722−|:---|:---| :---|
723+|:-------------|:----------------------------|:-------------------|
723−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
724+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
724−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
725+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
725
726
726__Example__
727__Example__
727
728
@@ -752,11 +753,11 @@ __Parameters__
752In the expression `selectObjectContent bucketName objectName selReq`
753In the expression `selectObjectContent bucketName objectName selReq`
753the parameters are:
754the parameters are:
754
755
755−|Param |Type |Description |
756+| Param | Type | Description |
756−|:---|:---| :---|
757+|:-------------|:----------------------------|:--------------------------|
757−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
758+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
758−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
759+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
759−| `selReq` | _SelectRequest_ | Select request parameters |
760+| `selReq` | _SelectRequest_ | Select request parameters |
760
761
761__SelectRequest record__
762__SelectRequest record__
762
763
@@ -804,9 +805,9 @@ __Parameters__
804
805
805In the expression `bucketExists bucketName` the parameters are:
806In the expression `bucketExists bucketName` the parameters are:
806
807
807−|Param |Type |Description |
808+| Param | Type | Description |
808−|:---|:---| :---|
809+|:-------------|:----------------------------|:-------------------|
809−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
810+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
810
811
811
812
812## 4. Presigned operations
813## 4. Presigned operations
@@ -828,22 +829,22 @@ __Parameters__
828In the expression `presignedGetObjectUrl bucketName objectName expiry queryParams headers`
829In the expression `presignedGetObjectUrl bucketName objectName expiry queryParams headers`
829the parameters are:
830the parameters are:
830
831
831−|Param |Type |Description |
832+| Param | Type | Description |
832−|:---|:---| :---|
833+|:--------------|:---------------------------------------------------------------|:------------------------------------------------|
833−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
834+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
834−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
835+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
835−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
836+| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
836−| `queryParams` | _Query_ (from package `http-types:Network.HTTP.Types`) | Query parameters to add to the URL |
837+| `queryParams` | _Query_ (from package `http-types:Network.HTTP.Types`) | Query parameters to add to the URL |
837−| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
838+| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
838
839
839__Return Value__
840__Return Value__
840
841
841Returns the generated URL - it will include authentication
842Returns the generated URL - it will include authentication
842information.
843information.
843
844
844−|Return type |Description |
845+| Return type | Description |
845−|:---|:---|
846+|:-------------|:------------------------|
846−| _ByteString_ | Generated presigned URL |
847+| _ByteString_ | Generated presigned URL |
847
848
848__Example__
849__Example__
849
850
@@ -886,21 +887,21 @@ __Parameters__
886In the expression `presignedPutObjectUrl bucketName objectName expiry headers`
887In the expression `presignedPutObjectUrl bucketName objectName expiry headers`
887the parameters are:
888the parameters are:
888
889
889−|Param |Type |Description |
890+| Param | Type | Description |
890−|:---|:---| :---|
891+|:-------------|:---------------------------------------------------------------|:------------------------------------------------|
891−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
892+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
892−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
893+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
893−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
894+| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
894−| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
895+| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
895
896
896__Return Value__
897__Return Value__
897
898
898Returns the generated URL - it will include authentication
899Returns the generated URL - it will include authentication
899information.
900information.
900
901
901−|Return type |Description |
902+| Return type | Description |
902−|:---|:---|
903+|:-------------|:------------------------|
903−| _ByteString_ | Generated presigned URL |
904+| _ByteString_ | Generated presigned URL |
904
905
905__Example__
906__Example__
906
907
@@ -928,7 +929,7 @@ main = do
928```
929```
929
930
930<a name="presignedPostPolicy"></a>
931<a name="presignedPostPolicy"></a>
931−### presignedPostPolicy :: PostPolicy -> Minio (ByteString, Map.Map Text ByteString)
932+### presignedPostPolicy :: PostPolicy -> Minio (ByteString, HashMap Text ByteString)
932
933
933Generate a presigned URL and POST policy to upload files via a POST
934Generate a presigned URL and POST policy to upload files via a POST
934request. This is intended for browser uploads and generates form data
935request. This is intended for browser uploads and generates form data
@@ -940,10 +941,10 @@ The `PostPolicy` argument is created using the `newPostPolicy` function:
940
941
941In the expression `newPostPolicy expirationTime conditions` the parameters are:
942In the expression `newPostPolicy expirationTime conditions` the parameters are:
942
943
943−|Param | Type| Description |
944+| Param | Type | Description |
944−|:---|:---|:---|
945+|:-----------------|:--------------------------------------------------|:---------------------------------------------|
945−| `expirationTime` | _UTCTime_ (from package `time:Data.Time.UTCTime`) | The expiration time for the policy |
946+| `expirationTime` | _UTCTime_ (from package `time:Data.Time.UTCTime`) | The expiration time for the policy |
946−| `conditions` | _[PostPolicyConditions]_ | List of conditions to be added to the policy |
947+| `conditions` | _[PostPolicyConditions]_ | List of conditions to be added to the policy |
947
948
948The policy conditions are created using various helper functions -
949The policy conditions are created using various helper functions -
949please refer to the Haddocks for details.
950please refer to the Haddocks for details.
@@ -965,7 +966,7 @@ import Network.Minio
965
966
966import qualified Data.ByteString as B
967import qualified Data.ByteString as B
967import qualified Data.ByteString.Char8 as Char8
968import qualified Data.ByteString.Char8 as Char8
968−import qualified Data.Map.Strict as Map
969+import qualified Data.HashMap.Strict as H
969import qualified Data.Text.Encoding as Enc
970import qualified Data.Text.Encoding as Enc
970import qualified Data.Time as Time
971import qualified Data.Time as Time
971
972
@@ -1005,7 +1006,7 @@ main = do
1005 let
1006 let
1006 formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
1007 formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
1007 "'", v, "'"]
1008 "'", v, "'"]
1008− formOptions = B.intercalate " " $ map formFn $ Map.toList formData
1009+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
1009
1010
1010
1011
1011 return $ B.intercalate " " $
1012 return $ B.intercalate " " $
examples/AssumeRole.hsnew file+47
@@ -0,0 +1,47 @@
1+--
2+-- MinIO Haskell SDK, (C) 2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE OverloadedStrings #-}
17+
18+import Control.Monad.IO.Class (liftIO)
19+import Network.Minio
20+import Prelude
21+
22+main :: IO ()
23+main = do
24+ -- Use play credentials for example.
25+ let assumeRole =
26+ STSAssumeRole
27+ ( CredentialValue
28+ "Q3AM3UQ867SPQQA43P2F"
29+ "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
30+ Nothing
31+ )
32+ $ defaultSTSAssumeRoleOptions
33+ { saroLocation = Just "us-east-1",
34+ saroEndpoint = Just "https://play.min.io:9000"
35+ }
36+
37+ -- Retrieve temporary credentials and print them.
38+ cv <- requestSTSCredential assumeRole
39+ print $ "Temporary credentials" ++ show (credentialValueText $ fst cv)
40+ print $ "Expiry" ++ show (snd cv)
41+
42+ -- Configure 'ConnectInfo' to request temporary credentials on demand.
43+ ci <- setSTSCredential assumeRole "https://play.min.io"
44+ res <- runMinio ci $ do
45+ buckets <- listBuckets
46+ liftIO $ print $ "Top 5 buckets: " ++ show (take 5 buckets)
47+ print res
examples/BucketExists.hs+6−9
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,20 +16,17 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.IO.Class (liftIO)
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35 let bucket = "missingbucket"
32 let bucket = "missingbucket"
@@ -39,5 +36,5 @@ main = do
39 liftIO $ putStrLn $ "Does " ++ show bucket ++ " exist? - " ++ show foundBucket
36 liftIO $ putStrLn $ "Does " ++ show bucket ++ " exist? - " ++ show foundBucket
40
37
41 case res1 of
38 case res1 of
42− Left e -> putStrLn $ "bucketExists failed." ++ show e
39+ Left e -> putStrLn $ "bucketExists failed." ++ show e
43 Right () -> return ()
40 Right () -> return ()
examples/CopyObject.hs+16−17
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,41 +16,40 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.Catch (catchIf)
21+import Network.Minio
24−import Prelude
22+import UnliftIO.Exception (catch, throwIO)
25
23
26-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
29--
27--
30-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
31−--
32−
33−ignoreMinioErr :: ServiceErr -> Minio ()
34−ignoreMinioErr = return . const ()
35−
36main :: IO ()
29main :: IO ()
37main = do
30main = do
38− let
39− bucket = "test"
31+ let bucket = "test"
40 object = "obj"
32 object = "obj"
41 objectCopy = "obj-copy"
33 objectCopy = "obj-copy"
42 localFile = "/etc/lsb-release"
34 localFile = "/etc/lsb-release"
43
35
44 res1 <- runMinio minioPlayCI $ do
36 res1 <- runMinio minioPlayCI $ do
45 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
37 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
38+ catch
46− catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
39+ (makeBucket bucket Nothing)
40+ ( \e -> case e of
41+ BucketAlreadyOwnedByYou -> return ()
42+ _ -> throwIO e
43+ )
47
44
48 -- 2. Upload a file to bucket/object.
45 -- 2. Upload a file to bucket/object.
49− fPutObject bucket object localFile
46+ fPutObject bucket object localFile defaultPutObjectOptions
50
47
51 -- 3. Copy bucket/object to bucket/objectCopy.
48 -- 3. Copy bucket/object to bucket/objectCopy.
49+ copyObject
52− copyObject def {dstBucket = bucket, dstObject = objectCopy} def { srcBucket = bucket , srcObject = object }
50+ defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
51+ defaultSourceInfo {srcBucket = bucket, srcObject = object}
53
52
54 case res1 of
53 case res1 of
55− Left e -> putStrLn $ "copyObject failed." ++ show e
54+ Left e -> putStrLn $ "copyObject failed." ++ show e
56 Right () -> putStrLn "copyObject succeeded."
55 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+28−28
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs --package optparse-applicative --package filepath
2+-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,39 +16,39 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−
21−{-# LANGUAGE OverloadedStrings #-}
19+{-# LANGUAGE OverloadedStrings #-}
22{-# LANGUAGE ScopedTypeVariables #-}
20{-# LANGUAGE ScopedTypeVariables #-}
23−import Network.Minio
24
21
25−import Data.Monoid ((<>))
26−import Data.Text (pack)
22+import Data.Text (pack)
23+import Network.Minio
27−import Options.Applicative
24+import Options.Applicative
28−import System.FilePath.Posix
25+import System.FilePath.Posix
29−import UnliftIO (throwIO, try)
26+import UnliftIO (throwIO, try)
30−
31−import Prelude
27+import Prelude
32
28
33-- | The following example uses minio's play server at
29-- | The following example uses minio's play server at
34−-- https://play.min.io:9000. The endpoint and associated
30+-- https://play.min.io. The endpoint and associated
35-- credentials are provided via the libary constant,
31-- credentials are provided via the libary constant,
36--
32--
37-- > minioPlayCI :: ConnectInfo
33-- > minioPlayCI :: ConnectInfo
38−--
39
34
40-- optparse-applicative package based command-line parsing.
35-- optparse-applicative package based command-line parsing.
41fileNameArgs :: Parser FilePath
36fileNameArgs :: Parser FilePath
42−fileNameArgs = strArgument
37+fileNameArgs =
38+ strArgument
43− (metavar "FILENAME"
39+ ( metavar "FILENAME"
44− <> help "Name of file to upload to AWS S3 or a MinIO server")
40+ <> help "Name of file to upload to AWS S3 or a MinIO server"
41+ )
45
42
43+cmdParser :: ParserInfo FilePath
44+cmdParser =
46−cmdParser = info
45+ info
47− (helper <*> fileNameArgs)
46+ (helper <*> fileNameArgs)
48− (fullDesc
47+ ( fullDesc
49− <> progDesc "FileUploader"
48+ <> progDesc "FileUploader"
50− <> header
49+ <> header
51− "FileUploader - a simple file-uploader program using minio-hs")
50+ "FileUploader - a simple file-uploader program using minio-hs"
51+ )
52
52
53main :: IO ()
53main :: IO ()
54main = do
54main = do
@@ -62,13 +62,13 @@ main = do
62 -- Make a bucket; catch bucket already exists exception if thrown.
62 -- Make a bucket; catch bucket already exists exception if thrown.
63 bErr <- try $ makeBucket bucket Nothing
63 bErr <- try $ makeBucket bucket Nothing
64 case bErr of
64 case bErr of
65− Left (MErrService BucketAlreadyOwnedByYou) -> return ()
65+ Left BucketAlreadyOwnedByYou -> return ()
66− Left e -> throwIO e
66+ Left e -> throwIO e
67− Right _ -> return ()
67+ Right _ -> return ()
68
68
69 -- Upload filepath to bucket; object is derived from filepath.
69 -- Upload filepath to bucket; object is derived from filepath.
70− fPutObject bucket object filepath def
70+ fPutObject bucket object filepath defaultPutObjectOptions
71
71
72 case res of
72 case res of
73− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
73+ Left e -> putStrLn $ "file upload failed due to " ++ show e
74 Right () -> putStrLn "file upload succeeded."
74 Right () -> putStrLn "file upload succeeded."
examples/GetConfig.hs+7−7
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,14 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
26+ res <-
28− res <- runMinio def $
27+ runMinio
28+ minioPlayCI
29 getConfig
29 getConfig
30 print res
30 print res
examples/GetObject.hs+9−14
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,31 +16,26 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import qualified Data.Conduit as C
21+import qualified Data.Conduit as C
24import qualified Data.Conduit.Binary as CB
22import qualified Data.Conduit.Binary as CB
25−
23+import Network.Minio
26−import Prelude
24+import Prelude
27
25
28-- | The following example uses minio's play server at
26-- | The following example uses minio's play server at
29−-- https://play.min.io:9000. The endpoint and associated
27+-- https://play.min.io. The endpoint and associated
30-- credentials are provided via the libary constant,
28-- credentials are provided via the libary constant,
31--
29--
32-- > minioPlayCI :: ConnectInfo
30-- > minioPlayCI :: ConnectInfo
33−--
34−
35main :: IO ()
31main :: IO ()
36main = do
32main = do
37− let
38− bucket = "my-bucket"
33+ let bucket = "my-bucket"
39 object = "my-object"
34 object = "my-object"
40 res <- runMinio minioPlayCI $ do
35 res <- runMinio minioPlayCI $ do
41− src <- getObject bucket object def
36+ src <- getObject bucket object defaultGetObjectOptions
42− C.connect src $ CB.sinkFileCautious "/tmp/my-object"
37+ C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
43
38
44 case res of
39 case res of
45− Left e -> putStrLn $ "getObject failed." ++ (show e)
40+ Left e -> putStrLn $ "getObject failed." ++ show e
46 Right _ -> putStrLn "getObject succeeded."
41 Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs+10−13
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,28 +16,25 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.S3API
23
20
21+import Network.Minio
22+import Network.Minio.S3API
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
37 object = "passwd"
33 object = "passwd"
34+ res <-
38− res <- runMinio minioPlayCI $
35+ runMinio minioPlayCI $
39− headObject bucket object
36+ headObject bucket object []
40
37
41 case res of
38 case res of
42− Left e -> putStrLn $ "headObject failed." ++ show e
39+ Left e -> putStrLn $ "headObject failed." ++ show e
43 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
40 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
examples/Heal.hs+13−10
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,18 +17,21 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
28− res <- runMinio def $
26+ res <- runMinio minioPlayCI $
29 do
27 do
28+ hsr <-
29+ startHeal
30+ Nothing
31+ Nothing
32+ HealOpts
30− hsr <- startHeal Nothing Nothing HealOpts { hoRecursive = True
33+ { hoRecursive = True,
31− , hoDryRun = False
34+ hoDryRun = False
32− }
35+ }
33 getHealStatus Nothing Nothing (hsrClientToken hsr)
36 getHealStatus Nothing Nothing (hsrClientToken hsr)
34 print res
37 print res
examples/ListBuckets.hs+5−7
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,19 +16,17 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.IO.Class (liftIO)
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32
30
33-- This example list buckets that belongs to the user and returns
31-- This example list buckets that belongs to the user and returns
34-- region of the first bucket returned.
32-- region of the first bucket returned.
examples/ListIncompleteUploads.hs+19−21
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,38 +16,36 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Conduit
21+import Conduit
22+import Network.Minio
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
37
33
38 -- Performs a recursive listing of incomplete uploads under bucket "test"
34 -- Performs a recursive listing of incomplete uploads under bucket "test"
39 -- on a local minio server.
35 -- on a local minio server.
36+ res <-
40− res <- runMinio minioPlayCI $
37+ runMinio minioPlayCI $
38+ runConduit $
41− runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
39+ listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
42 print res
40 print res
43
41
44− {-
42+{-
45− Following is the output of the above program on a local MinIO server.
43+ Following is the output of the above program on a local MinIO server.
46
44
47− Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
45+ Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
48− , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
46+ , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
49− , uiInitTime = 2017-03-01 10:16:25.698 UTC
47+ , uiInitTime = 2017-03-01 10:16:25.698 UTC
50− , uiSize = 17731794
48+ , uiSize = 17731794
51− }
49+ }
52− ]
50+ ]
53− -}
51+-}
examples/ListObjects.hs+15−17
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,33 +16,31 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−
23−import Conduit
24−import Prelude
25
20
21+import Conduit
22+import Network.Minio
23+import Prelude
26
24
27-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
28−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
29-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
30--
28--
31-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
32−--
33−
34main :: IO ()
30main :: IO ()
35main = do
31main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
38
33
39 -- Performs a recursive listing of all objects under bucket "test"
34 -- Performs a recursive listing of all objects under bucket "test"
40 -- on play.min.io.
35 -- on play.min.io.
36+ res <-
41− res <- runMinio minioPlayCI $
37+ runMinio minioPlayCI $
38+ runConduit $
42− runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
39+ listObjects bucket Nothing True .| mapM_C (liftIO . print)
43 print res
40 print res
44− {-
45− Following is the output of the above program on a local MinIO server.
46
41
42+{-
43+ Following is the output of the above program on a local MinIO server.
44+
47− Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]
45+ Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]
48− -}
46+-}
examples/MakeBucket.hs+8−11
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,24 +16,21 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−
21{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
23
20
21+import Network.Minio
24−import Prelude
22+import Prelude
25
23
26-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
29--
27--
30-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
29main :: IO ()
34main = do
30main = do
35 let bucket = "my-bucket"
31 let bucket = "my-bucket"
32+ res <-
36− res <- runMinio minioPlayCI $
33+ runMinio minioPlayCI $
37− -- N B the region provided for makeBucket is optional.
34+ -- N B the region provided for makeBucket is optional.
38− makeBucket bucket (Just "us-east-1")
35+ makeBucket bucket (Just "us-east-1")
39 print res
36 print res
examples/PresignedGetObject.hs+37−33
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,42 +16,40 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.IO.Class (liftIO)
21+import Control.Monad.IO.Class (liftIO)
24−import qualified Data.ByteString.Char8 as B
22+import qualified Data.ByteString.Char8 as B
25−import Data.CaseInsensitive (original)
23+import Data.CaseInsensitive (original)
26import qualified Data.Conduit.Combinators as CC
24import qualified Data.Conduit.Combinators as CC
27−import qualified Data.Text.Encoding as E
25+import qualified Data.Text.Encoding as E
26+import Network.Minio
28
27
29-- | The following example uses minio's play server at
28-- | The following example uses minio's play server at
30−-- https://play.min.io:9000. The endpoint and associated
29+-- https://play.min.io. The endpoint and associated
31-- credentials are provided via the libary constant,
30-- credentials are provided via the libary constant,
32--
31--
33-- > minioPlayCI :: ConnectInfo
32-- > minioPlayCI :: ConnectInfo
34−--
35−
36main :: IO ()
33main :: IO ()
37main = do
34main = do
38− let
39− bucket = "my-bucket"
35+ let bucket = "my-bucket"
40− object = "my-object"
36+ object = "my-object"
41− kb15 = 15*1024
37+ kb15 = 15 * 1024
42−
43− -- Set query parameter to modify content disposition response
38+ -- Set query parameter to modify content disposition response
44− -- header
39+ -- header
40+ queryParam =
45− queryParam = [("response-content-disposition",
41+ [ ( "response-content-disposition",
46− Just "attachment; filename=\"your-filename.txt\"")]
42+ Just "attachment; filename=\"your-filename.txt\""
43+ )
44+ ]
47
45
48 res <- runMinio minioPlayCI $ do
46 res <- runMinio minioPlayCI $ do
49 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
47 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
50− putObject bucket object (CC.repeat "a") (Just kb15) def
48+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
51− liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
49+ liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
52
50
53 -- Extract Etag of uploaded object
51 -- Extract Etag of uploaded object
54− oi <- statObject bucket object
52+ oi <- statObject bucket object defaultGetObjectOptions
55 let etag = oiETag oi
53 let etag = oiETag oi
56
54
57 -- Set header to add an if-match constraint - this makes sure
55 -- Set header to add an if-match constraint - this makes sure
@@ -61,23 +59,29 @@ main = do
61 -- Generate a URL with 7 days expiry time - note that the headers
59 -- Generate a URL with 7 days expiry time - note that the headers
62 -- used above must be added to the request with the signed URL
60 -- used above must be added to the request with the signed URL
63 -- generated.
61 -- generated.
62+ url <-
64− url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)
63+ presignedGetObjectUrl
64+ "my-bucket"
65+ "my-object"
66+ (7 * 24 * 3600)
65− queryParam headers
67+ queryParam
68+ headers
66
69
67 return (headers, etag, url)
70 return (headers, etag, url)
68
71
69 case res of
72 case res of
70 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
73 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
71− Right (headers, etag, url) -> do
74+ Right (headers, _, url) -> do
72−
73 -- We generate a curl command to demonstrate usage of the signed
75 -- We generate a curl command to demonstrate usage of the signed
74 -- URL.
76 -- URL.
75− let
76− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
77+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
78+ curlCmd =
77− curlCmd = B.intercalate " " $
79+ B.intercalate " " $
80+ ["curl --fail"]
78− ["curl --fail"] ++ map hdrOpt headers ++
81+ ++ map hdrOpt headers
79− ["-o /tmp/myfile", B.concat ["'", url, "'"]]
82+ ++ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
80
83
84+ putStrLn $
81− putStrLn $ "The following curl command would use the presigned " ++
85+ "The following curl command would use the presigned "
82− "URL to fetch the object and write it to \"/tmp/myfile\":"
86+ ++ "URL to fetch the object and write it to \"/tmp/myfile\":"
83 B.putStrLn curlCmd
87 B.putStrLn curlCmd
examples/PresignedPostPolicy.hs+44−41
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,69 +16,72 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import qualified Data.ByteString as B
21+import qualified Data.ByteString as B
24import qualified Data.ByteString.Char8 as Char8
22import qualified Data.ByteString.Char8 as Char8
25−import qualified Data.Map.Strict as Map
23+import qualified Data.HashMap.Strict as H
26−import qualified Data.Text.Encoding as Enc
24+import qualified Data.Text.Encoding as Enc
27−import qualified Data.Time as Time
25+import qualified Data.Time as Time
26+import Network.Minio
28
27
29-- | The following example uses minio's play server at
28-- | The following example uses minio's play server at
30−-- https://play.min.io:9000. The endpoint and associated
29+-- https://play.min.io. The endpoint and associated
31-- credentials are provided via the libary constant,
30-- credentials are provided via the libary constant,
32--
31--
33-- > minioPlayCI :: ConnectInfo
32-- > minioPlayCI :: ConnectInfo
34−--
35−
36main :: IO ()
33main :: IO ()
37main = do
34main = do
38 now <- Time.getCurrentTime
35 now <- Time.getCurrentTime
39− let
40− bucket = "my-bucket"
36+ let bucket = "my-bucket"
41− object = "my-object"
37+ object = "photos/my-object"
42−
43− -- set an expiration time of 10 days
38+ -- set an expiration time of 10 days
44− expireTime = Time.addUTCTime (3600 * 24 * 10) now
39+ expireTime = Time.addUTCTime (3600 * 24 * 10) now
45−
46− -- create a policy with expiration time and conditions - since the
40+ -- create a policy with expiration time and conditions - since the
47− -- conditions are validated, newPostPolicy returns an Either value
41+ -- conditions are validated, newPostPolicy returns an Either value
42+ policyE =
48− policyE = newPostPolicy expireTime
43+ newPostPolicy
44+ expireTime
49− [ -- set the object name condition
45+ [ -- set the object name condition
50− ppCondKey "photos/my-object"
46+ ppCondKey object,
51− -- set the bucket name condition
47+ -- set the bucket name condition
52− , ppCondBucket "my-bucket"
48+ ppCondBucket bucket,
53− -- set the size range of object as 1B to 10MiB
49+ -- set the size range of object as 1B to 10MiB
54− , ppCondContentLengthRange 1 (10*1024*1024)
50+ ppCondContentLengthRange 1 (10 * 1024 * 1024),
55− -- set content type as jpg image
51+ -- set content type as jpg image
56− , ppCondContentType "image/jpeg"
52+ ppCondContentType "image/jpeg",
57− -- on success set the server response code to 200
53+ -- on success set the server response code to 200
58− , ppCondSuccessActionStatus 200
54+ ppCondSuccessActionStatus 200
59− ]
55+ ]
60
56
61 case policyE of
57 case policyE of
62− Left err -> putStrLn $ show err
58+ Left err -> print err
63 Right policy -> do
59 Right policy -> do
64 res <- runMinio minioPlayCI $ do
60 res <- runMinio minioPlayCI $ do
65 (url, formData) <- presignedPostPolicy policy
61 (url, formData) <- presignedPostPolicy policy
66
62
67 -- a curl command is output to demonstrate using the generated
63 -- a curl command is output to demonstrate using the generated
68 -- URL and form-data
64 -- URL and form-data
69− let
65+ let formFn (k, v) =
66+ B.concat
67+ [ "-F ",
70− formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
68+ Enc.encodeUtf8 k,
69+ "=",
70+ "'",
71− "'", v, "'"]
71+ v,
72+ "'"
73+ ]
72− formOptions = B.intercalate " " $ map formFn $ Map.toList formData
74+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
73−
74
75
76+ return $
75− return $ B.intercalate " " $
77+ B.intercalate
78+ " "
76− ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
79+ ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
77
80
78 case res of
81 case res of
79− Left e -> putStrLn $ "post-policy error: " ++ (show e)
82+ Left e -> putStrLn $ "post-policy error: " ++ show e
80 Right cmd -> do
83 Right cmd -> do
81− putStrLn $ "Put a photo at /tmp/photo.jpg and run command:\n"
84+ putStrLn "Put a photo at /tmp/photo.jpg and run command:\n"
82
85
83 -- print the generated curl command
86 -- print the generated curl command
84 Char8.putStrLn cmd
87 Char8.putStrLn cmd
examples/PresignedPutObject.hs+22−23
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,44 +16,43 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23import qualified Data.ByteString.Char8 as B
21import qualified Data.ByteString.Char8 as B
24−import Data.CaseInsensitive (original)
22+import Data.CaseInsensitive (original)
23+import Network.Minio
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35− let
36− -- Use headers to set user-metadata - note that this header will
32+ let -- Use headers to set user-metadata - note that this header will
37− -- need to be set when the URL is used to make an upload.
33+ -- need to be set when the URL is used to make an upload.
34+ headers =
38− headers = [("x-amz-meta-url-creator",
35+ [ ( "x-amz-meta-url-creator",
39− "minio-hs-presigned-put-example")]
36+ "minio-hs-presigned-put-example"
37+ )
38+ ]
40 res <- runMinio minioPlayCI $ do
39 res <- runMinio minioPlayCI $ do
41−
42 -- generate a URL with 7 days expiry time
40 -- generate a URL with 7 days expiry time
43− presignedPutObjectUrl "my-bucket" "my-object" (7*24*3600) headers
41+ presignedPutObjectUrl "my-bucket" "my-object" (7 * 24 * 3600) headers
44
42
45 case res of
43 case res of
46 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
44 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
47 Right url -> do
45 Right url -> do
48−
49 -- We generate a curl command to demonstrate usage of the signed
46 -- We generate a curl command to demonstrate usage of the signed
50 -- URL.
47 -- URL.
51− let
52− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
48+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
49+ curlCmd =
53− curlCmd = B.intercalate " " $
50+ B.intercalate " " $
51+ ["curl "]
54− ["curl "] ++ map hdrOpt headers ++
52+ ++ map hdrOpt headers
55− ["-T /tmp/myfile", B.concat ["'", url, "'"]]
53+ ++ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
56−
54+
55+ putStrLn $
57− putStrLn $ "The following curl command would use the presigned " ++
56+ "The following curl command would use the presigned "
58− "URL to upload the file at \"/tmp/myfile\":"
57+ ++ "URL to upload the file at \"/tmp/myfile\":"
59 B.putStrLn curlCmd
58 B.putStrLn curlCmd
examples/PutObject.hs+13−16
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,39 +16,36 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23import qualified Data.Conduit.Combinators as CC
21import qualified Data.Conduit.Combinators as CC
24−
22+import Network.Minio
25−import Prelude
23+import Prelude
26
24
27-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
28−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
29-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
30--
28--
31-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
32−--
33−
34main :: IO ()
30main :: IO ()
35main = do
31main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
38 object = "obj"
33 object = "obj"
39 localFile = "/etc/lsb-release"
34 localFile = "/etc/lsb-release"
40 kb15 = 15 * 1024
35 kb15 = 15 * 1024
41
36
42 -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
37 -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
38+ res1 <-
43− res1 <- runMinio minioPlayCI $
39+ runMinio minioPlayCI $
44− putObject bucket object (CC.repeat "a") (Just kb15) def
40+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
45 case res1 of
41 case res1 of
46− Left e -> putStrLn $ "putObject failed." ++ show e
42+ Left e -> putStrLn $ "putObject failed." ++ show e
47 Right () -> putStrLn "putObject succeeded."
43 Right () -> putStrLn "putObject succeeded."
48
44
49 -- Eg 2. Upload a file using fPutObject with default options.
45 -- Eg 2. Upload a file using fPutObject with default options.
46+ res2 <-
50− res2 <- runMinio minioPlayCI $
47+ runMinio minioPlayCI $
51− fPutObject bucket object localFile def
48+ fPutObject bucket object localFile defaultPutObjectOptions
52 case res2 of
49 case res2 of
53− Left e -> putStrLn $ "fPutObject failed." ++ show e
50+ Left e -> putStrLn $ "fPutObject failed." ++ show e
54 Right () -> putStrLn "fPutObject succeeded."
51 Right () -> putStrLn "fPutObject succeeded."
examples/README.mdnew file+17
@@ -0,0 +1,17 @@
1+# Examples
2+
3+The examples in this directory illustrate usage of various APIs provided by this library. Each file is self-contained and can be run like a script directly.
4+
5+To build the examples, the build flag `examples` needs to be turned on:
6+
7+```sh
8+stack build --flag minio-hs:examples
9+```
10+
11+Now to run and example script [BucketExists.hs](https://github.com/minio/minio-hs/blob/master/examples/BucketExists.hs):
12+
13+```sh
14+stack exec BucketExists
15+```
16+
17+The CI system is configured to build these examples with every change, so they should be current.
examples/RemoveBucket.hs+5−10
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,23 +16,18 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−
21{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
23
20
21+import Network.Minio
24−import Prelude
22+import Prelude
25
23
26-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
29--
27--
30-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
29main :: IO ()
34main = do
30main = do
35− let
36− bucket = "my-bucket"
31+ let bucket = "my-bucket"
37 res <- runMinio minioPlayCI $ removeBucket bucket
32 res <- runMinio minioPlayCI $ removeBucket bucket
38 print res
33 print res
examples/RemoveIncompleteUpload.hs+9−12
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,27 +16,24 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
21+import Network.Minio
23−import Prelude
22+import Prelude
24
23
25-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
26−-- https://play.min.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
27-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
28--
27--
29-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
30−--
31−
32main :: IO ()
29main :: IO ()
33main = do
30main = do
34− let
35− bucket = "mybucket"
31+ let bucket = "mybucket"
36− object = "myobject"
32+ object = "myobject"
37
33
34+ res <-
38− res <- runMinio minioPlayCI $
35+ runMinio minioPlayCI $
39− removeIncompleteUpload bucket object
36+ removeIncompleteUpload bucket object
40
37
41 case res of
38 case res of
42 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
39 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/RemoveObject.hs+8−9
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,20 +16,19 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21
20
22−import Network.Minio
21+import Network.Minio
23−import Prelude
22+import Prelude
24
23
25main :: IO ()
24main :: IO ()
26main = do
25main = do
27− let
28− bucket = "mybucket"
26+ let bucket = "mybucket"
29− object = "myobject"
27+ object = "myobject"
30
28
29+ res <-
31− res <- runMinio minioPlayCI $
30+ runMinio minioPlayCI $
32− removeObject bucket object
31+ removeObject bucket object
33
32
34 case res of
33 case res of
35 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
34 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/SelectObject.hs+25−28
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-13.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2019 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2019 MinIO, Inc.
@@ -16,35 +16,32 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−
23−import qualified Conduit as C
24−import Control.Monad (when)
25−import qualified Data.ByteString.Lazy as LB
26
20
21+import qualified Conduit as C
22+import Control.Monad (unless)
23+import Network.Minio
27−import Prelude
24+import Prelude
28
25
29main :: IO ()
26main :: IO ()
30main = do
27main = do
31− let bucket = "selectbucket"
28+ let bucket = "selectbucket"
32− object = "1.csv"
29+ object = "1.csv"
30+ content =
33− content = "Name,Place,Temperature\n"
31+ "Name,Place,Temperature\n"
34− <> "James,San Jose,76\n"
32+ <> "James,San Jose,76\n"
35− <> "Alicia,San Leandro,88\n"
33+ <> "Alicia,San Leandro,88\n"
36− <> "Mark,San Carlos,90\n"
34+ <> "Mark,San Carlos,90\n"
37−
35+
38− res <- runMinio minioPlayCI $ do
36+ res <- runMinio minioPlayCI $ do
39−
40− exists <- bucketExists bucket
37+ exists <- bucketExists bucket
41− when (not exists) $
38+ unless exists $
42− makeBucket bucket Nothing
39+ makeBucket bucket Nothing
43−
40+
44− C.liftIO $ putStrLn "Uploading csv object"
41+ C.liftIO $ putStrLn "Uploading csv object"
45− putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
42+ putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
46−
43+
47− let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
44+ let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
48− res <- selectObjectContent bucket object sr
45+ res <- selectObjectContent bucket object sr
49− C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
46+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
50− print res
47+ print res
examples/ServerInfo.hs+8−8
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,14 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
28− res <- runMinio def $
26+ res <-
27+ runMinio
28+ minioPlayCI
29− getServerInfo
29+ getServerInfo
30 print res
30 print res
examples/ServiceSendRestart.hs+7−8
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,13 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
26+ res <-
28− res <- runMinio def $
27+ runMinio minioPlayCI $
29− serviceSendAction ServiceActionRestart
28+ serviceSendAction ServiceActionRestart
30 print res
29 print res
examples/ServiceSendStop.hs+7−8
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,13 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
26+ res <-
28− res <- runMinio def $
27+ runMinio minioPlayCI $
29− serviceSendAction ServiceActionStop
28+ serviceSendAction ServiceActionStop
30 print res
29 print res
examples/ServiceStatus.hs+8−8
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,14 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
28− res <- runMinio def $
26+ res <-
27+ runMinio
28+ minioPlayCI
29− serviceStatus
29+ serviceStatus
30 print res
30 print res
examples/SetConfig.hs+5−6
@@ -1,5 +1,5 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,16 +16,15 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
23
20
21+import Network.Minio
22+import Network.Minio.AdminAPI
24−import Prelude
23+import Prelude
25
24
26main :: IO ()
25main :: IO ()
27main = do
26main = do
28− res <- runMinio def $
27+ res <- runMinio minioPlayCI $
29 do
28 do
30 let config = "{\"version\":\"25\",\"credential\":{\"accessKey\":\"minio\",\"secretKey\":\"minio123\"},\"region\":\"\",\"browser\":\"on\",\"worm\":\"off\",\"domain\":\"\",\"storageclass\":{\"standard\":\"\",\"rrs\":\"\"},\"cache\":{\"drives\":[],\"expiry\":90,\"exclude\":[]},\"notify\":{\"amqp\":{\"2\":{\"enable\":false,\"url\":\"amqp://guest:guest@localhost:5672/\",\"exchange\":\"minio\",\"routingKey\":\"minio\",\"exchangeType\":\"direct\",\"deliveryMode\":0,\"mandatory\":false,\"immediate\":false,\"durable\":false,\"internal\":false,\"noWait\":false,\"autoDeleted\":false}},\"elasticsearch\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"url\":\"http://localhost:9200\",\"index\":\"minio_events\"}},\"kafka\":{\"1\":{\"enable\":false,\"brokers\":null,\"topic\":\"\"}},\"mqtt\":{\"1\":{\"enable\":false,\"broker\":\"\",\"topic\":\"\",\"qos\":0,\"clientId\":\"\",\"username\":\"\",\"password\":\"\",\"reconnectInterval\":0,\"keepAliveInterval\":0}},\"mysql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"dsnString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"nats\":{\"1\":{\"enable\":false,\"address\":\"\",\"subject\":\"\",\"username\":\"\",\"password\":\"\",\"token\":\"\",\"secure\":false,\"pingInterval\":0,\"streaming\":{\"enable\":false,\"clusterID\":\"\",\"clientID\":\"\",\"async\":false,\"maxPubAcksInflight\":0}}},\"postgresql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"connectionString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"redis\":{\"test1\":{\"enable\":true,\"format\":\"namespace\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_ns\"},\"test2\":{\"enable\":true,\"format\":\"access\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_log\"}},\"webhook\":{\"1\":{\"enable\":true,\"endpoint\":\"http://localhost:3000\"},\"2\":{\"enable\":true,\"endpoint\":\"http://localhost:3001\"}}}}"
29 let config = "{\"version\":\"25\",\"credential\":{\"accessKey\":\"minio\",\"secretKey\":\"minio123\"},\"region\":\"\",\"browser\":\"on\",\"worm\":\"off\",\"domain\":\"\",\"storageclass\":{\"standard\":\"\",\"rrs\":\"\"},\"cache\":{\"drives\":[],\"expiry\":90,\"exclude\":[]},\"notify\":{\"amqp\":{\"2\":{\"enable\":false,\"url\":\"amqp://guest:guest@localhost:5672/\",\"exchange\":\"minio\",\"routingKey\":\"minio\",\"exchangeType\":\"direct\",\"deliveryMode\":0,\"mandatory\":false,\"immediate\":false,\"durable\":false,\"internal\":false,\"noWait\":false,\"autoDeleted\":false}},\"elasticsearch\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"url\":\"http://localhost:9200\",\"index\":\"minio_events\"}},\"kafka\":{\"1\":{\"enable\":false,\"brokers\":null,\"topic\":\"\"}},\"mqtt\":{\"1\":{\"enable\":false,\"broker\":\"\",\"topic\":\"\",\"qos\":0,\"clientId\":\"\",\"username\":\"\",\"password\":\"\",\"reconnectInterval\":0,\"keepAliveInterval\":0}},\"mysql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"dsnString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"nats\":{\"1\":{\"enable\":false,\"address\":\"\",\"subject\":\"\",\"username\":\"\",\"password\":\"\",\"token\":\"\",\"secure\":false,\"pingInterval\":0,\"streaming\":{\"enable\":false,\"clusterID\":\"\",\"clientID\":\"\",\"async\":false,\"maxPubAcksInflight\":0}}},\"postgresql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"connectionString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"redis\":{\"test1\":{\"enable\":true,\"format\":\"namespace\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_ns\"},\"test2\":{\"enable\":true,\"format\":\"access\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_log\"}},\"webhook\":{\"1\":{\"enable\":true,\"endpoint\":\"http://localhost:3000\"},\"2\":{\"enable\":true,\"endpoint\":\"http://localhost:3001\"}}}}"
31 setConfig config
30 setConfig config
minio-hs.cabal+249−170
@@ -1,5 +1,6 @@
1+cabal-version: 2.4
1name: minio-hs
2name: minio-hs
2−version: 1.4.0
3+version: 1.7.0
3synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
4synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
4 storage.
5 storage.
5description: The MinIO Haskell client library provides simple APIs to
6description: The MinIO Haskell client library provides simple APIs to
@@ -13,22 +14,70 @@ maintainer: dev@min.io
13category: Network, AWS, Object Storage
14category: Network, AWS, Object Storage
14build-type: Simple
15build-type: Simple
15stability: Experimental
16stability: Experimental
16−extra-source-files:
17+extra-doc-files:
17 CHANGELOG.md
18 CHANGELOG.md
18 CONTRIBUTING.md
19 CONTRIBUTING.md
19 docs/API.md
20 docs/API.md
20− examples/*.hs
21 README.md
21 README.md
22+extra-source-files:
23+ examples/*.hs
22 stack.yaml
24 stack.yaml
25+tested-with: GHC == 8.10.7
26+ , GHC == 9.0.2
27+ , GHC == 9.2.8
28+ , GHC == 9.4.8
29+ , GHC == 9.6.5
30+ , GHC == 9.8.2
23
31
24−cabal-version: >=1.10
32+source-repository head
33+ type: git
34+ location: https://github.com/minio/minio-hs.git
25
35
26−library
36+Flag dev
27− hs-source-dirs: src
37+ Description: Build package in development mode
38+ Default: False
39+ Manual: True
40+
41+common base-settings
28 ghc-options: -Wall
42 ghc-options: -Wall
29− exposed-modules: Network.Minio
43+ -Wcompat
30− , Network.Minio.AdminAPI
44+ -Widentities
31− , Network.Minio.S3API
45+ -Wincomplete-uni-patterns
46+ -Wincomplete-record-updates
47+ -haddock
48+ if impl(ghc >= 8.0)
49+ ghc-options: -Wredundant-constraints
50+ if impl(ghc >= 8.2)
51+ ghc-options: -fhide-source-paths
52+ if impl(ghc >= 8.4)
53+ ghc-options: -Wpartial-fields
54+ -- -Wmissing-export-lists
55+ if impl(ghc >= 8.8)
56+ ghc-options: -Wmissing-deriving-strategies
57+ -Werror=missing-deriving-strategies
58+ -- if impl(ghc >= 8.10)
59+ -- ghc-options: -Wunused-packages -- disabled due to bug related to mixin config
60+ if impl(ghc >= 9.0)
61+ ghc-options: -Winvalid-haddock
62+ if impl(ghc >= 9.2)
63+ ghc-options: -Wredundant-bang-patterns
64+ if flag(dev)
65+ ghc-options: -Werror
66+
67+ default-language: Haskell2010
68+
69+ default-extensions: BangPatterns
70+ , DerivingStrategies
71+ , FlexibleContexts
72+ , FlexibleInstances
73+ , LambdaCase
74+ , MultiParamTypeClasses
75+ , MultiWayIf
76+ , OverloadedStrings
77+ , RankNTypes
78+ , ScopedTypeVariables
79+ , TupleSections
80+
32 other-modules: Lib.Prelude
81 other-modules: Lib.Prelude
33 , Network.Minio.API
82 , Network.Minio.API
34 , Network.Minio.APICommon
83 , Network.Minio.APICommon
@@ -46,23 +95,31 @@ library
46 , Network.Minio.Utils
95 , Network.Minio.Utils
47 , Network.Minio.XmlGenerator
96 , Network.Minio.XmlGenerator
48 , Network.Minio.XmlParser
97 , Network.Minio.XmlParser
98+ , Network.Minio.XmlCommon
49 , Network.Minio.JsonParser
99 , Network.Minio.JsonParser
100+ , Network.Minio.Credentials.Types
101+ , Network.Minio.Credentials.AssumeRole
102+ , Network.Minio.Credentials
103+
104+ mixins: base hiding (Prelude)
105+ , relude (Relude as Prelude)
106+ , relude
107+
50 build-depends: base >= 4.7 && < 5
108 build-depends: base >= 4.7 && < 5
51− , protolude >= 0.2 && < 0.3
109+ , relude >= 0.7 && < 2
52− , aeson >= 1.2
110+ , aeson >= 1.2 && < 3
53 , base64-bytestring >= 1.0
111 , base64-bytestring >= 1.0
54 , binary >= 0.8.5.0
112 , binary >= 0.8.5.0
55 , bytestring >= 0.10
113 , bytestring >= 0.10
56 , case-insensitive >= 1.2
114 , case-insensitive >= 1.2
57 , conduit >= 1.3
115 , conduit >= 1.3
58 , conduit-extra >= 1.3
116 , conduit-extra >= 1.3
59− , connection
117+ , crypton-connection
60− , containers >= 0.5
61 , cryptonite >= 0.25
118 , cryptonite >= 0.25
62 , cryptonite-conduit >= 0.2
119 , cryptonite-conduit >= 0.2
120+ , data-default-class
63 , digest >= 0.0.1
121 , digest >= 0.0.1
64 , directory
122 , directory
65− , exceptions
66 , filepath >= 1.4
123 , filepath >= 1.4
67 , http-client >= 0.5
124 , http-client >= 0.5
68 , http-client-tls
125 , http-client-tls
@@ -70,205 +127,227 @@ library
70 , http-types >= 0.12
127 , http-types >= 0.12
71 , ini
128 , ini
72 , memory >= 0.14
129 , memory >= 0.14
73− , raw-strings-qq >= 1
130+ , network-uri
74 , resourcet >= 1.2
131 , resourcet >= 1.2
75 , retry
132 , retry
76 , text >= 1.2
133 , text >= 1.2
77− , time >= 1.8
134+ , time >= 1.9
135+ , time-units ^>= 1.0.0
78 , transformers >= 0.5
136 , transformers >= 0.5
79− , unliftio >= 0.2
137+ , unliftio >= 0.2 && < 0.3
80− , unliftio-core >= 0.1
138+ , unliftio-core >= 0.2 && < 0.3
81 , unordered-containers >= 0.2
139 , unordered-containers >= 0.2
82 , xml-conduit >= 1.8
140 , xml-conduit >= 1.8
83− default-language: Haskell2010
141+
84− default-extensions: BangPatterns
142+library
85− , FlexibleContexts
143+ import: base-settings
86− , FlexibleInstances
144+ hs-source-dirs: src
87− , MultiParamTypeClasses
145+ exposed-modules: Network.Minio
88− , MultiWayIf
146+ , Network.Minio.AdminAPI
89− , NoImplicitPrelude
147+ , Network.Minio.S3API
90− , OverloadedStrings
91− , RankNTypes
92− , ScopedTypeVariables
93− , TypeFamilies
94− , TupleSections
95
148
96Flag live-test
149Flag live-test
150+ Description: Build the test suite that runs against a live MinIO server
97− Default: True
151+ Default: False
98 Manual: True
152 Manual: True
99
153
100test-suite minio-hs-live-server-test
154test-suite minio-hs-live-server-test
155+ import: base-settings
101 type: exitcode-stdio-1.0
156 type: exitcode-stdio-1.0
102 hs-source-dirs: test, src
157 hs-source-dirs: test, src
103 main-is: LiveServer.hs
158 main-is: LiveServer.hs
104 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
159 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
105− default-language: Haskell2010
106− default-extensions: BangPatterns
107− , FlexibleContexts
108− , FlexibleInstances
109− , MultiParamTypeClasses
110− , MultiWayIf
111− , NoImplicitPrelude
112− , OverloadedStrings
113− , RankNTypes
114− , ScopedTypeVariables
115− , TupleSections
116− , TypeFamilies
117− other-modules: Lib.Prelude
118− , Network.Minio
160+ other-modules: Network.Minio
119− , Network.Minio.API
161+ , Network.Minio.S3API
120− , Network.Minio.API.Test
121− , Network.Minio.APICommon
122 , Network.Minio.AdminAPI
162 , Network.Minio.AdminAPI
123− , Network.Minio.CopyObject
163+ , Network.Minio.API.Test
124− , Network.Minio.Data
125− , Network.Minio.Data.ByteString
126− , Network.Minio.Data.Crypto
127− , Network.Minio.Data.Time
128− , Network.Minio.Errors
129− , Network.Minio.JsonParser
130 , Network.Minio.JsonParser.Test
164 , Network.Minio.JsonParser.Test
131− , Network.Minio.ListOps
132− , Network.Minio.PresignedOperations
133− , Network.Minio.PutObject
134− , Network.Minio.S3API
135− , Network.Minio.SelectAPI
136− , Network.Minio.Sign.V4
137 , Network.Minio.TestHelpers
165 , Network.Minio.TestHelpers
138− , Network.Minio.Utils
139 , Network.Minio.Utils.Test
166 , Network.Minio.Utils.Test
140− , Network.Minio.XmlGenerator
141 , Network.Minio.XmlGenerator.Test
167 , Network.Minio.XmlGenerator.Test
142− , Network.Minio.XmlParser
143 , Network.Minio.XmlParser.Test
168 , Network.Minio.XmlParser.Test
169+ , Network.Minio.Credentials
144− build-depends: base >= 4.7 && < 5
170+ build-depends: minio-hs
145− , minio-hs
146− , protolude >= 0.1.6
147− , QuickCheck
148− , aeson
149− , base64-bytestring
150− , binary
151− , bytestring
152− , case-insensitive
153− , conduit
154− , conduit-extra
155− , connection
156− , containers
157− , cryptonite
158− , cryptonite-conduit
159− , digest
160− , directory
161− , exceptions
162− , filepath
163− , http-client
164− , http-client-tls
165− , http-conduit
166− , http-types
167− , ini
168− , memory
169− , raw-strings-qq >= 1
171+ , raw-strings-qq
170− , resourcet
171− , retry
172 , tasty
172 , tasty
173 , tasty-hunit
173 , tasty-hunit
174 , tasty-quickcheck
174 , tasty-quickcheck
175 , tasty-smallcheck
175 , tasty-smallcheck
176− , temporary
176+ , QuickCheck
177− , text
178− , time
179− , transformers
180− , unliftio
181− , unliftio-core
182− , unordered-containers
183− , xml-conduit
184 if !flag(live-test)
177 if !flag(live-test)
185 buildable: False
178 buildable: False
186
179
187test-suite minio-hs-test
180test-suite minio-hs-test
181+ import: base-settings
188 type: exitcode-stdio-1.0
182 type: exitcode-stdio-1.0
189 hs-source-dirs: test, src
183 hs-source-dirs: test, src
190 main-is: Spec.hs
184 main-is: Spec.hs
191− build-depends: base >= 4.7 && < 5
185+ build-depends: minio-hs
192− , minio-hs
186+ , raw-strings-qq
193− , protolude >= 0.1.6
194 , QuickCheck
187 , QuickCheck
195− , aeson
196− , base64-bytestring
197− , binary
198− , bytestring
199− , case-insensitive
200− , conduit
201− , conduit-extra
202− , connection
203− , containers
204− , cryptonite
205− , cryptonite-conduit
206− , digest
207− , directory
208− , exceptions
209− , filepath
210− , http-client
211− , http-client-tls
212− , http-conduit
213− , http-types
214− , ini
215− , memory
216− , raw-strings-qq >= 1
217− , resourcet
218− , retry
219 , tasty
188 , tasty
220 , tasty-hunit
189 , tasty-hunit
221 , tasty-quickcheck
190 , tasty-quickcheck
222 , tasty-smallcheck
191 , tasty-smallcheck
223− , temporary
224− , text
225− , time
226− , transformers
227− , unliftio
228− , unliftio-core
229− , unordered-containers
230− , xml-conduit
231 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
192 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
232− default-language: Haskell2010
233− default-extensions: BangPatterns
234− , FlexibleContexts
235− , FlexibleInstances
236− , MultiParamTypeClasses
237− , MultiWayIf
238− , NoImplicitPrelude
239− , OverloadedStrings
240− , RankNTypes
241− , ScopedTypeVariables
242− , TupleSections
243− , TypeFamilies
244 other-modules: Lib.Prelude
193 other-modules: Lib.Prelude
245 , Network.Minio
194 , Network.Minio
246− , Network.Minio.API
247− , Network.Minio.API.Test
248− , Network.Minio.APICommon
249− , Network.Minio.AdminAPI
250− , Network.Minio.CopyObject
251− , Network.Minio.Data
252− , Network.Minio.Data.ByteString
253− , Network.Minio.Data.Crypto
254− , Network.Minio.Data.Time
255− , Network.Minio.Errors
256− , Network.Minio.JsonParser
257− , Network.Minio.JsonParser.Test
258− , Network.Minio.ListOps
259− , Network.Minio.PresignedOperations
260− , Network.Minio.PutObject
261 , Network.Minio.S3API
195 , Network.Minio.S3API
262− , Network.Minio.SelectAPI
196+ , Network.Minio.AdminAPI
263− , Network.Minio.Sign.V4
264 , Network.Minio.TestHelpers
197 , Network.Minio.TestHelpers
265− , Network.Minio.Utils
198+ , Network.Minio.API.Test
199+ , Network.Minio.JsonParser.Test
266 , Network.Minio.Utils.Test
200 , Network.Minio.Utils.Test
267− , Network.Minio.XmlGenerator
268 , Network.Minio.XmlGenerator.Test
201 , Network.Minio.XmlGenerator.Test
269− , Network.Minio.XmlParser
270 , Network.Minio.XmlParser.Test
202 , Network.Minio.XmlParser.Test
203+ , Network.Minio.Credentials
271
204
272−source-repository head
205+Flag examples
273− type: git
206+ Description: Build the examples
274− location: https://github.com/minio/minio-hs
207+ Default: False
208+ Manual: True
209+
210+common examples-settings
211+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
212+ default-language: Haskell2010
213+ default-extensions: OverloadedStrings
214+ build-depends: base >= 4.7 && < 5
215+ , bytestring
216+ , case-insensitive
217+ , conduit
218+ , conduit-extra
219+ , filepath
220+ , minio-hs
221+ , optparse-applicative
222+ , text
223+ , time
224+ , unliftio
225+ , unordered-containers
226+ hs-source-dirs: examples
227+ if !flag(examples)
228+ buildable: False
229+
230+executable BucketExists
231+ import: examples-settings
232+ scope: private
233+ main-is: BucketExists.hs
234+
235+executable CopyObject
236+ import: examples-settings
237+ scope: private
238+ main-is: CopyObject.hs
239+
240+executable FileUploader
241+ import: examples-settings
242+ scope: private
243+ main-is: FileUploader.hs
244+
245+executable GetConfig
246+ import: examples-settings
247+ scope: private
248+ main-is: GetConfig.hs
249+
250+executable GetObject
251+ import: examples-settings
252+ scope: private
253+ main-is: GetObject.hs
254+
255+executable HeadObject
256+ import: examples-settings
257+ scope: private
258+ main-is: HeadObject.hs
259+
260+executable Heal
261+ import: examples-settings
262+ scope: private
263+ main-is: Heal.hs
264+
265+executable ListBuckets
266+ import: examples-settings
267+ scope: private
268+ main-is: ListBuckets.hs
269+
270+executable ListIncompleteUploads
271+ import: examples-settings
272+ scope: private
273+ main-is: ListIncompleteUploads.hs
274+
275+executable ListObjects
276+ import: examples-settings
277+ scope: private
278+ main-is: ListObjects.hs
279+
280+executable MakeBucket
281+ import: examples-settings
282+ scope: private
283+ main-is: MakeBucket.hs
284+
285+executable PresignedGetObject
286+ import: examples-settings
287+ scope: private
288+ main-is: PresignedGetObject.hs
289+
290+executable PresignedPostPolicy
291+ import: examples-settings
292+ scope: private
293+ main-is: PresignedPostPolicy.hs
294+
295+executable PresignedPutObject
296+ import: examples-settings
297+ scope: private
298+ main-is: PresignedPutObject.hs
299+
300+executable PutObject
301+ import: examples-settings
302+ scope: private
303+ main-is: PutObject.hs
304+
305+executable RemoveBucket
306+ import: examples-settings
307+ scope: private
308+ main-is: RemoveBucket.hs
309+
310+executable RemoveIncompleteUpload
311+ import: examples-settings
312+ scope: private
313+ main-is: RemoveIncompleteUpload.hs
314+
315+executable RemoveObject
316+ import: examples-settings
317+ scope: private
318+ main-is: RemoveObject.hs
319+
320+executable SelectObject
321+ import: examples-settings
322+ scope: private
323+ main-is: SelectObject.hs
324+
325+executable ServerInfo
326+ import: examples-settings
327+ scope: private
328+ main-is: ServerInfo.hs
329+
330+executable ServiceSendRestart
331+ import: examples-settings
332+ scope: private
333+ main-is: ServiceSendRestart.hs
334+
335+executable ServiceSendStop
336+ import: examples-settings
337+ scope: private
338+ main-is: ServiceSendStop.hs
339+
340+executable ServiceStatus
341+ import: examples-settings
342+ scope: private
343+ main-is: ServiceStatus.hs
344+
345+executable SetConfig
346+ import: examples-settings
347+ scope: private
348+ main-is: SetConfig.hs
349+
350+executable AssumeRole
351+ import: examples-settings
352+ scope: private
353+ main-is: AssumeRole.hs
src/Lib/Prelude.hs+33−11
@@ -15,19 +15,41 @@
15--
15--
16
16
17module Lib.Prelude
17module Lib.Prelude
18− ( module Exports
18+ ( module Exports,
19− , both
19+ both,
20+ showBS,
21+ toStrictBS,
22+ fromStrictBS,
23+ lastMay,
24+ )
20− ) where
25+where
21
26
22−import Protolude as Exports hiding (catch, catches,
23− throwIO, try)
24−
25−import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
27+import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
28+import qualified Data.ByteString.Lazy as LB
26−import Data.Time as Exports (UTCTime (..),
29+import Data.Time as Exports
30+ ( UTCTime (..),
27− diffUTCTime)
31+ diffUTCTime,
32+ )
28−import UnliftIO as Exports (catch, catches, throwIO,
33+import UnliftIO as Exports
34+ ( Handler,
35+ catch,
36+ catches,
37+ throwIO,
29− try)
38+ try,
39+ )
30
40
31-- | Apply a function on both elements of a pair
41-- | Apply a function on both elements of a pair
32both :: (a -> b) -> (a, a) -> (b, b)
42both :: (a -> b) -> (a, a) -> (b, b)
33both f (a, b) = (f a, f b)
43both f (a, b) = (f a, f b)
44+
45+showBS :: (Show a) => a -> ByteString
46+showBS a = encodeUtf8 (show a :: Text)
47+
48+toStrictBS :: LByteString -> ByteString
49+toStrictBS = LB.toStrict
50+
51+fromStrictBS :: ByteString -> LByteString
52+fromStrictBS = LB.fromStrict
53+
54+lastMay :: [a] -> Maybe a
55+lastMay a = last <$> nonEmpty a
src/Network/Minio.hs+254−218
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -16,219 +16,237 @@
16
16
17-- |
17-- |
18-- Module: Network.Minio
18-- Module: Network.Minio
19−-- Copyright: (c) 2017-2019 MinIO Dev Team
19+-- Copyright: (c) 2017-2023 MinIO Dev Team
20-- License: Apache 2.0
20-- License: Apache 2.0
21-- Maintainer: MinIO Dev Team <dev@min.io>
21-- Maintainer: MinIO Dev Team <dev@min.io>
22--
22--
23−-- Types and functions to access S3 compatible object storage servers
23+-- Types and functions to conveniently access S3 compatible object
24−-- like MinIO.
24+-- storage servers like MinIO.
25−
26module Network.Minio
25module Network.Minio
27− (
28− -- * Credentials
26+ ( -- * Credentials
29− Credentials (..)
27+ CredentialValue (..),
30− , Provider
28+ credentialValueText,
29+ AccessKey (..),
30+ SecretKey (..),
31+ SessionToken (..),
32+
33+ -- ** Credential Loaders
34+
35+ -- | Run actions that retrieve 'CredentialValue's from the environment or
36+ -- files or other custom sources.
37+ CredentialLoader,
31− , fromAWSConfigFile
38+ fromAWSConfigFile,
32− , fromAWSEnv
39+ fromAWSEnv,
33− , fromMinioEnv
40+ fromMinioEnv,
34− , findFirst
41+ findFirst,
35−
42+
36− -- * Connecting to object storage
43+ -- * Connecting to object storage
37− ---------------------------------
44+ ConnectInfo,
38− , ConnectInfo
39− , setRegion
45+ setRegion,
40− , setCreds
46+ setCreds,
41− , setCredsFrom
47+ setCredsFrom,
42− , isConnectInfoSecure
48+ isConnectInfoSecure,
43− , disableTLSCertValidation
49+ disableTLSCertValidation,
44− , MinioConn
50+ MinioConn,
45− , mkMinioConn
51+ mkMinioConn,
46−
52+
47− -- ** Connection helpers
53+ -- ** Connection helpers
48− ------------------------
54+
55+ -- | These are helpers to construct 'ConnectInfo' values for common
56+ -- cases.
49− , minioPlayCI
57+ minioPlayCI,
50− , awsCI
58+ awsCI,
51− , gcsCI
59+ gcsCI,
52−
60+
53−
61+ -- ** STS Credential types
62+ STSAssumeRole (..),
63+ STSAssumeRoleOptions (..),
64+ defaultSTSAssumeRoleOptions,
65+ requestSTSCredential,
66+ setSTSCredential,
67+ ExpiryTime (..),
68+ STSCredentialProvider,
69+
54− -- * Minio Monad
70+ -- * Minio Monad
71+
55− ----------------
72+ ----------------
73+
56− -- | The Minio Monad provides connection-reuse, bucket-location
74+ -- | The Minio Monad provides connection-reuse, bucket-location
57− -- caching, resource management and simpler error handling
75+ -- caching, resource management and simpler error handling
58− -- functionality. All actions on object storage are performed within
76+ -- functionality. All actions on object storage are performed within
59− -- this Monad.
77+ -- this Monad.
60−
78+ Minio,
61− , Minio
62− , runMinioWith
79+ runMinioWith,
63− , runMinio
80+ runMinio,
64−
81+ runMinioResWith,
65−
82+ runMinioRes,
83+
66− -- * Bucket Operations
84+ -- * Bucket Operations
67− ----------------------
85+
68−
69− -- ** Creation, removal and querying
86+ -- ** Creation, removal and querying
70− , Bucket
87+ Bucket,
71− , makeBucket
88+ makeBucket,
72− , removeBucket
89+ removeBucket,
73− , bucketExists
90+ bucketExists,
74− , Region
91+ Region,
75− , getLocation
92+ getLocation,
76−
93+
77− -- ** Listing
94+ -- ** Listing buckets
78− , BucketInfo(..)
95+ BucketInfo (..),
79− , listBuckets
96+ listBuckets,
80−
97+
81− -- ** Object info type represents object metadata information.
98+ -- ** Listing objects
99+ listObjects,
100+ listObjectsV1,
101+ ListItem (..),
82− , ObjectInfo
102+ ObjectInfo,
83− , oiObject
103+ oiObject,
84− , oiModTime
104+ oiModTime,
85− , oiETag
105+ oiETag,
86− , oiSize
106+ oiSize,
107+ oiUserMetadata,
87− , oiMetadata
108+ oiMetadata,
88−
109+
89− , ListItem(..)
110+ -- ** Listing incomplete uploads
90− , listObjects
111+ listIncompleteUploads,
91− , listObjectsV1
92−
93− , UploadId
112+ UploadId,
94− , UploadInfo(..)
113+ UploadInfo (..),
95− , listIncompleteUploads
114+ listIncompleteParts,
96− , ObjectPartInfo(..)
115+ ObjectPartInfo (..),
97− , listIncompleteParts
116+
98−
99− -- ** Bucket Notifications
117+ -- ** Bucket Notifications
118+ getBucketNotification,
119+ putBucketNotification,
120+ removeAllBucketNotification,
100− , Notification(..)
121+ Notification (..),
101− , defaultNotification
122+ defaultNotification,
102− , NotificationConfig(..)
123+ NotificationConfig (..),
103− , Arn
124+ Arn,
104− , Event(..)
125+ Event (..),
105− , Filter(..)
126+ Filter (..),
106− , defaultFilter
127+ defaultFilter,
107− , FilterKey(..)
128+ FilterKey (..),
108− , defaultFilterKey
129+ defaultFilterKey,
109− , FilterRules(..)
130+ FilterRules (..),
110− , defaultFilterRules
131+ defaultFilterRules,
111− , FilterRule(..)
132+ FilterRule (..),
112− , getBucketNotification
133+
113− , putBucketNotification
114− , removeAllBucketNotification
115−
116− -- * Object Operations
134+ -- * Object Operations
117− ----------------------
135+ Object,
118− , Object
136+
119−
120− -- ** File operations
137+ -- ** File-based operations
121− , fGetObject
138+ fGetObject,
122− , fPutObject
139+ fPutObject,
123−
140+
124− -- ** Conduit-based streaming operations
141+ -- ** Conduit-based streaming operations
125− , putObject
142+ putObject,
126− , PutObjectOptions
143+ PutObjectOptions,
127− , defaultPutObjectOptions
144+ defaultPutObjectOptions,
128− , pooContentType
145+ pooContentType,
129− , pooContentEncoding
146+ pooContentEncoding,
130− , pooContentDisposition
147+ pooContentDisposition,
131− , pooContentLanguage
148+ pooContentLanguage,
132− , pooCacheControl
149+ pooCacheControl,
133− , pooStorageClass
150+ pooStorageClass,
134− , pooUserMetadata
151+ pooUserMetadata,
135− , pooNumThreads
152+ pooNumThreads,
136− , pooSSE
153+ pooSSE,
137−
138− , getObject
154+ getObject,
139− , GetObjectOptions
155+ GetObjectOptions,
140− , defaultGetObjectOptions
156+ defaultGetObjectOptions,
141− , gooRange
157+ gooRange,
142− , gooIfMatch
158+ gooIfMatch,
143− , gooIfNoneMatch
159+ gooIfNoneMatch,
144− , gooIfModifiedSince
160+ gooIfModifiedSince,
145− , gooIfUnmodifiedSince
161+ gooIfUnmodifiedSince,
146− , gooSSECKey
162+ gooSSECKey,
147−
163+ GetObjectResponse,
164+ gorObjectInfo,
165+ gorObjectStream,
166+
148− -- ** Server-side copying
167+ -- ** Server-side object copying
149− , copyObject
168+ copyObject,
150− , SourceInfo
169+ SourceInfo,
151− , defaultSourceInfo
170+ defaultSourceInfo,
152− , srcBucket
171+ srcBucket,
153− , srcObject
172+ srcObject,
154− , srcRange
173+ srcRange,
155− , srcIfMatch
174+ srcIfMatch,
156− , srcIfNoneMatch
175+ srcIfNoneMatch,
157− , srcIfModifiedSince
176+ srcIfModifiedSince,
158− , srcIfUnmodifiedSince
177+ srcIfUnmodifiedSince,
159− , DestinationInfo
178+ DestinationInfo,
160− , defaultDestinationInfo
179+ defaultDestinationInfo,
161− , dstBucket
180+ dstBucket,
162− , dstObject
181+ dstObject,
163−
182+
164− -- ** Querying
183+ -- ** Querying object info
165− , statObject
184+ statObject,
166−
185+
167− -- ** Object removal functions
186+ -- ** Object removal operations
168− , removeObject
187+ removeObject,
169− , removeIncompleteUpload
188+ removeIncompleteUpload,
170−
189+
171− -- ** Select Object Content with SQL
190+ -- ** Select Object Content with SQL
172− , module Network.Minio.SelectAPI
191+ module Network.Minio.SelectAPI,
173−
192+
174− -- * Server-Size Encryption Helpers
193+ -- * Server-Side Encryption Helpers
175− -----------------------------------
194+ mkSSECKey,
176− , SSECKey
195+ SSECKey,
177− , mkSSECKey
178− , SSE(..)
196+ SSE (..),
179−
197+
180−
181− -- * Presigned Operations
198+ -- * Presigned Operations
182− -------------------------
183− , UrlExpiry
184− , presignedPutObjectUrl
199+ presignedPutObjectUrl,
185− , presignedGetObjectUrl
200+ presignedGetObjectUrl,
186− , presignedHeadObjectUrl
201+ presignedHeadObjectUrl,
187−
202+ UrlExpiry,
203+
188− -- ** Utilities for POST (browser) uploads
204+ -- ** POST (browser) upload helpers
189− , PostPolicy
205+
190− , PostPolicyError(..)
206+ -- | Please see
207+ -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
208+ -- for detailed information.
191− , newPostPolicy
209+ newPostPolicy,
192− , presignedPostPolicy
210+ presignedPostPolicy,
193− , showPostPolicy
211+ showPostPolicy,
194−
212+ PostPolicy,
195− -- *** Utilities to specify Post Policy conditions
213+ PostPolicyError (..),
214+
215+ -- *** Post Policy condition helpers
196− , PostPolicyCondition
216+ PostPolicyCondition,
197− , ppCondBucket
217+ ppCondBucket,
198− , ppCondContentLengthRange
218+ ppCondContentLengthRange,
199− , ppCondContentType
219+ ppCondContentType,
200− , ppCondKey
220+ ppCondKey,
201− , ppCondKeyStartsWith
221+ ppCondKeyStartsWith,
202− , ppCondSuccessActionStatus
222+ ppCondSuccessActionStatus,
203−
223+
204− -- * Error handling
224+ -- * Error handling
205− -----------------------
225+
206− -- | Data types representing various errors that may occur while working
226+ -- | Data types representing various errors that may occur while
207− -- with an object storage service.
227+ -- working with an object storage service.
208− , MinioErr(..)
228+ MinioErr (..),
209− , MErrV(..)
229+ MErrV (..),
210− , ServiceErr(..)
230+ ServiceErr (..),
211−
231+ )
212−) where
232+where
213
233
214{-
234{-
215This module exports the high-level MinIO API for object storage.
235This module exports the high-level MinIO API for object storage.
216-}
236-}
217
237
218−import qualified Data.Conduit as C
238+import qualified Data.Conduit as C
219−import qualified Data.Conduit.Binary as CB
239+import qualified Data.Conduit.Binary as CB
220import qualified Data.Conduit.Combinators as CC
240import qualified Data.Conduit.Combinators as CC
221−
241+import Network.Minio.API
222−import Lib.Prelude
223−
224−import Network.Minio.CopyObject
242+import Network.Minio.CopyObject
243+import Network.Minio.Credentials
225−import Network.Minio.Data
244+import Network.Minio.Data
226−import Network.Minio.Errors
245+import Network.Minio.Errors
227−import Network.Minio.ListOps
246+import Network.Minio.ListOps
228−import Network.Minio.PutObject
247+import Network.Minio.PutObject
229−import Network.Minio.S3API
248+import Network.Minio.S3API
230−import Network.Minio.SelectAPI
249+import Network.Minio.SelectAPI
231−import Network.Minio.Utils
232
250
233-- | Lists buckets.
251-- | Lists buckets.
234listBuckets :: Minio [BucketInfo]
252listBuckets :: Minio [BucketInfo]
@@ -240,11 +258,15 @@ listBuckets = getService
240fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
258fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
241fGetObject bucket object fp opts = do
259fGetObject bucket object fp opts = do
242 src <- getObject bucket object opts
260 src <- getObject bucket object opts
243− C.connect src $ CB.sinkFileCautious fp
261+ C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
244
262
245-- | Upload the given file to the given object.
263-- | Upload the given file to the given object.
246−fPutObject :: Bucket -> Object -> FilePath
264+fPutObject ::
265+ Bucket ->
266+ Object ->
267+ FilePath ->
247− -> PutObjectOptions -> Minio ()
268+ PutObjectOptions ->
269+ Minio ()
248fPutObject bucket object f opts =
270fPutObject bucket object f opts =
249 void $ putObjectInternal bucket object opts $ ODFile f Nothing
271 void $ putObjectInternal bucket object opts $ ODFile f Nothing
250
272
@@ -252,8 +274,13 @@ fPutObject bucket object f opts =
252-- known; this helps the library select optimal part sizes to perform
274-- known; this helps the library select optimal part sizes to perform
253-- a multipart upload. If not specified, it is assumed that the object
275-- a multipart upload. If not specified, it is assumed that the object
254-- can be potentially 5TiB and selects multipart sizes appropriately.
276-- can be potentially 5TiB and selects multipart sizes appropriately.
277+putObject ::
278+ Bucket ->
279+ Object ->
255−putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio ()
280+ C.ConduitM () ByteString Minio () ->
281+ Maybe Int64 ->
256− -> Maybe Int64 -> PutObjectOptions -> Minio ()
282+ PutObjectOptions ->
283+ Minio ()
257putObject bucket object src sizeMay opts =
284putObject bucket object src sizeMay opts =
258 void $ putObjectInternal bucket object opts $ ODStream src sizeMay
285 void $ putObjectInternal bucket object opts $ ODStream src sizeMay
259
286
@@ -263,18 +290,25 @@ putObject bucket object src sizeMay opts =
263-- copy operation if the new object is to be greater than 5GiB in
290-- copy operation if the new object is to be greater than 5GiB in
264-- size.
291-- size.
265copyObject :: DestinationInfo -> SourceInfo -> Minio ()
292copyObject :: DestinationInfo -> SourceInfo -> Minio ()
266−copyObject dstInfo srcInfo = void $ copyObjectInternal (dstBucket dstInfo)
293+copyObject dstInfo srcInfo =
294+ void $
295+ copyObjectInternal
296+ (dstBucket dstInfo)
267− (dstObject dstInfo) srcInfo
297+ (dstObject dstInfo)
298+ srcInfo
268
299
269-- | Remove an object from the object store.
300-- | Remove an object from the object store.
270removeObject :: Bucket -> Object -> Minio ()
301removeObject :: Bucket -> Object -> Minio ()
271removeObject = deleteObject
302removeObject = deleteObject
272
303
273−-- | Get an object from the object store as a resumable source (conduit).
304+-- | Get an object from the object store.
305+getObject ::
306+ Bucket ->
307+ Object ->
274−getObject :: Bucket -> Object -> GetObjectOptions
308+ GetObjectOptions ->
275− -> Minio (C.ConduitM () ByteString Minio ())
309+ Minio GetObjectResponse
276−getObject bucket object opts = snd <$> getObject' bucket object []
310+getObject bucket object opts =
277− (gooToHeaders opts)
311+ getObject' bucket object [] $ gooToHeaders opts
278
312
279-- | Get an object's metadata from the object store. It accepts the
313-- | Get an object's metadata from the object store. It accepts the
280-- same options as GetObject.
314-- same options as GetObject.
@@ -304,6 +338,8 @@ bucketExists = headBucket
304-- | Removes an ongoing multipart upload of an object.
338-- | Removes an ongoing multipart upload of an object.
305removeIncompleteUpload :: Bucket -> Object -> Minio ()
339removeIncompleteUpload :: Bucket -> Object -> Minio ()
306removeIncompleteUpload bucket object = do
340removeIncompleteUpload bucket object = do
341+ uploads <-
342+ C.runConduit $
307− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
343+ listIncompleteUploads bucket (Just object) False
308− C..| CC.sinkList
344+ C..| CC.sinkList
309 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
345 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs+259−159
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,169 +15,260 @@
15--
15--
16
16
17module Network.Minio.API
17module Network.Minio.API
18− ( connect
18+ ( connect,
19− , S3ReqInfo(..)
19+ S3ReqInfo (..),
20− , runMinio
20+ runMinio,
21− , executeRequest
21+ executeRequest,
22+ buildRequest,
22− , mkStreamRequest
23+ mkStreamRequest,
23− , getLocation
24+ getLocation,
24−
25− , isValidBucketName
25+ isValidBucketName,
26− , checkBucketNameValidity
26+ checkBucketNameValidity,
27− , isValidObjectName
27+ isValidObjectName,
28− , checkObjectNameValidity
28+ checkObjectNameValidity,
29+ requestSTSCredential,
30+ )
29− ) where
31+where
30−
32+
31−import Control.Retry (fullJitterBackoff,
33+import Control.Retry
34+ ( fullJitterBackoff,
32− limitRetriesByCumulativeDelay,
35+ limitRetriesByCumulativeDelay,
33− retrying)
36+ retrying,
37+ )
34−import qualified Data.ByteString as B
38+import qualified Data.ByteString as B
35−import qualified Data.Char as C
39+import qualified Data.Char as C
36−import qualified Data.Conduit as C
40+import qualified Data.Conduit as C
37−import qualified Data.Map as Map
41+import qualified Data.HashMap.Strict as H
38−import qualified Data.Text as T
42+import qualified Data.Text as T
39−import qualified Data.Time.Clock as Time
43+import qualified Data.Time.Clock as Time
44+import Lib.Prelude
45+import Network.HTTP.Client (defaultManagerSettings)
46+import qualified Network.HTTP.Client as NClient
40−import Network.HTTP.Conduit (Response)
47+import Network.HTTP.Conduit (Response)
41−import qualified Network.HTTP.Conduit as NC
48+import qualified Network.HTTP.Conduit as NC
49+import Network.HTTP.Types (simpleQueryToQuery)
42−import qualified Network.HTTP.Types as HT
50+import qualified Network.HTTP.Types as HT
43−import Network.HTTP.Types.Header (hHost)
51+import Network.HTTP.Types.Header (hHost)
44−
45−import Lib.Prelude
46−
47−import Network.Minio.APICommon
52+import Network.Minio.APICommon
53+import Network.Minio.Credentials
48−import Network.Minio.Data
54+import Network.Minio.Data
49−import Network.Minio.Errors
55+import Network.Minio.Errors
50−import Network.Minio.Sign.V4
56+import Network.Minio.Sign.V4
51−import Network.Minio.Utils
57+import Network.Minio.Utils
52−import Network.Minio.XmlParser
58+import Network.Minio.XmlParser
53
59
54-- | Fetch bucket location (region)
60-- | Fetch bucket location (region)
55getLocation :: Bucket -> Minio Region
61getLocation :: Bucket -> Minio Region
56getLocation bucket = do
62getLocation bucket = do
63+ resp <-
64+ executeRequest $
57− resp <- executeRequest $ defaultS3ReqInfo {
65+ defaultS3ReqInfo
58− riBucket = Just bucket
66+ { riBucket = Just bucket,
59− , riQueryParams = [("location", Nothing)]
67+ riQueryParams = [("location", Nothing)],
60− , riNeedsLocation = False
68+ riNeedsLocation = False
61− }
69+ }
62 parseLocation $ NC.responseBody resp
70 parseLocation $ NC.responseBody resp
63
71
64−
65-- | Looks for region in RegionMap and updates it using getLocation if
72-- | Looks for region in RegionMap and updates it using getLocation if
66-- absent.
73-- absent.
67discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
74discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
68discoverRegion ri = runMaybeT $ do
75discoverRegion ri = runMaybeT $ do
69 bucket <- MaybeT $ return $ riBucket ri
76 bucket <- MaybeT $ return $ riBucket ri
70 regionMay <- lift $ lookupRegionCache bucket
77 regionMay <- lift $ lookupRegionCache bucket
71− maybe (do
78+ maybe
79+ ( do
72− l <- lift $ getLocation bucket
80+ l <- lift $ getLocation bucket
73− lift $ addToRegionCache bucket l
81+ lift $ addToRegionCache bucket l
74− return l
82+ return l
83+ )
84+ return
75− ) return regionMay
85+ regionMay
76−
86+
87+-- | Returns the region to be used for the request.
77getRegion :: S3ReqInfo -> Minio (Maybe Region)
88getRegion :: S3ReqInfo -> Minio (Maybe Region)
78getRegion ri = do
89getRegion ri = do
79− ci <- asks mcConnInfo
90+ ci <- asks mcConnInfo
80−
91+
81− -- getService/makeBucket/getLocation -- don't need location
92+ -- getService/makeBucket/getLocation -- don't need location
93+ if
82− if | not $ riNeedsLocation ri ->
94+ | not $ riNeedsLocation ri ->
83− return $ Just $ connectRegion ci
95+ return $ Just $ connectRegion ci
84−
85− -- if autodiscovery of location is disabled by user
96+ -- if autodiscovery of location is disabled by user
86− | not $ connectAutoDiscoverRegion ci ->
97+ | not $ connectAutoDiscoverRegion ci ->
87− return $ Just $ connectRegion ci
98+ return $ Just $ connectRegion ci
88−
89− -- discover the region for the request
99+ -- discover the region for the request
90− | otherwise -> discoverRegion ri
100+ | otherwise -> discoverRegion ri
91
101
92getRegionHost :: Region -> Minio Text
102getRegionHost :: Region -> Minio Text
93getRegionHost r = do
103getRegionHost r = do
94− ci <- asks mcConnInfo
104+ ci <- asks mcConnInfo
95−
105+
96− if "amazonaws.com" `T.isSuffixOf` connectHost ci
106+ if "amazonaws.com" `T.isSuffixOf` connectHost ci
107+ then
108+ maybe
97− then maybe (throwIO $ MErrVRegionNotSupported r)
109+ (throwIO $ MErrVRegionNotSupported r)
110+ return
98− return (Map.lookup r awsRegionMap)
111+ (H.lookup r awsRegionMap)
99 else return $ connectHost ci
112 else return $ connectHost ci
100
113
114+-- | Computes the appropriate host, path and region for the request.
115+--
116+-- For AWS, always use virtual bucket style, unless bucket has periods. For
117+-- MinIO and other non-AWS, default to path style.
118+getHostPathRegion :: S3ReqInfo -> Minio (Text, ByteString, Maybe Region)
119+getHostPathRegion ri = do
120+ ci <- asks mcConnInfo
121+ regionMay <- getRegion ri
122+ case riBucket ri of
123+ Nothing ->
124+ -- Implies a ListBuckets request.
125+ return (connectHost ci, "/", regionMay)
126+ Just bucket -> do
127+ regionHost <- case regionMay of
128+ Nothing -> return $ connectHost ci
129+ Just "" -> return $ connectHost ci
130+ Just r -> getRegionHost r
131+ let pathStyle =
132+ ( regionHost,
133+ getS3Path (riBucket ri) (riObject ri),
134+ regionMay
135+ )
136+ virtualStyle =
137+ ( bucket <> "." <> regionHost,
138+ encodeUtf8 $ "/" <> fromMaybe "" (riObject ri),
139+ regionMay
140+ )
141+ ( if isAWSConnectInfo ci
142+ then
143+ return $
144+ if bucketHasPeriods bucket
145+ then pathStyle
146+ else virtualStyle
147+ else return pathStyle
148+ )
149+
150+-- | requestSTSCredential requests temporary credentials using the Security Token
151+-- Service API. The returned credential will include a populated 'SessionToken'
152+-- and an 'ExpiryTime'.
153+requestSTSCredential :: (STSCredentialProvider p) => p -> IO (CredentialValue, ExpiryTime)
154+requestSTSCredential p = do
155+ endpoint <- maybe (throwIO $ MErrValidation MErrVSTSEndpointNotFound) return $ getSTSEndpoint p
156+ let endPt = NC.parseRequest_ $ toString endpoint
157+ settings
158+ | NC.secure endPt = NC.tlsManagerSettings
159+ | otherwise = defaultManagerSettings
160+
161+ mgr <- NC.newManager settings
162+ liftIO $ retrieveSTSCredentials p ("", 0, False) mgr
163+
101buildRequest :: S3ReqInfo -> Minio NC.Request
164buildRequest :: S3ReqInfo -> Minio NC.Request
102buildRequest ri = do
165buildRequest ri = do
103− maybe (return ()) checkBucketNameValidity $ riBucket ri
166+ maybe (return ()) checkBucketNameValidity $ riBucket ri
104− maybe (return ()) checkObjectNameValidity $ riObject ri
167+ maybe (return ()) checkObjectNameValidity $ riObject ri
105−
168+
106− ci <- asks mcConnInfo
169+ ci <- asks mcConnInfo
107−
170+
108− regionMay <- getRegion ri
171+ (host, path, regionMay) <- getHostPathRegion ri
109−
172+
110− regionHost <- maybe (return $ connectHost ci) getRegionHost regionMay
111−
112− let ri' = ri { riHeaders = hostHeader : riHeaders ri
113− , riRegion = regionMay
114− }
115− ci' = ci { connectHost = regionHost }
173+ let ci' = ci {connectHost = host}
116− hostHeader = (hHost, getHostAddr ci')
174+ hostHeader = (hHost, getHostAddr ci')
117−
175+ ri' =
176+ ri
177+ { riHeaders = hostHeader : riHeaders ri,
178+ riRegion = regionMay
179+ }
118− -- Does not contain body and auth info.
180+ -- Does not contain body and auth info.
181+ baseRequest =
119− baseRequest = NC.defaultRequest
182+ NC.defaultRequest
120− { NC.method = riMethod ri'
183+ { NC.method = riMethod ri',
121− , NC.secure = connectIsSecure ci'
184+ NC.secure = connectIsSecure ci',
122− , NC.host = encodeUtf8 $ connectHost ci'
185+ NC.host = encodeUtf8 $ connectHost ci',
123− , NC.port = connectPort ci'
186+ NC.port = connectPort ci',
124− , NC.path = getS3Path (riBucket ri') (riObject ri')
187+ NC.path = path,
125− , NC.requestHeaders = riHeaders ri'
188+ NC.requestHeaders = riHeaders ri',
126− , NC.queryString = HT.renderQuery False $ riQueryParams ri'
189+ NC.queryString = HT.renderQuery False $ riQueryParams ri'
127− }
190+ }
128−
191+
129− timeStamp <- liftIO Time.getCurrentTime
192+ timeStamp <- liftIO Time.getCurrentTime
130−
131− let sp = SignParams (connectAccessKey ci') (connectSecretKey ci')
132− timeStamp (riRegion ri') Nothing Nothing
133−
134− -- Cases to handle:
135− --
136− -- 1. Connection is secure: use unsigned payload
137− --
138− -- 2. Insecure connection, streaming signature is enabled via use of
139− -- conduit payload: use streaming signature for request.
140− --
141− -- 3. Insecure connection, non-conduit payload: compute payload
142− -- sha256hash, buffer request in memory and perform request.
143−
144− -- case 2 from above.
145− if | isStreamingPayload (riPayload ri') &&
146− (not $ connectIsSecure ci') -> do
147− (pLen, pSrc) <- case riPayload ri of
148− PayloadC l src -> return (l, src)
149− _ -> throwIO MErrVUnexpectedPayload
150− let reqFn = signV4Stream pLen sp baseRequest
151− return $ reqFn pSrc
152−
153− | otherwise -> do
154− -- case 1 described above.
155− sp' <- if | connectIsSecure ci' -> return sp
156− -- case 3 described above.
157− | otherwise -> do
158− pHash <- getPayloadSHA256Hash $ riPayload ri'
159− return $ sp { spPayloadHash = Just pHash }
160−
161− let signHeaders = signV4 sp' baseRequest
162− return $ baseRequest
163− { NC.requestHeaders =
164− NC.requestHeaders baseRequest ++
165− mkHeaderFromPairs signHeaders
166− , NC.requestBody = getRequestBody (riPayload ri')
167− }
168
193
194+ mgr <- asks mcConnManager
195+ cv <- liftIO $ getCredential (connectCreds ci') (getEndpoint ci') mgr
196+
197+ let sp =
198+ SignParams
199+ (coerce $ cvAccessKey cv)
200+ (coerce $ cvSecretKey cv)
201+ (coerce $ cvSessionToken cv)
202+ ServiceS3
203+ timeStamp
204+ (riRegion ri')
205+ (riPresignExpirySecs ri')
206+ Nothing
207+
208+ -- Cases to handle:
209+ --
210+ -- 0. Handle presign URL case.
211+ --
212+ -- 1. Connection is secure: use unsigned payload
213+ --
214+ -- 2. Insecure connection, streaming signature is enabled via use of
215+ -- conduit payload: use streaming signature for request.
216+ --
217+ -- 3. Insecure connection, non-conduit payload: compute payload
218+ -- sha256hash, buffer request in memory and perform request.
219+
220+ if
221+ | isJust (riPresignExpirySecs ri') ->
222+ -- case 0 from above.
223+ do
224+ let signPairs = signV4QueryParams sp baseRequest
225+ qpToAdd = simpleQueryToQuery signPairs
226+ existingQueryParams = HT.parseQuery (NC.queryString baseRequest)
227+ updatedQueryParams = existingQueryParams ++ qpToAdd
228+ return $ NClient.setQueryString updatedQueryParams baseRequest
229+ | isStreamingPayload (riPayload ri') && not (connectIsSecure ci') ->
230+ -- case 2 from above.
231+ do
232+ (pLen, pSrc) <- case riPayload ri of
233+ PayloadC l src -> return (l, src)
234+ _ -> throwIO MErrVUnexpectedPayload
235+ let reqFn = signV4Stream pLen sp baseRequest
236+ return $ reqFn pSrc
237+ | otherwise ->
238+ do
239+ sp' <-
240+ ( if connectIsSecure ci'
241+ then -- case 1 described above.
242+ return sp
243+ else
244+ ( -- case 3 described above.
245+ do
246+ pHash <- getPayloadSHA256Hash $ riPayload ri'
247+ return $ sp {spPayloadHash = Just pHash}
248+ )
249+ )
250+
251+ let signHeaders = signV4 sp' baseRequest
252+ return $
253+ baseRequest
254+ { NC.requestHeaders =
255+ NC.requestHeaders baseRequest ++ signHeaders,
256+ NC.requestBody = getRequestBody (riPayload ri')
257+ }
169
258
170retryAPIRequest :: Minio a -> Minio a
259retryAPIRequest :: Minio a -> Minio a
171retryAPIRequest apiCall = do
260retryAPIRequest apiCall = do
261+ resE <-
172− resE <- retrying retryPolicy (const shouldRetry) $
262+ retrying retryPolicy (const shouldRetry) $
263+ const $
173− const $ try apiCall
264+ try apiCall
174 either throwIO return resE
265 either throwIO return resE
175 where
266 where
176 -- Retry using the full-jitter backoff method for up to 10 mins
267 -- Retry using the full-jitter backoff method for up to 10 mins
177 -- total
268 -- total
269+ retryPolicy =
178− retryPolicy = limitRetriesByCumulativeDelay tenMins
270+ limitRetriesByCumulativeDelay tenMins $
179− $ fullJitterBackoff oneMilliSecond
271+ fullJitterBackoff oneMilliSecond
180−
181 oneMilliSecond = 1000 -- in microseconds
272 oneMilliSecond = 1000 -- in microseconds
182 tenMins = 10 * 60 * 1000000 -- in microseconds
273 tenMins = 10 * 60 * 1000000 -- in microseconds
183 -- retry on connection related failure
274 -- retry on connection related failure
@@ -189,23 +280,23 @@ retryAPIRequest apiCall = do
189 -- API request failed with a retryable exception
280 -- API request failed with a retryable exception
190 Left httpExn@(NC.HttpExceptionRequest _ exn) ->
281 Left httpExn@(NC.HttpExceptionRequest _ exn) ->
191 case (exn :: NC.HttpExceptionContent) of
282 case (exn :: NC.HttpExceptionContent) of
192− NC.ResponseTimeout -> return True
283+ NC.ResponseTimeout -> return True
193− NC.ConnectionTimeout -> return True
284+ NC.ConnectionTimeout -> return True
194 NC.ConnectionFailure _ -> return True
285 NC.ConnectionFailure _ -> return True
195 -- We received an unexpected exception
286 -- We received an unexpected exception
196− _ -> throwIO httpExn
287+ _ -> throwIO httpExn
197 -- We received an unexpected exception
288 -- We received an unexpected exception
198 Left someOtherExn -> throwIO someOtherExn
289 Left someOtherExn -> throwIO someOtherExn
199
290
200−
201executeRequest :: S3ReqInfo -> Minio (Response LByteString)
291executeRequest :: S3ReqInfo -> Minio (Response LByteString)
202executeRequest ri = do
292executeRequest ri = do
203 req <- buildRequest ri
293 req <- buildRequest ri
204 mgr <- asks mcConnManager
294 mgr <- asks mcConnManager
205 retryAPIRequest $ httpLbs req mgr
295 retryAPIRequest $ httpLbs req mgr
206
296
207−mkStreamRequest :: S3ReqInfo
297+mkStreamRequest ::
298+ S3ReqInfo ->
208− -> Minio (Response (C.ConduitM () ByteString Minio ()))
299+ Minio (Response (C.ConduitM () ByteString Minio ()))
209mkStreamRequest ri = do
300mkStreamRequest ri = do
210 req <- buildRequest ri
301 req <- buildRequest ri
211 mgr <- asks mcConnManager
302 mgr <- asks mcConnManager
@@ -214,41 +305,50 @@ mkStreamRequest ri = do
214-- Bucket name validity check according to AWS rules.
305-- Bucket name validity check according to AWS rules.
215isValidBucketName :: Bucket -> Bool
306isValidBucketName :: Bucket -> Bool
216isValidBucketName bucket =
307isValidBucketName bucket =
308+ not
309+ ( or
217− not (or [ len < 3 || len > 63
310+ [ len < 3 || len > 63,
218− , or (map labelCheck labels)
311+ any labelCheck labels,
219− , or (map labelCharsCheck labels)
312+ any labelCharsCheck labels,
220− , isIPCheck
313+ isIPCheck
221− ])
314+ ]
315+ )
222 where
316 where
223 len = T.length bucket
317 len = T.length bucket
224 labels = T.splitOn "." bucket
318 labels = T.splitOn "." bucket
225−
226 -- does label `l` fail basic checks of length and start/end?
319 -- does label `l` fail basic checks of length and start/end?
227 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
320 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
228−
229 -- does label `l` have non-allowed characters?
321 -- does label `l` have non-allowed characters?
230− labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
322+ labelCharsCheck l =
323+ isJust $
324+ T.find
325+ ( \x ->
326+ not
327+ ( C.isAsciiLower x
231− x == '-' ||
328+ || x == '-'
232− C.isDigit x)) l
329+ || C.isDigit x
233−
330+ )
331+ )
332+ l
234 -- does label `l` have non-digit characters?
333 -- does label `l` have non-digit characters?
235 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
334 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
236 labelAsNums = map (not . labelNonDigits) labels
335 labelAsNums = map (not . labelNonDigits) labels
237−
238 -- check if bucket name looks like an IP
336 -- check if bucket name looks like an IP
239 isIPCheck = and labelAsNums && length labelAsNums == 4
337 isIPCheck = and labelAsNums && length labelAsNums == 4
240
338
241-- Throws exception iff bucket name is invalid according to AWS rules.
339-- Throws exception iff bucket name is invalid according to AWS rules.
242−checkBucketNameValidity :: MonadIO m => Bucket -> m ()
340+checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
243checkBucketNameValidity bucket =
341checkBucketNameValidity bucket =
244− when (not $ isValidBucketName bucket) $
342+ unless (isValidBucketName bucket) $
343+ throwIO $
245− throwIO $ MErrVInvalidBucketName bucket
344+ MErrVInvalidBucketName bucket
246
345
247isValidObjectName :: Object -> Bool
346isValidObjectName :: Object -> Bool
248isValidObjectName object =
347isValidObjectName object =
249 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
348 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
250
349
251−checkObjectNameValidity :: MonadIO m => Object -> m ()
350+checkObjectNameValidity :: (MonadIO m) => Object -> m ()
252checkObjectNameValidity object =
351checkObjectNameValidity object =
253− when (not $ isValidObjectName object) $
352+ unless (isValidObjectName object) $
353+ throwIO $
254− throwIO $ MErrVInvalidObjectName object
354+ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hs+36−24
@@ -16,37 +16,39 @@
16
16
17module Network.Minio.APICommon where
17module Network.Minio.APICommon where
18
18
19−import qualified Conduit as C
19+import qualified Conduit as C
20−import qualified Data.ByteString as BS
20+import qualified Data.ByteString as BS
21−import qualified Data.ByteString.Lazy as LB
21+import qualified Data.ByteString.Lazy as LB
22−import Data.Conduit.Binary (sourceHandleRange)
22+import Data.Conduit.Binary (sourceHandleRange)
23+import qualified Data.Text as T
24+import Lib.Prelude
23−import qualified Network.HTTP.Conduit as NC
25+import qualified Network.HTTP.Conduit as NC
24−import qualified Network.HTTP.Types as HT
26+import qualified Network.HTTP.Types as HT
25−
26−import Lib.Prelude
27−
28−import Network.Minio.Data
27+import Network.Minio.Data
29−import Network.Minio.Data.Crypto
28+import Network.Minio.Data.Crypto
30−import Network.Minio.Errors
29+import Network.Minio.Errors
31
30
32sha256Header :: ByteString -> HT.Header
31sha256Header :: ByteString -> HT.Header
33−sha256Header = ("x-amz-content-sha256", )
32+sha256Header = ("x-amz-content-sha256",)
34
33
35-- | This function throws an error if the payload is a conduit (as it
34-- | This function throws an error if the payload is a conduit (as it
36-- will not be possible to re-read the conduit after it is consumed).
35-- will not be possible to re-read the conduit after it is consumed).
37getPayloadSHA256Hash :: Payload -> Minio ByteString
36getPayloadSHA256Hash :: Payload -> Minio ByteString
38getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
37getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
39−getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
38+getPayloadSHA256Hash (PayloadH h off size) =
39+ hashSHA256FromSource $
40− sourceHandleRange h
40+ sourceHandleRange
41+ h
41− (return . fromIntegral $ off)
42+ (return . fromIntegral $ off)
42− (return . fromIntegral $ size)
43+ (return . fromIntegral $ size)
43getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
44getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
44
45
45getRequestBody :: Payload -> NC.RequestBody
46getRequestBody :: Payload -> NC.RequestBody
46getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
47getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
47getRequestBody (PayloadH h off size) =
48getRequestBody (PayloadH h off size) =
48− NC.requestBodySource (fromIntegral size) $
49+ NC.requestBodySource size $
49− sourceHandleRange h
50+ sourceHandleRange
51+ h
50 (return . fromIntegral $ off)
52 (return . fromIntegral $ off)
51 (return . fromIntegral $ size)
53 (return . fromIntegral $ size)
52getRequestBody (PayloadC n src) = NC.requestBodySource n src
54getRequestBody (PayloadC n src) = NC.requestBodySource n src
@@ -55,14 +57,24 @@ mkStreamingPayload :: Payload -> Payload
55mkStreamingPayload payload =
57mkStreamingPayload payload =
56 case payload of
58 case payload of
57 PayloadBS bs ->
59 PayloadBS bs ->
60+ PayloadC
58− PayloadC (fromIntegral $ BS.length bs)
61+ (fromIntegral $ BS.length bs)
59 (C.sourceLazy $ LB.fromStrict bs)
62 (C.sourceLazy $ LB.fromStrict bs)
60 PayloadH h off len ->
63 PayloadH h off len ->
64+ PayloadC len $
61− PayloadC len $ sourceHandleRange h
65+ sourceHandleRange
66+ h
62− (return . fromIntegral $ off)
67+ (return . fromIntegral $ off)
63− (return . fromIntegral $ len)
68+ (return . fromIntegral $ len)
64 _ -> payload
69 _ -> payload
65
70
66isStreamingPayload :: Payload -> Bool
71isStreamingPayload :: Payload -> Bool
67isStreamingPayload (PayloadC _ _) = True
72isStreamingPayload (PayloadC _ _) = True
68−isStreamingPayload _ = False
73+isStreamingPayload _ = False
74+
75+-- | Checks if the connect info is for Amazon S3.
76+isAWSConnectInfo :: ConnectInfo -> Bool
77+isAWSConnectInfo ci = ".amazonaws.com" `T.isSuffixOf` connectHost ci
78+
79+bucketHasPeriods :: Bucket -> Bool
80+bucketHasPeriods b = isJust $ T.find (== '.') b
src/Network/Minio/AdminAPI.hs+543−391
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2018-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -16,270 +16,341 @@
16
16
17module Network.Minio.AdminAPI
17module Network.Minio.AdminAPI
18 ( -- * MinIO Admin API
18 ( -- * MinIO Admin API
19+
19 --------------------
20 --------------------
21+
20 -- | Provides MinIO admin API and related types. It is in
22 -- | Provides MinIO admin API and related types. It is in
21 -- experimental state.
23 -- experimental state.
22− DriveInfo(..)
24+ DriveInfo (..),
23− , ErasureInfo(..)
25+ ErasureInfo (..),
24− , Backend(..)
26+ Backend (..),
25− , ConnStats(..)
27+ ConnStats (..),
26− , HttpStats(..)
28+ HttpStats (..),
27− , ServerProps(..)
29+ ServerProps (..),
28− , CountNAvgTime(..)
30+ CountNAvgTime (..),
29− , StorageClass(..)
31+ StorageClass (..),
30− , StorageInfo(..)
32+ StorageInfo (..),
31− , SIData(..)
33+ SIData (..),
32− , ServerInfo(..)
34+ ServerInfo (..),
33− , getServerInfo
35+ getServerInfo,
34−
35− , HealOpts(..)
36+ HealOpts (..),
36− , HealResultItem(..)
37+ HealResultItem (..),
37− , HealStatus(..)
38+ HealStatus (..),
38− , HealStartResp(..)
39+ HealStartResp (..),
39− , startHeal
40+ startHeal,
40− , forceStartHeal
41+ forceStartHeal,
41− , getHealStatus
42+ getHealStatus,
42−
43− , SetConfigResult(..)
43+ SetConfigResult (..),
44− , NodeSummary(..)
44+ NodeSummary (..),
45− , setConfig
45+ setConfig,
46− , getConfig
46+ getConfig,
47−
48− , ServerVersion(..)
47+ ServerVersion (..),
49− , ServiceStatus(..)
48+ ServiceStatus (..),
50− , serviceStatus
49+ serviceStatus,
51−
52− , ServiceAction(..)
50+ ServiceAction (..),
53− , serviceSendAction
51+ serviceSendAction,
52+ )
54− ) where
53+where
55−
54+
56−import Data.Aeson (FromJSON, ToJSON, Value (Object),
55+import Data.Aeson
56+ ( FromJSON,
57+ ToJSON,
58+ Value (Object),
57− eitherDecode, object, pairs,
59+ eitherDecode,
60+ object,
61+ pairs,
62+ parseJSON,
58− parseJSON, toEncoding, toJSON,
63+ toEncoding,
64+ toJSON,
59− withObject, withText, (.:), (.:?),
65+ withObject,
66+ withText,
67+ (.:),
68+ (.:?),
60− (.=))
69+ (.=),
70+ )
61−import qualified Data.Aeson as A
71+import qualified Data.Aeson as A
62−import Data.Aeson.Types (typeMismatch)
72+import Data.Aeson.Types (typeMismatch)
63−import qualified Data.ByteString as B
73+import qualified Data.ByteString as B
64−import qualified Data.ByteString.Lazy as LBS
74+import qualified Data.ByteString.Lazy as LBS
65−import qualified Data.Text as T
75+import qualified Data.Text as T
66−import Data.Time (NominalDiffTime, getCurrentTime)
76+import Data.Time (NominalDiffTime, getCurrentTime)
77+import Lib.Prelude
67−import Network.HTTP.Conduit (Response)
78+import Network.HTTP.Conduit (Response)
68−import qualified Network.HTTP.Conduit as NC
79+import qualified Network.HTTP.Conduit as NC
69−import qualified Network.HTTP.Types as HT
80+import qualified Network.HTTP.Types as HT
70−import Network.HTTP.Types.Header (hHost)
81+import Network.HTTP.Types.Header (hHost)
71−
72−import Lib.Prelude
73−
74−import Network.Minio.APICommon
82+import Network.Minio.APICommon
83+import Network.Minio.Credentials
75−import Network.Minio.Data
84+import Network.Minio.Data
76−import Network.Minio.Errors
85+import Network.Minio.Errors
77−import Network.Minio.Sign.V4
86+import Network.Minio.Sign.V4
78−import Network.Minio.Utils
87+import Network.Minio.Utils
79
88
80data DriveInfo = DriveInfo
89data DriveInfo = DriveInfo
81− { diUuid :: Text
90+ { diUuid :: Text,
82− , diEndpoint :: Text
91+ diEndpoint :: Text,
83− , diState :: Text
92+ diState :: Text
93+ }
84− } deriving (Eq, Show)
94+ deriving stock (Show, Eq)
85
95
86instance FromJSON DriveInfo where
96instance FromJSON DriveInfo where
87− parseJSON = withObject "DriveInfo" $ \v -> DriveInfo
97+ parseJSON = withObject "DriveInfo" $ \v ->
98+ DriveInfo
99+ <$> v
88− <$> v .: "uuid"
100+ .: "uuid"
101+ <*> v
89− <*> v .: "endpoint"
102+ .: "endpoint"
103+ <*> v
90− <*> v .: "state"
104+ .: "state"
91
105
92data StorageClass = StorageClass
106data StorageClass = StorageClass
93− { scParity :: Int
107+ { scParity :: Int,
94− , scData :: Int
108+ scData :: Int
109+ }
95− } deriving (Eq, Show)
110+ deriving stock (Show, Eq)
96
111
97data ErasureInfo = ErasureInfo
112data ErasureInfo = ErasureInfo
98− { eiOnlineDisks :: Int
113+ { eiOnlineDisks :: Int,
99− , eiOfflineDisks :: Int
114+ eiOfflineDisks :: Int,
100− , eiStandard :: StorageClass
115+ eiStandard :: StorageClass,
101− , eiReducedRedundancy :: StorageClass
116+ eiReducedRedundancy :: StorageClass,
102− , eiSets :: [[DriveInfo]]
117+ eiSets :: [[DriveInfo]]
118+ }
103− } deriving (Eq, Show)
119+ deriving stock (Show, Eq)
104
120
105instance FromJSON ErasureInfo where
121instance FromJSON ErasureInfo where
106− parseJSON = withObject "ErasureInfo" $ \v -> do
122+ parseJSON = withObject "ErasureInfo" $ \v -> do
107− onlineDisks <- v .: "OnlineDisks"
123+ onlineDisks <- v .: "OnlineDisks"
108− offlineDisks <- v .: "OfflineDisks"
124+ offlineDisks <- v .: "OfflineDisks"
125+ stdClass <-
109− stdClass <- StorageClass
126+ StorageClass
127+ <$> v
110− <$> v .: "StandardSCData"
128+ .: "StandardSCData"
129+ <*> v
111− <*> v .: "StandardSCParity"
130+ .: "StandardSCParity"
131+ rrClass <-
112− rrClass <- StorageClass
132+ StorageClass
133+ <$> v
113− <$> v .: "RRSCData"
134+ .: "RRSCData"
135+ <*> v
114− <*> v .: "RRSCParity"
136+ .: "RRSCParity"
115− sets <- v .: "Sets"
137+ sets <- v .: "Sets"
116− return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
138+ return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
117−
139+
118−data Backend = BackendFS
140+data Backend
141+ = BackendFS
119− | BackendErasure ErasureInfo
142+ | BackendErasure ErasureInfo
120− deriving (Eq, Show)
143+ deriving stock (Show, Eq)
121
144
122instance FromJSON Backend where
145instance FromJSON Backend where
123− parseJSON = withObject "Backend" $ \v -> do
146+ parseJSON = withObject "Backend" $ \v -> do
124− typ <- v .: "Type"
147+ typ <- v .: "Type"
125− case typ :: Int of
148+ case typ :: Int of
126− 1 -> return BackendFS
149+ 1 -> return BackendFS
127− 2 -> BackendErasure <$> parseJSON (Object v)
150+ 2 -> BackendErasure <$> parseJSON (Object v)
128− _ -> typeMismatch "BackendType" (Object v)
151+ _ -> typeMismatch "BackendType" (Object v)
129
152
130data ConnStats = ConnStats
153data ConnStats = ConnStats
131− { csTransferred :: Int64
154+ { csTransferred :: Int64,
132− , csReceived :: Int64
155+ csReceived :: Int64
156+ }
133− } deriving (Eq, Show)
157+ deriving stock (Show, Eq)
134
158
135instance FromJSON ConnStats where
159instance FromJSON ConnStats where
136− parseJSON = withObject "ConnStats" $ \v -> ConnStats
160+ parseJSON = withObject "ConnStats" $ \v ->
161+ ConnStats
162+ <$> v
137− <$> v .: "transferred"
163+ .: "transferred"
164+ <*> v
138− <*> v .: "received"
165+ .: "received"
139
166
140data ServerProps = ServerProps
167data ServerProps = ServerProps
141− { spUptime :: NominalDiffTime
168+ { spUptime :: NominalDiffTime,
142− , spVersion :: Text
169+ spVersion :: Text,
143− , spCommitId :: Text
170+ spCommitId :: Text,
144− , spRegion :: Text
171+ spRegion :: Text,
145− , spSqsArns :: [Text]
172+ spSqsArns :: [Text]
173+ }
146− } deriving (Eq, Show)
174+ deriving stock (Show, Eq)
147
175
148instance FromJSON ServerProps where
176instance FromJSON ServerProps where
149− parseJSON = withObject "SIServer" $ \v -> do
177+ parseJSON = withObject "SIServer" $ \v -> do
150− uptimeNs <- v .: "uptime"
178+ uptimeNs <- v .: "uptime"
151− let uptime = uptimeNs / 1e9
179+ let uptime = uptimeNs / 1e9
152− ver <- v .: "version"
180+ ver <- v .: "version"
153− commitId <- v .: "commitID"
181+ commitId <- v .: "commitID"
154− region <- v .: "region"
182+ region <- v .: "region"
155− arn <- v .: "sqsARN"
183+ arn <- v .: "sqsARN"
156− return $ ServerProps uptime ver commitId region arn
184+ return $ ServerProps uptime ver commitId region arn
157
185
158data StorageInfo = StorageInfo
186data StorageInfo = StorageInfo
159− { siUsed :: Int64
187+ { siUsed :: Int64,
160− , siBackend :: Backend
188+ siBackend :: Backend
189+ }
161− } deriving (Eq, Show)
190+ deriving stock (Show, Eq)
162
191
163instance FromJSON StorageInfo where
192instance FromJSON StorageInfo where
164− parseJSON = withObject "StorageInfo" $ \v -> StorageInfo
193+ parseJSON = withObject "StorageInfo" $ \v ->
194+ StorageInfo
195+ <$> v
165− <$> v .: "Used"
196+ .: "Used"
197+ <*> v
166− <*> v .: "Backend"
198+ .: "Backend"
167
199
168data CountNAvgTime = CountNAvgTime
200data CountNAvgTime = CountNAvgTime
169− { caCount :: Int64
201+ { caCount :: Int64,
170− , caAvgDuration :: Text
202+ caAvgDuration :: Text
203+ }
171− } deriving (Eq, Show)
204+ deriving stock (Show, Eq)
172
205
173instance FromJSON CountNAvgTime where
206instance FromJSON CountNAvgTime where
174− parseJSON = withObject "CountNAvgTime" $ \v -> CountNAvgTime
207+ parseJSON = withObject "CountNAvgTime" $ \v ->
208+ CountNAvgTime
209+ <$> v
175− <$> v .: "count"
210+ .: "count"
211+ <*> v
176− <*> v .: "avgDuration"
212+ .: "avgDuration"
177
213
178data HttpStats = HttpStats
214data HttpStats = HttpStats
179− { hsTotalHeads :: CountNAvgTime
215+ { hsTotalHeads :: CountNAvgTime,
180− , hsSuccessHeads :: CountNAvgTime
216+ hsSuccessHeads :: CountNAvgTime,
181− , hsTotalGets :: CountNAvgTime
217+ hsTotalGets :: CountNAvgTime,
182− , hsSuccessGets :: CountNAvgTime
218+ hsSuccessGets :: CountNAvgTime,
183− , hsTotalPuts :: CountNAvgTime
219+ hsTotalPuts :: CountNAvgTime,
184− , hsSuccessPuts :: CountNAvgTime
220+ hsSuccessPuts :: CountNAvgTime,
185− , hsTotalPosts :: CountNAvgTime
221+ hsTotalPosts :: CountNAvgTime,
186− , hsSuccessPosts :: CountNAvgTime
222+ hsSuccessPosts :: CountNAvgTime,
187− , hsTotalDeletes :: CountNAvgTime
223+ hsTotalDeletes :: CountNAvgTime,
188− , hsSuccessDeletes :: CountNAvgTime
224+ hsSuccessDeletes :: CountNAvgTime
225+ }
189− } deriving (Eq, Show)
226+ deriving stock (Show, Eq)
190
227
191instance FromJSON HttpStats where
228instance FromJSON HttpStats where
192− parseJSON = withObject "HttpStats" $ \v -> HttpStats
229+ parseJSON = withObject "HttpStats" $ \v ->
230+ HttpStats
231+ <$> v
193− <$> v .: "totalHEADs"
232+ .: "totalHEADs"
233+ <*> v
194− <*> v .: "successHEADs"
234+ .: "successHEADs"
235+ <*> v
195− <*> v .: "totalGETs"
236+ .: "totalGETs"
237+ <*> v
196− <*> v .: "successGETs"
238+ .: "successGETs"
239+ <*> v
197− <*> v .: "totalPUTs"
240+ .: "totalPUTs"
241+ <*> v
198− <*> v .: "successPUTs"
242+ .: "successPUTs"
243+ <*> v
199− <*> v .: "totalPOSTs"
244+ .: "totalPOSTs"
245+ <*> v
200− <*> v .: "successPOSTs"
246+ .: "successPOSTs"
247+ <*> v
201− <*> v .: "totalDELETEs"
248+ .: "totalDELETEs"
249+ <*> v
202− <*> v .: "successDELETEs"
250+ .: "successDELETEs"
203
251
204data SIData = SIData
252data SIData = SIData
205− { sdStorage :: StorageInfo
253+ { sdStorage :: StorageInfo,
206− , sdConnStats :: ConnStats
254+ sdConnStats :: ConnStats,
207− , sdHttpStats :: HttpStats
255+ sdHttpStats :: HttpStats,
208− , sdProps :: ServerProps
256+ sdProps :: ServerProps
257+ }
209− } deriving (Eq, Show)
258+ deriving stock (Show, Eq)
210
259
211instance FromJSON SIData where
260instance FromJSON SIData where
212− parseJSON = withObject "SIData" $ \v -> SIData
261+ parseJSON = withObject "SIData" $ \v ->
262+ SIData
263+ <$> v
213− <$> v .: "storage"
264+ .: "storage"
265+ <*> v
214− <*> v .: "network"
266+ .: "network"
267+ <*> v
215− <*> v .: "http"
268+ .: "http"
269+ <*> v
216− <*> v .: "server"
270+ .: "server"
217
271
218data ServerInfo = ServerInfo
272data ServerInfo = ServerInfo
219− { siError :: Text
273+ { siError :: Text,
220− , siAddr :: Text
274+ siAddr :: Text,
221− , siData :: SIData
275+ siData :: SIData
276+ }
222− } deriving (Eq, Show)
277+ deriving stock (Show, Eq)
223
278
224instance FromJSON ServerInfo where
279instance FromJSON ServerInfo where
225− parseJSON = withObject "ServerInfo" $ \v -> ServerInfo
280+ parseJSON = withObject "ServerInfo" $ \v ->
281+ ServerInfo
282+ <$> v
226− <$> v .: "error"
283+ .: "error"
284+ <*> v
227− <*> v .: "addr"
285+ .: "addr"
286+ <*> v
228− <*> v .: "data"
287+ .: "data"
229
288
230data ServerVersion = ServerVersion
289data ServerVersion = ServerVersion
231− { svVersion :: Text
290+ { svVersion :: Text,
232− , svCommitId :: Text
291+ svCommitId :: Text
292+ }
233− } deriving (Eq, Show)
293+ deriving stock (Show, Eq)
234
294
235instance FromJSON ServerVersion where
295instance FromJSON ServerVersion where
236− parseJSON = withObject "ServerVersion" $ \v -> ServerVersion
296+ parseJSON = withObject "ServerVersion" $ \v ->
297+ ServerVersion
298+ <$> v
237− <$> v .: "version"
299+ .: "version"
300+ <*> v
238− <*> v .: "commitID"
301+ .: "commitID"
239
302
240data ServiceStatus = ServiceStatus
303data ServiceStatus = ServiceStatus
241− { ssVersion :: ServerVersion
304+ { ssVersion :: ServerVersion,
242− , ssUptime :: NominalDiffTime
305+ ssUptime :: NominalDiffTime
306+ }
243− } deriving (Eq, Show)
307+ deriving stock (Show, Eq)
244
308
245instance FromJSON ServiceStatus where
309instance FromJSON ServiceStatus where
246− parseJSON = withObject "ServiceStatus" $ \v -> do
310+ parseJSON = withObject "ServiceStatus" $ \v -> do
247− serverVersion <- v .: "serverVersion"
311+ serverVersion <- v .: "serverVersion"
248− uptimeNs <- v .: "uptime"
312+ uptimeNs <- v .: "uptime"
249− let uptime = uptimeNs / 1e9
313+ let uptime = uptimeNs / 1e9
250− return $ ServiceStatus serverVersion uptime
314+ return $ ServiceStatus serverVersion uptime
251
315
316+data ServiceAction
252−data ServiceAction = ServiceActionRestart
317+ = ServiceActionRestart
253− | ServiceActionStop
318+ | ServiceActionStop
254− deriving (Eq, Show)
319+ deriving stock (Show, Eq)
255
320
256instance ToJSON ServiceAction where
321instance ToJSON ServiceAction where
257− toJSON a = object [ "action" .= serviceActionToText a ]
322+ toJSON a = object ["action" .= serviceActionToText a]
258
323
259serviceActionToText :: ServiceAction -> Text
324serviceActionToText :: ServiceAction -> Text
260serviceActionToText a = case a of
325serviceActionToText a = case a of
261 ServiceActionRestart -> "restart"
326 ServiceActionRestart -> "restart"
262− ServiceActionStop -> "stop"
327+ ServiceActionStop -> "stop"
263
328
264adminPath :: ByteString
329adminPath :: ByteString
265adminPath = "/minio/admin"
330adminPath = "/minio/admin"
266
331
267data HealStartResp = HealStartResp
332data HealStartResp = HealStartResp
268− { hsrClientToken :: Text
333+ { hsrClientToken :: Text,
269− , hsrClientAddr :: Text
334+ hsrClientAddr :: Text,
270− , hsrStartTime :: UTCTime
335+ hsrStartTime :: UTCTime
336+ }
271− } deriving (Eq, Show)
337+ deriving stock (Show, Eq)
272
338
273instance FromJSON HealStartResp where
339instance FromJSON HealStartResp where
274− parseJSON = withObject "HealStartResp" $ \v -> HealStartResp
340+ parseJSON = withObject "HealStartResp" $ \v ->
341+ HealStartResp
342+ <$> v
275− <$> v .: "clientToken"
343+ .: "clientToken"
344+ <*> v
276− <*> v .: "clientAddress"
345+ .: "clientAddress"
346+ <*> v
277− <*> v .: "startTime"
347+ .: "startTime"
278
348
279data HealOpts = HealOpts
349data HealOpts = HealOpts
280− { hoRecursive :: Bool
350+ { hoRecursive :: Bool,
281− , hoDryRun :: Bool
351+ hoDryRun :: Bool
352+ }
282− } deriving (Eq, Show)
353+ deriving stock (Show, Eq)
283
354
284instance ToJSON HealOpts where
355instance ToJSON HealOpts where
285 toJSON (HealOpts r d) =
356 toJSON (HealOpts r d) =
@@ -288,197 +359,259 @@ instance ToJSON HealOpts where
288 pairs ("recursive" .= r <> "dryRun" .= d)
359 pairs ("recursive" .= r <> "dryRun" .= d)
289
360
290instance FromJSON HealOpts where
361instance FromJSON HealOpts where
291− parseJSON = withObject "HealOpts" $ \v -> HealOpts
362+ parseJSON = withObject "HealOpts" $ \v ->
363+ HealOpts
364+ <$> v
292− <$> v .: "recursive"
365+ .: "recursive"
366+ <*> v
293− <*> v .: "dryRun"
367+ .: "dryRun"
294−
368+
295−data HealItemType = HealItemMetadata
369+data HealItemType
370+ = HealItemMetadata
296− | HealItemBucket
371+ | HealItemBucket
297− | HealItemBucketMetadata
372+ | HealItemBucketMetadata
298− | HealItemObject
373+ | HealItemObject
299− deriving (Eq, Show)
374+ deriving stock (Show, Eq)
300
375
301instance FromJSON HealItemType where
376instance FromJSON HealItemType where
302− parseJSON = withText "HealItemType" $ \v -> case v of
377+ parseJSON = withText "HealItemType" $ \v -> case v of
303− "metadata" -> return HealItemMetadata
378+ "metadata" -> return HealItemMetadata
304− "bucket" -> return HealItemBucket
379+ "bucket" -> return HealItemBucket
305− "object" -> return HealItemObject
380+ "object" -> return HealItemObject
306− "bucket-metadata" -> return HealItemBucketMetadata
381+ "bucket-metadata" -> return HealItemBucketMetadata
307− _ -> typeMismatch "HealItemType" (A.String v)
382+ _ -> typeMismatch "HealItemType" (A.String v)
308
383
309data NodeSummary = NodeSummary
384data NodeSummary = NodeSummary
310− { nsName :: Text
385+ { nsName :: Text,
311− , nsErrSet :: Bool
386+ nsErrSet :: Bool,
312− , nsErrMessage :: Text
387+ nsErrMessage :: Text
388+ }
313− } deriving (Eq, Show)
389+ deriving stock (Show, Eq)
314
390
315instance FromJSON NodeSummary where
391instance FromJSON NodeSummary where
316− parseJSON = withObject "NodeSummary" $ \v -> NodeSummary
392+ parseJSON = withObject "NodeSummary" $ \v ->
393+ NodeSummary
394+ <$> v
317− <$> v .: "name"
395+ .: "name"
396+ <*> v
318− <*> v .: "errSet"
397+ .: "errSet"
398+ <*> v
319− <*> v .: "errMsg"
399+ .: "errMsg"
320
400
321data SetConfigResult = SetConfigResult
401data SetConfigResult = SetConfigResult
322− { scrStatus :: Bool
402+ { scrStatus :: Bool,
323− , scrNodeSummary :: [NodeSummary]
403+ scrNodeSummary :: [NodeSummary]
404+ }
324− } deriving (Eq, Show)
405+ deriving stock (Show, Eq)
325
406
326instance FromJSON SetConfigResult where
407instance FromJSON SetConfigResult where
327− parseJSON = withObject "SetConfigResult" $ \v -> SetConfigResult
408+ parseJSON = withObject "SetConfigResult" $ \v ->
409+ SetConfigResult
410+ <$> v
328− <$> v .: "status"
411+ .: "status"
412+ <*> v
329− <*> v .: "nodeResults"
413+ .: "nodeResults"
330
414
331data HealResultItem = HealResultItem
415data HealResultItem = HealResultItem
332− { hriResultIdx :: Int
416+ { hriResultIdx :: Int,
333− , hriType :: HealItemType
417+ hriType :: HealItemType,
334− , hriBucket :: Bucket
418+ hriBucket :: Bucket,
335− , hriObject :: Object
419+ hriObject :: Object,
336− , hriDetail :: Text
420+ hriDetail :: Text,
337− , hriParityBlocks :: Maybe Int
421+ hriParityBlocks :: Maybe Int,
338− , hriDataBlocks :: Maybe Int
422+ hriDataBlocks :: Maybe Int,
339− , hriDiskCount :: Int
423+ hriDiskCount :: Int,
340− , hriSetCount :: Int
424+ hriSetCount :: Int,
341− , hriObjectSize :: Int
425+ hriObjectSize :: Int,
342− , hriBefore :: [DriveInfo]
426+ hriBefore :: [DriveInfo],
343− , hriAfter :: [DriveInfo]
427+ hriAfter :: [DriveInfo]
428+ }
344− } deriving (Eq, Show)
429+ deriving stock (Show, Eq)
345
430
346instance FromJSON HealResultItem where
431instance FromJSON HealResultItem where
347− parseJSON = withObject "HealResultItem" $ \v -> HealResultItem
432+ parseJSON = withObject "HealResultItem" $ \v ->
433+ HealResultItem
434+ <$> v
348− <$> v .: "resultId"
435+ .: "resultId"
436+ <*> v
349− <*> v .: "type"
437+ .: "type"
438+ <*> v
350− <*> v .: "bucket"
439+ .: "bucket"
440+ <*> v
351− <*> v .: "object"
441+ .: "object"
442+ <*> v
352− <*> v .: "detail"
443+ .: "detail"
444+ <*> v
353− <*> v .:? "parityBlocks"
445+ .:? "parityBlocks"
446+ <*> v
354− <*> v .:? "dataBlocks"
447+ .:? "dataBlocks"
448+ <*> v
355− <*> v .: "diskCount"
449+ .: "diskCount"
450+ <*> v
356− <*> v .: "setCount"
451+ .: "setCount"
452+ <*> v
357− <*> v .: "objectSize"
453+ .: "objectSize"
454+ <*> ( do
358− <*> (do before <- v .: "before"
455+ before <- v .: "before"
359− before .: "drives")
456+ before .: "drives"
457+ )
458+ <*> ( do
360− <*> (do after <- v .: "after"
459+ after <- v .: "after"
361− after .: "drives")
460+ after .: "drives"
461+ )
362
462
363data HealStatus = HealStatus
463data HealStatus = HealStatus
364− { hsSummary :: Text
464+ { hsSummary :: Text,
365− , hsStartTime :: UTCTime
465+ hsStartTime :: UTCTime,
366− , hsSettings :: HealOpts
466+ hsSettings :: HealOpts,
367− , hsNumDisks :: Int
467+ hsNumDisks :: Int,
368− , hsFailureDetail :: Maybe Text
468+ hsFailureDetail :: Maybe Text,
369− , hsItems :: Maybe [HealResultItem]
469+ hsItems :: Maybe [HealResultItem]
470+ }
370− } deriving (Eq, Show)
471+ deriving stock (Show, Eq)
371
472
372instance FromJSON HealStatus where
473instance FromJSON HealStatus where
373− parseJSON = withObject "HealStatus" $ \v -> HealStatus
474+ parseJSON = withObject "HealStatus" $ \v ->
475+ HealStatus
476+ <$> v
374− <$> v .: "Summary"
477+ .: "Summary"
478+ <*> v
375− <*> v .: "StartTime"
479+ .: "StartTime"
480+ <*> v
376− <*> v .: "Settings"
481+ .: "Settings"
482+ <*> v
377− <*> v .: "NumDisks"
483+ .: "NumDisks"
484+ <*> v
378− <*> v .:? "Detail"
485+ .:? "Detail"
486+ <*> v
379− <*> v .: "Items"
487+ .: "Items"
380
488
381healPath :: Maybe Bucket -> Maybe Text -> ByteString
489healPath :: Maybe Bucket -> Maybe Text -> ByteString
382healPath bucket prefix = do
490healPath bucket prefix = do
383− if (isJust bucket)
491+ if isJust bucket
492+ then
493+ encodeUtf8 $
494+ "v1/heal/"
384− then encodeUtf8 $ "v1/heal/" <> fromMaybe "" bucket <> "/"
495+ <> fromMaybe "" bucket
496+ <> "/"
385− <> fromMaybe "" prefix
497+ <> fromMaybe "" prefix
386− else encodeUtf8 $ "v1/heal/"
498+ else encodeUtf8 ("v1/heal/" :: Text)
387
499
388-- | Get server version and uptime.
500-- | Get server version and uptime.
389serviceStatus :: Minio ServiceStatus
501serviceStatus :: Minio ServiceStatus
390serviceStatus = do
502serviceStatus = do
503+ rsp <-
504+ executeAdminRequest
505+ AdminReqInfo
391− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
506+ { ariMethod = HT.methodGet,
392− , ariPayload = PayloadBS B.empty
507+ ariPayload = PayloadBS B.empty,
393− , ariPayloadHash = Nothing
508+ ariPayloadHash = Nothing,
394− , ariPath = "v1/service"
509+ ariPath = "v1/service",
395− , ariHeaders = []
510+ ariHeaders = [],
396− , ariQueryParams = []
511+ ariQueryParams = []
397− }
512+ }
398−
513+
399− let rspBS = NC.responseBody rsp
514+ let rspBS = NC.responseBody rsp
400− case eitherDecode rspBS of
515+ case eitherDecode rspBS of
401− Right ss -> return ss
516+ Right ss -> return ss
402− Left err -> throwIO $ MErrVJsonParse $ T.pack err
517+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
403
518
404-- | Send service restart or stop action to MinIO server.
519-- | Send service restart or stop action to MinIO server.
405serviceSendAction :: ServiceAction -> Minio ()
520serviceSendAction :: ServiceAction -> Minio ()
406serviceSendAction action = do
521serviceSendAction action = do
407− let payload = PayloadBS $ LBS.toStrict $ A.encode action
522+ let payload = PayloadBS $ LBS.toStrict $ A.encode action
523+ void $
524+ executeAdminRequest
525+ AdminReqInfo
408− void $ executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
526+ { ariMethod = HT.methodPost,
409− , ariPayload = payload
527+ ariPayload = payload,
410− , ariPayloadHash = Nothing
528+ ariPayloadHash = Nothing,
411− , ariPath = "v1/service"
529+ ariPath = "v1/service",
412− , ariHeaders = []
530+ ariHeaders = [],
413− , ariQueryParams = []
531+ ariQueryParams = []
414− }
532+ }
415
533
416-- | Get the current config file from server.
534-- | Get the current config file from server.
417getConfig :: Minio ByteString
535getConfig :: Minio ByteString
418getConfig = do
536getConfig = do
537+ rsp <-
538+ executeAdminRequest
539+ AdminReqInfo
419− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
540+ { ariMethod = HT.methodGet,
420− , ariPayload = PayloadBS B.empty
541+ ariPayload = PayloadBS B.empty,
421− , ariPayloadHash = Nothing
542+ ariPayloadHash = Nothing,
422− , ariPath = "v1/config"
543+ ariPath = "v1/config",
423− , ariHeaders = []
544+ ariHeaders = [],
424− , ariQueryParams = []
545+ ariQueryParams = []
425− }
546+ }
426− return $ LBS.toStrict $ NC.responseBody rsp
547+ return $ LBS.toStrict $ NC.responseBody rsp
427
548
428-- | Set a new config to the server.
549-- | Set a new config to the server.
429setConfig :: ByteString -> Minio SetConfigResult
550setConfig :: ByteString -> Minio SetConfigResult
430setConfig config = do
551setConfig config = do
552+ rsp <-
553+ executeAdminRequest
554+ AdminReqInfo
431− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPut
555+ { ariMethod = HT.methodPut,
432− , ariPayload = PayloadBS config
556+ ariPayload = PayloadBS config,
433− , ariPayloadHash = Nothing
557+ ariPayloadHash = Nothing,
434− , ariPath = "v1/config"
558+ ariPath = "v1/config",
435− , ariHeaders = []
559+ ariHeaders = [],
436− , ariQueryParams = []
560+ ariQueryParams = []
437− }
561+ }
438−
562+
439− let rspBS = NC.responseBody rsp
563+ let rspBS = NC.responseBody rsp
440− case eitherDecode rspBS of
564+ case eitherDecode rspBS of
441− Right scr -> return scr
565+ Right scr -> return scr
442− Left err -> throwIO $ MErrVJsonParse $ T.pack err
566+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
443
567
444-- | Get the progress of currently running heal task, this API should be
568-- | Get the progress of currently running heal task, this API should be
445-- invoked right after `startHeal`. `token` is obtained after `startHeal`
569-- invoked right after `startHeal`. `token` is obtained after `startHeal`
446-- which should be used to get the heal status.
570-- which should be used to get the heal status.
447getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
571getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
448getHealStatus bucket prefix token = do
572getHealStatus bucket prefix token = do
449− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
573+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
450− let qparams = HT.queryTextToQuery [("clientToken", Just token)]
574+ let qparams = HT.queryTextToQuery [("clientToken", Just token)]
575+ rsp <-
576+ executeAdminRequest
577+ AdminReqInfo
451− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
578+ { ariMethod = HT.methodPost,
452− , ariPayload = PayloadBS B.empty
579+ ariPayload = PayloadBS B.empty,
453− , ariPayloadHash = Nothing
580+ ariPayloadHash = Nothing,
454− , ariPath = healPath bucket prefix
581+ ariPath = healPath bucket prefix,
455− , ariHeaders = []
582+ ariHeaders = [],
456− , ariQueryParams = qparams
583+ ariQueryParams = qparams
457− }
584+ }
458− let rspBS = NC.responseBody rsp
585+ let rspBS = NC.responseBody rsp
459− case eitherDecode rspBS of
586+ case eitherDecode rspBS of
460− Right hs -> return hs
587+ Right hs -> return hs
461− Left err -> throwIO $ MErrVJsonParse $ T.pack err
588+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
462
589
463doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
590doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
464doHeal bucket prefix opts forceStart = do
591doHeal bucket prefix opts forceStart = do
465− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
592+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
466− let payload = PayloadBS $ LBS.toStrict $ A.encode opts
593+ let payload = PayloadBS $ LBS.toStrict $ A.encode opts
594+ let qparams =
595+ bool
596+ []
467− let qparams = bool [] (HT.queryTextToQuery [("forceStart", Just "true")])
597+ (HT.queryTextToQuery [("forceStart", Just "true")])
468− forceStart
598+ forceStart
469−
599+
600+ rsp <-
601+ executeAdminRequest
602+ AdminReqInfo
470− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
603+ { ariMethod = HT.methodPost,
471− , ariPayload = payload
604+ ariPayload = payload,
472− , ariPayloadHash = Nothing
605+ ariPayloadHash = Nothing,
473− , ariPath = healPath bucket prefix
606+ ariPath = healPath bucket prefix,
474− , ariHeaders = []
607+ ariHeaders = [],
475− , ariQueryParams = qparams
608+ ariQueryParams = qparams
476− }
609+ }
477−
610+
478− let rspBS = NC.responseBody rsp
611+ let rspBS = NC.responseBody rsp
479− case eitherDecode rspBS of
612+ case eitherDecode rspBS of
480− Right hsr -> return hsr
613+ Right hsr -> return hsr
481− Left err -> throwIO $ MErrVJsonParse $ T.pack err
614+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
482
615
483-- | Start a heal sequence that scans data under given (possible empty)
616-- | Start a heal sequence that scans data under given (possible empty)
484-- `bucket` and `prefix`. The `recursive` bool turns on recursive
617-- `bucket` and `prefix`. The `recursive` bool turns on recursive
@@ -500,60 +633,79 @@ forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
500-- properties, storage information, network statistics, etc.
633-- properties, storage information, network statistics, etc.
501getServerInfo :: Minio [ServerInfo]
634getServerInfo :: Minio [ServerInfo]
502getServerInfo = do
635getServerInfo = do
636+ rsp <-
637+ executeAdminRequest
638+ AdminReqInfo
503− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
639+ { ariMethod = HT.methodGet,
504− , ariPayload = PayloadBS B.empty
640+ ariPayload = PayloadBS B.empty,
505− , ariPayloadHash = Nothing
641+ ariPayloadHash = Nothing,
506− , ariPath = "v1/info"
642+ ariPath = "v1/info",
507− , ariHeaders = []
643+ ariHeaders = [],
508− , ariQueryParams = []
644+ ariQueryParams = []
509− }
645+ }
510− let rspBS = NC.responseBody rsp
646+ let rspBS = NC.responseBody rsp
511− case eitherDecode rspBS of
647+ case eitherDecode rspBS of
512− Right si -> return si
648+ Right si -> return si
513− Left err -> throwIO $ MErrVJsonParse $ T.pack err
649+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
514
650
515executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
651executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
516executeAdminRequest ari = do
652executeAdminRequest ari = do
517− req <- buildAdminRequest ari
653+ req <- buildAdminRequest ari
518− mgr <- asks mcConnManager
654+ mgr <- asks mcConnManager
519− httpLbs req mgr
655+ httpLbs req mgr
520
656
521buildAdminRequest :: AdminReqInfo -> Minio NC.Request
657buildAdminRequest :: AdminReqInfo -> Minio NC.Request
522buildAdminRequest areq = do
658buildAdminRequest areq = do
523− ci <- asks mcConnInfo
659+ ci <- asks mcConnInfo
660+ sha256Hash <-
524− sha256Hash <- if | connectIsSecure ci ->
661+ if connectIsSecure ci
525− -- if secure connection
662+ then -- if secure connection
526− return "UNSIGNED-PAYLOAD"
663+ return "UNSIGNED-PAYLOAD"
527−
528− -- otherwise compute sha256
664+ else -- otherwise compute sha256
529− | otherwise -> getPayloadSHA256Hash (ariPayload areq)
665+ getPayloadSHA256Hash (ariPayload areq)
530−
666+
531− timeStamp <- liftIO getCurrentTime
667+ timeStamp <- liftIO getCurrentTime
532−
668+
669+ mgr <- asks mcConnManager
670+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671+
533− let hostHeader = (hHost, getHostAddr ci)
672+ let hostHeader = (hHost, getHostAddr ci)
673+ newAreq =
674+ areq
534− newAreq = areq { ariPayloadHash = Just sha256Hash
675+ { ariPayloadHash = Just sha256Hash,
535− , ariHeaders = hostHeader
676+ ariHeaders =
677+ hostHeader
536− : sha256Header sha256Hash
678+ : sha256Header sha256Hash
537− : ariHeaders areq
679+ : ariHeaders areq
538− }
680+ }
539− signReq = toRequest ci newAreq
681+ signReq = toRequest ci newAreq
540− sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
682+ sp =
683+ SignParams
684+ (coerce $ cvAccessKey cv)
685+ (coerce $ cvSecretKey cv)
686+ (coerce $ cvSessionToken cv)
687+ ServiceS3
688+ timeStamp
689+ Nothing
690+ Nothing
541− timeStamp Nothing Nothing (ariPayloadHash newAreq)
691+ (ariPayloadHash newAreq)
542− signHeaders = signV4 sp signReq
692+ signHeaders = signV4 sp signReq
543−
693+
544− -- Update signReq with Authorization header containing v4 signature
694+ -- Update signReq with Authorization header containing v4 signature
695+ return
545− return signReq {
696+ signReq
546− NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
697+ { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
547− }
698+ }
548 where
699 where
549 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
700 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
701+ toRequest ci aReq =
550− toRequest ci aReq = NC.defaultRequest
702+ NC.defaultRequest
551− { NC.method = ariMethod aReq
703+ { NC.method = ariMethod aReq,
552− , NC.secure = connectIsSecure ci
704+ NC.secure = connectIsSecure ci,
553− , NC.host = encodeUtf8 $ connectHost ci
705+ NC.host = encodeUtf8 $ connectHost ci,
554− , NC.port = connectPort ci
706+ NC.port = connectPort ci,
555− , NC.path = B.intercalate "/" [adminPath, ariPath aReq]
707+ NC.path = B.intercalate "/" [adminPath, ariPath aReq],
556− , NC.requestHeaders = ariHeaders aReq
708+ NC.requestHeaders = ariHeaders aReq,
557− , NC.queryString = HT.renderQuery False $ ariQueryParams aReq
709+ NC.queryString = HT.renderQuery False $ ariQueryParams aReq,
558− , NC.requestBody = getRequestBody (ariPayload aReq)
710+ NC.requestBody = getRequestBody (ariPayload aReq)
559 }
711 }
src/Network/Minio/CopyObject.hs+48−34
@@ -16,19 +16,19 @@
16
16
17module Network.Minio.CopyObject where
17module Network.Minio.CopyObject where
18
18
19−import qualified Data.List as List
19+import qualified Data.List as List
20−
21−import Lib.Prelude
20+import Lib.Prelude
22−
23−import Network.Minio.Data
21+import Network.Minio.Data
24−import Network.Minio.Errors
22+import Network.Minio.Errors
25−import Network.Minio.S3API
23+import Network.Minio.S3API
26−import Network.Minio.Utils
24+import Network.Minio.Utils
27−
28
25
29-- | Copy an object using single or multipart copy strategy.
26-- | Copy an object using single or multipart copy strategy.
30−copyObjectInternal :: Bucket -> Object -> SourceInfo
27+copyObjectInternal ::
28+ Bucket ->
29+ Object ->
30+ SourceInfo ->
31− -> Minio ETag
31+ Minio ETag
32copyObjectInternal b' o srcInfo = do
32copyObjectInternal b' o srcInfo = do
33 let sBucket = srcBucket srcInfo
33 let sBucket = srcBucket srcInfo
34 sObject = srcObject srcInfo
34 sObject = srcObject srcInfo
@@ -43,27 +43,33 @@ copyObjectInternal b' o srcInfo = do
43 startOffset = fst range
43 startOffset = fst range
44 endOffset = snd range
44 endOffset = snd range
45
45
46+ when
46− when (isJust rangeMay &&
47+ ( isJust rangeMay
48+ && ( (startOffset < 0)
47− or [startOffset < 0, endOffset < startOffset,
49+ || (endOffset < startOffset)
48− endOffset >= fromIntegral srcSize]) $
50+ || (endOffset >= srcSize)
51+ )
52+ )
53+ $ throwIO
49− throwIO $ MErrVInvalidSrcObjByteRange range
54+ $ MErrVInvalidSrcObjByteRange range
50
55
51 -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
56 -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
52 -- 2. If startOffset /= 0 use multipart copy
57 -- 2. If startOffset /= 0 use multipart copy
53− let destSize = (\(a, b) -> b - a + 1 ) $
58+ let destSize =
59+ (\(a, b) -> b - a + 1) $
54− maybe (0, srcSize - 1) identity rangeMay
60+ maybe (0, srcSize - 1) identity rangeMay
55
61
56 if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
62 if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
57 then multiPartCopyObject b' o srcInfo srcSize
63 then multiPartCopyObject b' o srcInfo srcSize
58−
59− else fst <$> copyObjectSingle b' o srcInfo{srcRange = Nothing} []
64+ else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} []
60
65
61-- | Given the input byte range of the source object, compute the
66-- | Given the input byte range of the source object, compute the
62-- splits for a multipart copy object procedure. Minimum part size
67-- splits for a multipart copy object procedure. Minimum part size
63-- used is minPartSize.
68-- used is minPartSize.
64selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
69selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
65−selectCopyRanges (st, end) = zip pns $
70+selectCopyRanges (st, end) =
71+ zip pns $
66− map (\(x, y) -> (st + x, st + x + y - 1)) $ zip startOffsets partSizes
72+ zipWith (\x y -> (st + x, st + x + y - 1)) startOffsets partSizes
67 where
73 where
68 size = end - st + 1
74 size = end - st + 1
69 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
75 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
@@ -71,22 +77,30 @@ selectCopyRanges (st, end) = zip pns $
71-- | Perform a multipart copy object action. Since we cannot verify
77-- | Perform a multipart copy object action. Since we cannot verify
72-- existing parts based on the source object, there is no resuming
78-- existing parts based on the source object, there is no resuming
73-- copy action support.
79-- copy action support.
74−multiPartCopyObject :: Bucket -> Object -> SourceInfo -> Int64
80+multiPartCopyObject ::
81+ Bucket ->
82+ Object ->
83+ SourceInfo ->
84+ Int64 ->
75− -> Minio ETag
85+ Minio ETag
76multiPartCopyObject b o cps srcSize = do
86multiPartCopyObject b o cps srcSize = do
77 uid <- newMultipartUpload b o []
87 uid <- newMultipartUpload b o []
78
88
79− let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $ srcRange cps
89+ let byteRange = maybe (0, srcSize - 1) identity $ srcRange cps
80 partRanges = selectCopyRanges byteRange
90 partRanges = selectCopyRanges byteRange
91+ partSources =
92+ map
81− partSources = map (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end) }))
93+ (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end)}))
82− partRanges
94+ partRanges
83− dstInfo = defaultDestinationInfo { dstBucket = b, dstObject = o}
95+ dstInfo = defaultDestinationInfo {dstBucket = b, dstObject = o}
84−
96+
97+ copiedParts <-
85− copiedParts <- limitedMapConcurrently 10
98+ limitedMapConcurrently
99+ 10
86− (\(pn, cps') -> do
100+ ( \(pn, cps') -> do
87− (etag, _) <- copyObjectPart dstInfo cps' uid pn []
101+ (etag, _) <- copyObjectPart dstInfo cps' uid pn []
88− return (pn, etag)
102+ return (pn, etag)
89− )
103+ )
90− partSources
104+ partSources
91
105
92 completeMultipartUpload b o uid copiedParts
106 completeMultipartUpload b o uid copiedParts
src/Network/Minio/Credentials.hsnew file+77
@@ -0,0 +1,77 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.Credentials
18+ ( CredentialValue (..),
19+ credentialValueText,
20+ STSCredentialProvider (..),
21+ AccessKey (..),
22+ SecretKey (..),
23+ SessionToken (..),
24+ ExpiryTime (..),
25+ STSCredentialStore,
26+ initSTSCredential,
27+ getSTSCredential,
28+ Creds (..),
29+ getCredential,
30+ Endpoint,
31+
32+ -- * STS Assume Role
33+ defaultSTSAssumeRoleOptions,
34+ STSAssumeRole (..),
35+ STSAssumeRoleOptions (..),
36+ )
37+where
38+
39+import Data.Time (diffUTCTime, getCurrentTime)
40+import qualified Network.HTTP.Client as NC
41+import Network.Minio.Credentials.AssumeRole
42+import Network.Minio.Credentials.Types
43+import qualified UnliftIO.MVar as M
44+
45+data STSCredentialStore = STSCredentialStore
46+ { cachedCredentials :: M.MVar (CredentialValue, ExpiryTime),
47+ refreshAction :: Endpoint -> NC.Manager -> IO (CredentialValue, ExpiryTime)
48+ }
49+
50+initSTSCredential :: (STSCredentialProvider p) => p -> IO STSCredentialStore
51+initSTSCredential p = do
52+ let action = retrieveSTSCredentials p
53+ -- start with dummy credential, so that refresh happens for first request.
54+ now <- getCurrentTime
55+ mvar <- M.newMVar (CredentialValue mempty mempty mempty, coerce now)
56+ return $
57+ STSCredentialStore
58+ { cachedCredentials = mvar,
59+ refreshAction = action
60+ }
61+
62+getSTSCredential :: STSCredentialStore -> Endpoint -> NC.Manager -> IO (CredentialValue, Bool)
63+getSTSCredential store ep mgr = M.modifyMVar (cachedCredentials store) $ \cc@(v, expiry) -> do
64+ now <- getCurrentTime
65+ if diffUTCTime now (coerce expiry) > 0
66+ then do
67+ res <- refreshAction store ep mgr
68+ return (res, (fst res, True))
69+ else return (cc, (v, False))
70+
71+data Creds
72+ = CredsStatic CredentialValue
73+ | CredsSTS STSCredentialStore
74+
75+getCredential :: Creds -> Endpoint -> NC.Manager -> IO CredentialValue
76+getCredential (CredsStatic v) _ _ = return v
77+getCredential (CredsSTS s) ep mgr = fst <$> getSTSCredential s ep mgr
src/Network/Minio/Credentials/AssumeRole.hsnew file+266
@@ -0,0 +1,266 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.Credentials.AssumeRole where
18+
19+import qualified Data.ByteArray as BA
20+import qualified Data.ByteString.Lazy as LB
21+import qualified Data.Text as T
22+import qualified Data.Time as Time
23+import Data.Time.Units (Second)
24+import Lib.Prelude (UTCTime, throwIO)
25+import Network.HTTP.Client (RequestBody (RequestBodyBS))
26+import qualified Network.HTTP.Client as NC
27+import Network.HTTP.Types (hContentType, methodPost, renderSimpleQuery)
28+import Network.HTTP.Types.Header (hHost)
29+import Network.Minio.Credentials.Types
30+import Network.Minio.Data.Crypto (hashSHA256)
31+import Network.Minio.Errors (MErrV (..))
32+import Network.Minio.Sign.V4
33+import Network.Minio.Utils (getHostHeader, httpLbs)
34+import Network.Minio.XmlCommon
35+import Text.XML.Cursor hiding (bool)
36+
37+stsVersion :: ByteString
38+stsVersion = "2011-06-15"
39+
40+defaultDurationSeconds :: Second
41+defaultDurationSeconds = 3600
42+
43+-- | Assume Role API argument.
44+--
45+-- @since 1.7.0
46+data STSAssumeRole = STSAssumeRole
47+ { -- | Credentials to use in the AssumeRole STS API.
48+ sarCredentials :: CredentialValue,
49+ -- | Optional settings.
50+ sarOptions :: STSAssumeRoleOptions
51+ }
52+
53+-- | Options for STS Assume Role API.
54+data STSAssumeRoleOptions = STSAssumeRoleOptions
55+ { -- | STS endpoint to which the request will be made. For MinIO, this is the
56+ -- same as the server endpoint. For AWS, this has to be the Security Token
57+ -- Service endpoint. If using with 'setSTSCredential', this option can be
58+ -- left as 'Nothing' and the endpoint in 'ConnectInfo' will be used.
59+ saroEndpoint :: Maybe Text,
60+ -- | Desired validity for the generated credentials.
61+ saroDurationSeconds :: Maybe Second,
62+ -- | IAM policy to apply for the generated credentials.
63+ saroPolicyJSON :: Maybe ByteString,
64+ -- | Location is usually required for AWS.
65+ saroLocation :: Maybe Text,
66+ saroRoleARN :: Maybe Text,
67+ saroRoleSessionName :: Maybe Text
68+ }
69+
70+-- | Default STS Assume Role options - all options are Nothing, except for
71+-- duration which is set to 1 hour.
72+defaultSTSAssumeRoleOptions :: STSAssumeRoleOptions
73+defaultSTSAssumeRoleOptions =
74+ STSAssumeRoleOptions
75+ { saroEndpoint = Nothing,
76+ saroDurationSeconds = Just 3600,
77+ saroPolicyJSON = Nothing,
78+ saroLocation = Nothing,
79+ saroRoleARN = Nothing,
80+ saroRoleSessionName = Nothing
81+ }
82+
83+data AssumeRoleCredentials = AssumeRoleCredentials
84+ { arcCredentials :: CredentialValue,
85+ arcExpiration :: UTCTime
86+ }
87+ deriving stock (Show, Eq)
88+
89+data AssumeRoleResult = AssumeRoleResult
90+ { arrSourceIdentity :: Text,
91+ arrAssumedRoleArn :: Text,
92+ arrAssumedRoleId :: Text,
93+ arrRoleCredentials :: AssumeRoleCredentials
94+ }
95+ deriving stock (Show, Eq)
96+
97+-- | parseSTSAssumeRoleResult parses an XML response of the following form:
98+--
99+-- <AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
100+-- <AssumeRoleResult>
101+-- <SourceIdentity>Alice</SourceIdentity>
102+-- <AssumedRoleUser>
103+-- <Arn>arn:aws:sts::123456789012:assumed-role/demo/TestAR</Arn>
104+-- <AssumedRoleId>ARO123EXAMPLE123:TestAR</AssumedRoleId>
105+-- </AssumedRoleUser>
106+-- <Credentials>
107+-- <AccessKeyId>ASIAIOSFODNN7EXAMPLE</AccessKeyId>
108+-- <SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
109+-- <SessionToken>
110+-- AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
111+-- LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
112+-- QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
113+-- 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
114+-- +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
115+-- </SessionToken>
116+-- <Expiration>2019-11-09T13:34:41Z</Expiration>
117+-- </Credentials>
118+-- <PackedPolicySize>6</PackedPolicySize>
119+-- </AssumeRoleResult>
120+-- <ResponseMetadata>
121+-- <RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
122+-- </ResponseMetadata>
123+-- </AssumeRoleResponse>
124+parseSTSAssumeRoleResult :: (MonadIO m) => ByteString -> Text -> m AssumeRoleResult
125+parseSTSAssumeRoleResult xmldata namespace = do
126+ r <- parseRoot $ LB.fromStrict xmldata
127+ let s3Elem' = s3Elem namespace
128+ sourceIdentity =
129+ T.concat $
130+ r
131+ $/ s3Elem' "AssumeRoleResult"
132+ &/ s3Elem' "SourceIdentity"
133+ &/ content
134+ roleArn =
135+ T.concat $
136+ r
137+ $/ s3Elem' "AssumeRoleResult"
138+ &/ s3Elem' "AssumedRoleUser"
139+ &/ s3Elem' "Arn"
140+ &/ content
141+ roleId =
142+ T.concat $
143+ r
144+ $/ s3Elem' "AssumeRoleResult"
145+ &/ s3Elem' "AssumedRoleUser"
146+ &/ s3Elem' "AssumedRoleId"
147+ &/ content
148+
149+ convSB :: Text -> BA.ScrubbedBytes
150+ convSB = BA.convert . (encodeUtf8 :: Text -> ByteString)
151+
152+ credsInfo = do
153+ cr <-
154+ maybe (Left $ MErrVXmlParse "No Credentials Element found") Right $
155+ listToMaybe $
156+ r $/ s3Elem' "AssumeRoleResult" &/ s3Elem' "Credentials"
157+ let cur = fromNode $ node cr
158+ return
159+ ( CredentialValue
160+ { cvAccessKey =
161+ coerce $
162+ T.concat $
163+ cur $/ s3Elem' "AccessKeyId" &/ content,
164+ cvSecretKey =
165+ coerce $
166+ convSB $
167+ T.concat $
168+ cur
169+ $/ s3Elem' "SecretAccessKey"
170+ &/ content,
171+ cvSessionToken =
172+ Just $
173+ coerce $
174+ convSB $
175+ T.concat $
176+ cur
177+ $/ s3Elem' "SessionToken"
178+ &/ content
179+ },
180+ T.concat $ cur $/ s3Elem' "Expiration" &/ content
181+ )
182+ creds <- either throwIO pure credsInfo
183+ expiry <- parseS3XMLTime $ snd creds
184+ let roleCredentials =
185+ AssumeRoleCredentials
186+ { arcCredentials = fst creds,
187+ arcExpiration = expiry
188+ }
189+ return
190+ AssumeRoleResult
191+ { arrSourceIdentity = sourceIdentity,
192+ arrAssumedRoleArn = roleArn,
193+ arrAssumedRoleId = roleId,
194+ arrRoleCredentials = roleCredentials
195+ }
196+
197+instance STSCredentialProvider STSAssumeRole where
198+ getSTSEndpoint = saroEndpoint . sarOptions
199+ retrieveSTSCredentials sar (host', port', isSecure') mgr = do
200+ -- Assemble STS request
201+ let requiredParams =
202+ [ ("Action", "AssumeRole"),
203+ ("Version", stsVersion)
204+ ]
205+ opts = sarOptions sar
206+
207+ durSecs :: Int =
208+ fromIntegral $
209+ fromMaybe defaultDurationSeconds $
210+ saroDurationSeconds opts
211+ otherParams =
212+ [ ("RoleArn",) . encodeUtf8 <$> saroRoleARN opts,
213+ ("RoleSessionName",) . encodeUtf8 <$> saroRoleSessionName opts,
214+ Just ("DurationSeconds", show durSecs),
215+ ("Policy",) <$> saroPolicyJSON opts
216+ ]
217+ parameters = requiredParams ++ catMaybes otherParams
218+ (host, port, isSecure) =
219+ case getSTSEndpoint sar of
220+ Just ep ->
221+ let endPt = NC.parseRequest_ $ toString ep
222+ in (NC.host endPt, NC.port endPt, NC.secure endPt)
223+ Nothing -> (host', port', isSecure')
224+ reqBody = renderSimpleQuery False parameters
225+ req =
226+ NC.defaultRequest
227+ { NC.host = host,
228+ NC.port = port,
229+ NC.secure = isSecure,
230+ NC.method = methodPost,
231+ NC.requestHeaders =
232+ [ (hHost, getHostHeader (host, port)),
233+ (hContentType, "application/x-www-form-urlencoded")
234+ ],
235+ NC.requestBody = RequestBodyBS reqBody
236+ }
237+
238+ -- Sign the STS request.
239+ timeStamp <- liftIO Time.getCurrentTime
240+ let sp =
241+ SignParams
242+ { spAccessKey = coerce $ cvAccessKey $ sarCredentials sar,
243+ spSecretKey = coerce $ cvSecretKey $ sarCredentials sar,
244+ spSessionToken = coerce $ cvSessionToken $ sarCredentials sar,
245+ spService = ServiceSTS,
246+ spTimeStamp = timeStamp,
247+ spRegion = saroLocation opts,
248+ spExpirySecs = Nothing,
249+ spPayloadHash = Just $ hashSHA256 reqBody
250+ }
251+ signHeaders = signV4 sp req
252+ signedReq =
253+ req
254+ { NC.requestHeaders = NC.requestHeaders req ++ signHeaders
255+ }
256+
257+ -- Make the STS request
258+ resp <- httpLbs signedReq mgr
259+ result <-
260+ parseSTSAssumeRoleResult
261+ (toStrict $ NC.responseBody resp)
262+ "https://sts.amazonaws.com/doc/2011-06-15/"
263+ return
264+ ( arcCredentials $ arrRoleCredentials result,
265+ coerce $ arcExpiration $ arrRoleCredentials result
266+ )
src/Network/Minio/Credentials/Types.hsnew file+90
@@ -0,0 +1,90 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17+{-# LANGUAGE StrictData #-}
18+
19+module Network.Minio.Credentials.Types where
20+
21+import qualified Data.ByteArray as BA
22+import Lib.Prelude (UTCTime)
23+import qualified Network.HTTP.Client as NC
24+
25+-- | Access Key type.
26+newtype AccessKey = AccessKey {unAccessKey :: Text}
27+ deriving stock (Show)
28+ deriving newtype (Eq, IsString, Semigroup, Monoid)
29+
30+-- | Secret Key type - has a show instance that does not print the value.
31+newtype SecretKey = SecretKey {unSecretKey :: BA.ScrubbedBytes}
32+ deriving stock (Show)
33+ deriving newtype (Eq, IsString, Semigroup, Monoid)
34+
35+-- | Session Token type - has a show instance that does not print the value.
36+newtype SessionToken = SessionToken {unSessionToken :: BA.ScrubbedBytes}
37+ deriving stock (Show)
38+ deriving newtype (Eq, IsString, Semigroup, Monoid)
39+
40+-- | Object storage credential data type. It has support for the optional
41+-- [SessionToken](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html)
42+-- for using temporary credentials requested via STS.
43+--
44+-- The show instance for this type does not print the value of secrets for
45+-- security.
46+--
47+-- @since 1.7.0
48+data CredentialValue = CredentialValue
49+ { cvAccessKey :: AccessKey,
50+ cvSecretKey :: SecretKey,
51+ cvSessionToken :: Maybe SessionToken
52+ }
53+ deriving stock (Eq, Show)
54+
55+scrubbedToText :: BA.ScrubbedBytes -> Text
56+scrubbedToText =
57+ let b2t :: ByteString -> Text
58+ b2t = decodeUtf8
59+ s2b :: BA.ScrubbedBytes -> ByteString
60+ s2b = BA.convert
61+ in b2t . s2b
62+
63+-- | Convert a 'CredentialValue' to a text tuple. Use this to output the
64+-- credential to files or other programs.
65+credentialValueText :: CredentialValue -> (Text, Text, Maybe Text)
66+credentialValueText cv =
67+ ( coerce $ cvAccessKey cv,
68+ (scrubbedToText . coerce) $ cvSecretKey cv,
69+ scrubbedToText . coerce <$> cvSessionToken cv
70+ )
71+
72+-- | Endpoint represented by host, port and TLS enabled flag.
73+type Endpoint = (ByteString, Int, Bool)
74+
75+-- | Typeclass for STS credential providers.
76+--
77+-- @since 1.7.0
78+class STSCredentialProvider p where
79+ retrieveSTSCredentials ::
80+ p ->
81+ -- | STS Endpoint (host, port, isSecure)
82+ Endpoint ->
83+ NC.Manager ->
84+ IO (CredentialValue, ExpiryTime)
85+ getSTSEndpoint :: p -> Maybe Text
86+
87+-- | 'ExpiryTime' represents a time at which a credential expires.
88+newtype ExpiryTime = ExpiryTime {unExpiryTime :: UTCTime}
89+ deriving stock (Show)
90+ deriving newtype (Eq)
src/Network/Minio/Data.hs+730−557
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,47 +13,64 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
17−{-# LANGUAGE CPP #-}
16+{-# LANGUAGE CPP #-}
18−{-# LANGUAGE GADTs #-}
17+{-# LANGUAGE GADTs #-}
19{-# LANGUAGE GeneralizedNewtypeDeriving #-}
18{-# LANGUAGE GeneralizedNewtypeDeriving #-}
19+{-# LANGUAGE StrictData #-}
20−{-# LANGUAGE TypeFamilies #-}
20+{-# LANGUAGE TypeFamilies #-}
21+
21module Network.Minio.Data where
22module Network.Minio.Data where
22
23
23−import qualified Conduit as C
24+import qualified Conduit as C
24−import Control.Concurrent.MVar (MVar)
25−import qualified Control.Concurrent.MVar as M
25+import qualified Control.Concurrent.MVar as M
26−import Control.Monad.IO.Unlift (MonadUnliftIO, UnliftIO (..),
26+import Control.Monad.Trans.Except (throwE)
27− askUnliftIO, withUnliftIO)
27+import Control.Monad.Trans.Resource
28−import Control.Monad.Trans.Resource
28+ ( MonadResource,
29+ MonadThrow (..),
30+ MonadUnliftIO,
31+ ResourceT,
32+ runResourceT,
33+ )
29−import qualified Data.Aeson as A
34+import qualified Data.Aeson as A
30−import qualified Data.ByteArray as BA
35+import qualified Data.ByteArray as BA
31−import qualified Data.ByteString as B
36+import qualified Data.ByteString as B
32−import Data.CaseInsensitive (mk)
37+import qualified Data.ByteString.Lazy as LB
38+import Data.Default.Class (def)
33−import qualified Data.HashMap.Strict as H
39+import qualified Data.HashMap.Strict as H
34−import qualified Data.Ini as Ini
40+import qualified Data.Ini as Ini
35−import qualified Data.Map as Map
41+import qualified Data.List as List
36−import Data.String (IsString (..))
37−import qualified Data.Text as T
42+import qualified Data.Text as T
38−import qualified Data.Text.Encoding as TE
43+import qualified Data.Text.Encoding as TE
39−import Data.Time (defaultTimeLocale, formatTime)
44+import Data.Time (defaultTimeLocale, formatTime)
40−import GHC.Show (Show (show))
45+import Lib.Prelude (UTCTime, throwIO)
41−import qualified Network.Connection as Conn
46+import qualified Network.Connection as Conn
42−import Network.HTTP.Client (defaultManagerSettings)
47+import Network.HTTP.Client (defaultManagerSettings)
43−import qualified Network.HTTP.Client.TLS as TLS
48+import qualified Network.HTTP.Client.TLS as TLS
44−import qualified Network.HTTP.Conduit as NC
49+import qualified Network.HTTP.Conduit as NC
45−import Network.HTTP.Types (ByteRange, Header, Method, Query,
50+import Network.HTTP.Types
51+ ( ByteRange,
52+ Header,
53+ Method,
54+ Query,
46− hRange)
55+ hRange,
56+ )
47−import qualified Network.HTTP.Types as HT
57+import qualified Network.HTTP.Types as HT
58+import Network.Minio.Credentials
59+import Network.Minio.Data.Crypto
60+ ( encodeToBase64,
61+ hashMD5ToBase64,
62+ )
63+import Network.Minio.Data.Time (UrlExpiry)
48−import Network.Minio.Errors
64+import Network.Minio.Errors
65+ ( MErrV (MErrVInvalidEncryptionKeyLength, MErrVMissingCredentials),
66+ MinioErr (..),
67+ )
68+import Network.Minio.Utils
49−import System.Directory (doesFileExist, getHomeDirectory)
69+import System.Directory (doesFileExist, getHomeDirectory)
50−import qualified System.Environment as Env
70+import qualified System.Environment as Env
51−import System.FilePath.Posix (combine)
71+import System.FilePath.Posix (combine)
52−import Text.XML
53−import qualified UnliftIO as U
72+import qualified UnliftIO as U
54−
73+import qualified UnliftIO.MVar as UM
55−import Lib.Prelude
56−import Network.Minio.Data.Crypto
57
74
58-- | max obj size is 5TiB
75-- | max obj size is 5TiB
59maxObjectSize :: Int64
76maxObjectSize :: Int64
@@ -73,124 +90,157 @@ maxMultipartParts = 10000
73-- TODO: Add a type which provides typed constants for region. this
90-- TODO: Add a type which provides typed constants for region. this
74-- type should have a IsString instance to infer the appropriate
91-- type should have a IsString instance to infer the appropriate
75-- constant.
92-- constant.
93+
76-- | awsRegionMap - library constant
94-- | awsRegionMap - library constant
77−awsRegionMap :: Map.Map Text Text
95+awsRegionMap :: H.HashMap Text Text
78−awsRegionMap = Map.fromList [
96+awsRegionMap =
97+ H.fromList
79− ("us-east-1", "s3.amazonaws.com")
98+ [ ("us-east-1", "s3.us-east-1.amazonaws.com"),
80− , ("us-east-2", "s3-us-east-2.amazonaws.com")
99+ ("us-east-2", "s3.us-east-2.amazonaws.com"),
81− , ("us-west-1", "s3-us-west-1.amazonaws.com")
100+ ("us-west-1", "s3.us-west-1.amazonaws.com"),
82− , ("us-east-2", "s3-us-west-2.amazonaws.com")
101+ ("us-west-2", "s3.us-west-2.amazonaws.com"),
83− , ("ca-central-1", "s3-ca-central-1.amazonaws.com")
102+ ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
84− , ("ap-south-1", "s3-ap-south-1.amazonaws.com")
103+ ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
104+ ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
85− , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
105+ ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
86− , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
106+ ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
107+ ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
87− , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
108+ ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
88− , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
109+ ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
110+ ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
89− , ("eu-west-1", "s3-eu-west-1.amazonaws.com")
111+ ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
90− , ("eu-west-2", "s3-eu-west-2.amazonaws.com")
112+ ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
113+ ("eu-west-3", "s3.eu-west-3.amazonaws.com"),
91− , ("eu-central-1", "s3-eu-central-1.amazonaws.com")
114+ ("eu-central-1", "s3.eu-central-1.amazonaws.com"),
115+ ("eu-central-2", "s3.eu-central-2.amazonaws.com"),
116+ ("eu-south-1", "s3.eu-south-1.amazonaws.com"),
92− , ("sa-east-1", "s3-sa-east-1.amazonaws.com")
117+ ("eu-south-2", "s3.eu-south-2.amazonaws.com"),
118+ ("af-south-1", "s3.af-south-1.amazonaws.com"),
119+ ("ap-east-1", "s3.ap-east-1.amazonaws.com"),
120+ ("cn-north-1", "s3.cn-north-1.amazonaws.com.cn"),
121+ ("cn-northwest-1", "s3.cn-northwest-1.amazonaws.com.cn"),
122+ ("eu-north-1", "s3.eu-north-1.amazonaws.com"),
123+ ("me-south-1", "s3.me-south-1.amazonaws.com"),
124+ ("me-central-1", "s3.me-central-1.amazonaws.com"),
125+ ("us-gov-east-1", "s3.us-gov-east-1.amazonaws.com"),
126+ ("us-gov-west-1", "s3.us-gov-west-1.amazonaws.com"),
127+ ("sa-east-1", "s3.sa-east-1.amazonaws.com")
93− ]
128+ ]
94
129
95-- | Connection Info data type. To create a 'ConnectInfo' value,
130-- | Connection Info data type. To create a 'ConnectInfo' value,
96-- enable the @OverloadedStrings@ language extension and use the
131-- enable the @OverloadedStrings@ language extension and use the
97-- `IsString` instance to provide a URL, for example:
132-- `IsString` instance to provide a URL, for example:
98--
133--
99−-- > let c :: ConnectInfo = "https://play.min.io:9000"
134+-- > let c :: ConnectInfo = "https://play.min.io"
100−data ConnectInfo =
135+data ConnectInfo = ConnectInfo
101− ConnectInfo { connectHost :: Text
136+ { connectHost :: Text,
102− , connectPort :: Int
137+ connectPort :: Int,
103− , connectAccessKey :: Text
138+ connectCreds :: Creds,
104− , connectSecretKey :: Text
139+ connectIsSecure :: Bool,
105− , connectIsSecure :: Bool
106− , connectRegion :: Region
140+ connectRegion :: Region,
107− , connectAutoDiscoverRegion :: Bool
141+ connectAutoDiscoverRegion :: Bool,
108− , connectDisableTLSCertValidation :: Bool
142+ connectDisableTLSCertValidation :: Bool
109− } deriving (Eq, Show)
143+ }
144+
145+getEndpoint :: ConnectInfo -> Endpoint
146+getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
110
147
111instance IsString ConnectInfo where
148instance IsString ConnectInfo where
112− fromString str =
149+ fromString str =
113− let req = NC.parseRequest_ str
150+ let req = NC.parseRequest_ str
114− in ConnectInfo
151+ in ConnectInfo
115− { connectHost = TE.decodeUtf8 $ NC.host req
152+ { connectHost = TE.decodeUtf8 $ NC.host req,
116− , connectPort = NC.port req
153+ connectPort = NC.port req,
117− , connectAccessKey = ""
154+ connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
118− , connectSecretKey = ""
119− , connectIsSecure = NC.secure req
155+ connectIsSecure = NC.secure req,
120− , connectRegion = ""
156+ connectRegion = "",
121− , connectAutoDiscoverRegion = True
157+ connectAutoDiscoverRegion = True,
122− , connectDisableTLSCertValidation = False
158+ connectDisableTLSCertValidation = False
123− }
159+ }
124−
160+
125−-- | Contains access key and secret key to access object storage.
126−data Credentials = Credentials { cAccessKey :: Text
127− , cSecretKey :: Text
128− } deriving (Eq, Show)
129−
130−-- | A Provider is an action that may return Credentials
161+-- | A 'CredentialLoader' is an action that may return a 'CredentialValue'.
131−type Provider = IO (Maybe Credentials)
162+-- Loaders may be chained together using 'findFirst'.
132−
163+--
164+-- @since 1.7.0
165+type CredentialLoader = IO (Maybe CredentialValue)
166+
133−-- | Combines the given list of providers, by calling each one in
167+-- | Combines the given list of loaders, by calling each one in
134−-- order until Credentials are found.
168+-- order until a 'CredentialValue' is returned.
135−findFirst :: [Provider] -> Provider
169+findFirst :: [CredentialLoader] -> IO (Maybe CredentialValue)
136findFirst [] = return Nothing
170findFirst [] = return Nothing
137−findFirst (f:fs) = do c <- f
171+findFirst (f : fs) = do
172+ c <- f
138− maybe (findFirst fs) (return . Just) c
173+ maybe (findFirst fs) (return . Just) c
139
174
175+-- | This action returns a 'CredentialValue' populated from
140−-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
176+-- @~\/.aws\/credentials@
141−fromAWSConfigFile :: Provider
177+fromAWSConfigFile :: CredentialLoader
142fromAWSConfigFile = do
178fromAWSConfigFile = do
143− credsE <- runExceptT $ do
179+ credsE <- runExceptT $ do
144− homeDir <- lift $ getHomeDirectory
180+ homeDir <- lift getHomeDirectory
145− let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
181+ let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
146− fileExists <- lift $ doesFileExist awsCredsFile
182+ fileExists <- lift $ doesFileExist awsCredsFile
147− bool (throwE "FileNotFound") (return ()) fileExists
183+ bool (throwE "FileNotFound") (return ()) fileExists
148− ini <- ExceptT $ Ini.readIniFile awsCredsFile
184+ ini <- ExceptT $ Ini.readIniFile awsCredsFile
185+ akey <-
149− akey <- ExceptT $ return
186+ ExceptT $
187+ return $
150− $ Ini.lookupValue "default" "aws_access_key_id" ini
188+ Ini.lookupValue "default" "aws_access_key_id" ini
189+ skey <-
151− skey <- ExceptT $ return
190+ ExceptT $
191+ return $
152− $ Ini.lookupValue "default" "aws_secret_access_key" ini
192+ Ini.lookupValue "default" "aws_secret_access_key" ini
153− return $ Credentials akey skey
193+ return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
154− return $ hush credsE
194+ return $ either (const Nothing) Just credsE
155−
195+
156−-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
196+-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
157−-- @AWS_SECRET_ACCESS_KEY@ environment variables.
197+-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
158−fromAWSEnv :: Provider
198+fromAWSEnv :: CredentialLoader
159fromAWSEnv = runMaybeT $ do
199fromAWSEnv = runMaybeT $ do
160− akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
200+ akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
161− skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
201+ skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
162− return $ Credentials (T.pack akey) (T.pack skey)
202+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
163
203
164−-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
204+-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
165−-- @MINIO_SECRET_KEY@ environment variables.
205+-- and @MINIO_SECRET_KEY@ environment variables.
166−fromMinioEnv :: Provider
206+fromMinioEnv :: CredentialLoader
167fromMinioEnv = runMaybeT $ do
207fromMinioEnv = runMaybeT $ do
168− akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
208+ akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
169− skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
209+ skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
170− return $ Credentials (T.pack akey) (T.pack skey)
210+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
171−
211+
172−-- | setCredsFrom retrieves access credentials from the first
212+-- | setCredsFrom retrieves access credentials from the first action in the
173−-- `Provider` form the given list that succeeds and sets it in the
213+-- given list that succeeds and sets it in the 'ConnectInfo'.
174−-- `ConnectInfo`.
175−setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo
214+setCredsFrom :: [CredentialLoader] -> ConnectInfo -> IO ConnectInfo
176−setCredsFrom ps ci = do pMay <- findFirst ps
215+setCredsFrom ps ci = do
216+ pMay <- findFirst ps
177− maybe
217+ maybe
178− (throwIO MErrVMissingCredentials)
218+ (throwIO MErrVMissingCredentials)
179− (return . (flip setCreds ci))
219+ (return . (`setCreds` ci))
180− pMay
220+ pMay
181−
221+
182−-- | setCreds sets the given `Credentials` in the `ConnectInfo`.
222+-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
183−setCreds :: Credentials -> ConnectInfo -> ConnectInfo
223+setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
184−setCreds (Credentials accessKey secretKey) connInfo =
224+setCreds cv connInfo =
185− connInfo { connectAccessKey = accessKey
225+ connInfo
186− , connectSecretKey = secretKey
226+ { connectCreds = CredsStatic cv
187− }
227+ }
228+
229+-- | 'setSTSCredential' configures `ConnectInfo` to retrieve temporary
230+-- credentials via the STS API on demand. It is automatically refreshed on
231+-- expiry.
232+setSTSCredential :: (STSCredentialProvider p) => p -> ConnectInfo -> IO ConnectInfo
233+setSTSCredential p ci = do
234+ store <- initSTSCredential p
235+ return ci {connectCreds = CredsSTS store}
188
236
189-- | Set the S3 region parameter in the `ConnectInfo`
237-- | Set the S3 region parameter in the `ConnectInfo`
190setRegion :: Region -> ConnectInfo -> ConnectInfo
238setRegion :: Region -> ConnectInfo -> ConnectInfo
191−setRegion r connInfo = connInfo { connectRegion = r
239+setRegion r connInfo =
240+ connInfo
241+ { connectRegion = r,
192− , connectAutoDiscoverRegion = False
242+ connectAutoDiscoverRegion = False
193− }
243+ }
194
244
195-- | Check if the connection to object storage server is secure
245-- | Check if the connection to object storage server is secure
196-- (i.e. uses TLS)
246-- (i.e. uses TLS)
@@ -202,39 +252,37 @@ isConnectInfoSecure = connectIsSecure
202-- certificates. Note that this option has no effect, if you provide
252-- certificates. Note that this option has no effect, if you provide
203-- your own Manager in `mkMinioConn`.
253-- your own Manager in `mkMinioConn`.
204disableTLSCertValidation :: ConnectInfo -> ConnectInfo
254disableTLSCertValidation :: ConnectInfo -> ConnectInfo
205−disableTLSCertValidation c = c { connectDisableTLSCertValidation = True }
255+disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
206
256
207getHostAddr :: ConnectInfo -> ByteString
257getHostAddr :: ConnectInfo -> ByteString
208−getHostAddr ci = if | port == 80 || port == 443 -> toS host
258+getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
209− | otherwise -> toS $
210− T.concat [ host, ":" , Lib.Prelude.show port]
211− where
212− port = connectPort ci
213− host = connectHost ci
214−
215
259
216−-- | Default GCS ConnectInfo. Works only for "Simple Migration"
260+-- | Default Google Compute Storage ConnectInfo. Works only for
217−-- use-case with interoperability mode enabled on GCP console. For
261+-- "Simple Migration" use-case with interoperability mode enabled on
262+-- GCP console. For more information -
218−-- more information - https://cloud.google.com/storage/docs/migrating
263+-- https://cloud.google.com/storage/docs/migrating
264+--
219-- Credentials should be supplied before use.
265-- Credentials should be supplied before use.
220gcsCI :: ConnectInfo
266gcsCI :: ConnectInfo
267+gcsCI =
221−gcsCI = setRegion "us"
268+ setRegion
269+ "us"
222− "https://storage.googleapis.com"
270+ "https://storage.googleapis.com"
223−
224
271
225−-- | Default AWS ConnectInfo. Connects to "us-east-1". Credentials
272+-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
226-- should be supplied before use.
273-- should be supplied before use.
227awsCI :: ConnectInfo
274awsCI :: ConnectInfo
228awsCI = "https://s3.amazonaws.com"
275awsCI = "https://s3.amazonaws.com"
229
276
230−
231−-- | <https://play.min.io:9000 MinIO Play Server>
277+-- | <https://play.min.io MinIO Play Server>
232-- ConnectInfo. Credentials are already filled in.
278-- ConnectInfo. Credentials are already filled in.
233minioPlayCI :: ConnectInfo
279minioPlayCI :: ConnectInfo
280+minioPlayCI =
234−minioPlayCI = let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
281+ let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
235− in setCreds playCreds
282+ in setCreds playCreds $
236− $ setRegion "us-east-1"
283+ setRegion
284+ "us-east-1"
237− "https://play.min.io:9000"
285+ "https://play.min.io"
238
286
239-- |
287-- |
240-- Represents a bucket in the object store
288-- Represents a bucket in the object store
@@ -244,9 +292,7 @@ type Bucket = Text
244-- Represents an object name
292-- Represents an object name
245type Object = Text
293type Object = Text
246
294
247−-- |
248−-- Represents a region
295+-- | Represents a region
249−-- TODO: This could be a Sum Type with all defined regions for AWS.
250type Region = Text
296type Region = Text
251
297
252-- | A type alias to represent an Entity-Tag returned by S3-compatible APIs.
298-- | A type alias to represent an Entity-Tag returned by S3-compatible APIs.
@@ -255,127 +301,147 @@ type ETag = Text
255-- | Data type to represent an object encryption key. Create one using
301-- | Data type to represent an object encryption key. Create one using
256-- the `mkSSECKey` function.
302-- the `mkSSECKey` function.
257newtype SSECKey = SSECKey BA.ScrubbedBytes
303newtype SSECKey = SSECKey BA.ScrubbedBytes
258− deriving (Eq, Show)
304+ deriving stock (Eq, Show)
259
305
260-- | Validates that the given ByteString is 32 bytes long and creates
306-- | Validates that the given ByteString is 32 bytes long and creates
261-- an encryption key.
307-- an encryption key.
262−mkSSECKey :: MonadThrow m => ByteString -> m SSECKey
308+mkSSECKey :: (MonadThrow m) => ByteString -> m SSECKey
263−mkSSECKey keyBytes | B.length keyBytes /= 32 =
309+mkSSECKey keyBytes
310+ | B.length keyBytes /= 32 =
264− throwM MErrVInvalidEncryptionKeyLength
311+ throwM MErrVInvalidEncryptionKeyLength
265− | otherwise =
312+ | otherwise =
266− return $ SSECKey $ BA.convert keyBytes
313+ return $ SSECKey $ BA.convert keyBytes
267
314
268-- | Data type to represent Server-Side-Encryption settings
315-- | Data type to represent Server-Side-Encryption settings
269data SSE where
316data SSE where
270− -- | Specifies SSE S3 encryption - server manages encryption keys
317+ -- | Specifies SSE S3 encryption - server manages encryption keys
271− SSE :: SSE
318+ SSE :: SSE
272− -- | Specifies that KMS service should be used. The first argument
319+ -- | Specifies that KMS service should be used. The first argument
273− -- to the constructor is the Key Id to be used by the server (if
320+ -- to the constructor is the Key Id to be used by the server (if
274− -- not specified, the default KMS key id is used). The second
321+ -- not specified, the default KMS key id is used). The second
275− -- argument is the optional KMS context that must have a
322+ -- argument is the optional KMS context that must have a
276− -- `A.ToJSON` instance - please refer to the AWS S3 documentation
323+ -- `A.ToJSON` instance - please refer to the AWS S3 documentation
277− -- for detailed information.
324+ -- for detailed information.
278− SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
325+ SSEKMS :: (A.ToJSON a) => Maybe ByteString -> Maybe a -> SSE
279− -- | Specifies server-side encryption with customer provided
326+ -- | Specifies server-side encryption with customer provided
280− -- key. The argument is the encryption key to be used.
327+ -- key. The argument is the encryption key to be used.
281− SSEC :: SSECKey -> SSE
328+ SSEC :: SSECKey -> SSE
282
329
283toPutObjectHeaders :: SSE -> [HT.Header]
330toPutObjectHeaders :: SSE -> [HT.Header]
284toPutObjectHeaders sseArg =
331toPutObjectHeaders sseArg =
285− let sseHeader = "x-amz-server-side-encryption"
332+ let sseHeader = "x-amz-server-side-encryption"
286− sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
333+ sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
287− sseKmsContextHeader = sseHeader <> "-context"
334+ sseKmsContextHeader = sseHeader <> "-context"
288− ssecAlgo = sseHeader <> "-customer-algorithm"
335+ ssecAlgo = sseHeader <> "-customer-algorithm"
289− ssecKey = sseHeader <> "-customer-key"
336+ ssecKey = sseHeader <> "-customer-key"
290− ssecKeyMD5 = ssecKey <> "-MD5"
337+ ssecKeyMD5 = ssecKey <> "-MD5"
291−
292− in case sseArg of
338+ in case sseArg of
293− SSE -> [(sseHeader, "AES256")]
339+ SSE -> [(sseHeader, "AES256")]
294−
295− SSEKMS keyIdMay ctxMay ->
340+ SSEKMS keyIdMay ctxMay ->
296− [(sseHeader, "aws:kms")] ++
341+ [(sseHeader, "aws:kms")]
297− maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay ++
342+ ++ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay
298− maybe [] (\k -> [(sseKmsContextHeader, toS $ A.encode k)]) ctxMay
343+ ++ maybe [] (\k -> [(sseKmsContextHeader, LB.toStrict $ A.encode k)]) ctxMay
299−
300− SSEC (SSECKey sb) ->
344+ SSEC (SSECKey sb) ->
301− [(ssecAlgo, "AES256"),
345+ [ (ssecAlgo, "AES256"),
302− (ssecKey, encodeToBase64 sb),
346+ (ssecKey, encodeToBase64 sb),
303− (ssecKeyMD5, hashMD5ToBase64 sb)]
347+ (ssecKeyMD5, hashMD5ToBase64 sb)
348+ ]
304
349
305-- | Data type for options in PutObject call. Start with the empty
350-- | Data type for options in PutObject call. Start with the empty
306-- `defaultPutObjectOptions` and use various the various poo*
351-- `defaultPutObjectOptions` and use various the various poo*
307-- accessors.
352-- accessors.
308−data PutObjectOptions = PutObjectOptions {
353+data PutObjectOptions = PutObjectOptions
309− -- | Set a standard MIME type describing the format of the object.
354+ { -- | Set a standard MIME type describing the format of the object.
310− pooContentType :: Maybe Text
355+ pooContentType :: Maybe Text,
311− -- | Set what content encodings have been applied to the object and thus
356+ -- | Set what content encodings have been applied to the object and thus
312− -- what decoding mechanisms must be applied to obtain the media-type
357+ -- what decoding mechanisms must be applied to obtain the media-type
313− -- referenced by the Content-Type header field.
358+ -- referenced by the Content-Type header field.
314− , pooContentEncoding :: Maybe Text
359+ pooContentEncoding :: Maybe Text,
315− -- | Set presentational information for the object.
360+ -- | Set presentational information for the object.
316− , pooContentDisposition :: Maybe Text
361+ pooContentDisposition :: Maybe Text,
317− -- | Set to specify caching behavior for the object along the
362+ -- | Set to specify caching behavior for the object along the
318− -- request/reply chain.
363+ -- request/reply chain.
319− , pooCacheControl :: Maybe Text
364+ pooCacheControl :: Maybe Text,
320− -- | Set to describe the language(s) intended for the audience.
365+ -- | Set to describe the language(s) intended for the audience.
321− , pooContentLanguage :: Maybe Text
366+ pooContentLanguage :: Maybe Text,
322− -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
367+ -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
323− -- performance needs, storage class is @STANDARD@ by default (i.e
368+ -- performance needs, storage class is @STANDARD@ by default (i.e
324− -- when Nothing is passed).
369+ -- when Nothing is passed).
325− , pooStorageClass :: Maybe Text
370+ pooStorageClass :: Maybe Text,
326− -- | Set user defined metadata to store with the object.
371+ -- | Set user defined metadata to store with the object.
327− , pooUserMetadata :: [(Text, Text)]
372+ pooUserMetadata :: [(Text, Text)],
328− -- | Set number of worker threads used to upload an object.
373+ -- | Set number of worker threads used to upload an object.
329− , pooNumThreads :: Maybe Word
374+ pooNumThreads :: Maybe Word,
330− -- | Set object encryption parameters for the request.
375+ -- | Set object encryption parameters for the request.
331− , pooSSE :: Maybe SSE
376+ pooSSE :: Maybe SSE
332 }
377 }
333
378
334-- | Provide default `PutObjectOptions`.
379-- | Provide default `PutObjectOptions`.
335defaultPutObjectOptions :: PutObjectOptions
380defaultPutObjectOptions :: PutObjectOptions
336defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
381defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
337
382
338−addXAmzMetaPrefix :: Text -> Text
339−addXAmzMetaPrefix s = do
340− if (T.isPrefixOf "x-amz-meta-" s)
341− then s
342− else T.concat ["x-amz-meta-", s]
343−
344−mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
345−mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix $ T.toLower x, encodeUtf8 y))
346−
347pooToHeaders :: PutObjectOptions -> [HT.Header]
383pooToHeaders :: PutObjectOptions -> [HT.Header]
348−pooToHeaders poo = userMetadata
384+pooToHeaders poo =
385+ userMetadata
349− ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
386+ ++ mapMaybe tupToMaybe (zip names values)
350− ++ maybe [] toPutObjectHeaders (pooSSE poo)
387+ ++ maybe [] toPutObjectHeaders (pooSSE poo)
351 where
388 where
352− tupToMaybe (k, Just v) = Just (k, v)
389+ tupToMaybe (k, Just v) = Just (k, v)
353 tupToMaybe (_, Nothing) = Nothing
390 tupToMaybe (_, Nothing) = Nothing
354−
355 userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
391 userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
356−
392+ names =
357− names = ["content-type",
393+ [ "content-type",
358− "content-encoding",
394+ "content-encoding",
359− "content-disposition",
395+ "content-disposition",
360− "content-language",
396+ "content-language",
361− "cache-control",
397+ "cache-control",
362− "x-amz-storage-class"]
398+ "x-amz-storage-class"
399+ ]
400+ values =
401+ map
363− values = map (fmap encodeUtf8 . (poo &))
402+ (fmap encodeUtf8 . (poo &))
403+ [ pooContentType,
364− [pooContentType, pooContentEncoding,
404+ pooContentEncoding,
365− pooContentDisposition, pooContentLanguage,
405+ pooContentDisposition,
406+ pooContentLanguage,
366− pooCacheControl, pooStorageClass]
407+ pooCacheControl,
367−
408+ pooStorageClass
409+ ]
368
410
369-- |
411-- |
370-- BucketInfo returned for list buckets call
412-- BucketInfo returned for list buckets call
371−data BucketInfo = BucketInfo {
413+data BucketInfo = BucketInfo
372− biName :: Bucket
414+ { biName :: Bucket,
373− , biCreationDate :: UTCTime
415+ biCreationDate :: UTCTime
416+ }
374− } deriving (Show, Eq)
417+ deriving stock (Show, Eq)
375
418
376-- | A type alias to represent a part-number for multipart upload
419-- | A type alias to represent a part-number for multipart upload
377type PartNumber = Int16
420type PartNumber = Int16
378
421
422+-- | Select part sizes - the logic is that the minimum part-size will
423+-- be 64MiB.
424+selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
425+selectPartSizes size =
426+ uncurry (List.zip3 [1 ..]) $
427+ List.unzip $
428+ loop 0 size
429+ where
430+ ceil :: Double -> Int64
431+ ceil = ceiling
432+ partSize =
433+ max
434+ minPartSize
435+ ( ceil $
436+ fromIntegral size
437+ / fromIntegral maxMultipartParts
438+ )
439+ m = partSize
440+ loop st sz
441+ | st > sz = []
442+ | st + m >= sz = [(st, sz - st)]
443+ | otherwise = (st, m) : loop (st + m) sz
444+
379-- | A type alias to represent an upload-id for multipart upload
445-- | A type alias to represent an upload-id for multipart upload
380type UploadId = Text
446type UploadId = Text
381
447
@@ -384,88 +450,116 @@ type PartTuple = (PartNumber, ETag)
384
450
385-- | Represents result from a listing of object parts of an ongoing
451-- | Represents result from a listing of object parts of an ongoing
386-- multipart upload.
452-- multipart upload.
387−data ListPartsResult = ListPartsResult {
453+data ListPartsResult = ListPartsResult
388− lprHasMore :: Bool
454+ { lprHasMore :: Bool,
389− , lprNextPart :: Maybe Int
455+ lprNextPart :: Maybe Int,
390− , lprParts :: [ObjectPartInfo]
456+ lprParts :: [ObjectPartInfo]
457+ }
391− } deriving (Show, Eq)
458+ deriving stock (Show, Eq)
392
459
393-- | Represents information about an object part in an ongoing
460-- | Represents information about an object part in an ongoing
394-- multipart upload.
461-- multipart upload.
395−data ObjectPartInfo = ObjectPartInfo {
462+data ObjectPartInfo = ObjectPartInfo
396− opiNumber :: PartNumber
463+ { opiNumber :: PartNumber,
397− , opiETag :: ETag
464+ opiETag :: ETag,
398− , opiSize :: Int64
465+ opiSize :: Int64,
399− , opiModTime :: UTCTime
466+ opiModTime :: UTCTime
467+ }
400− } deriving (Show, Eq)
468+ deriving stock (Show, Eq)
401
469
402-- | Represents result from a listing of incomplete uploads to a
470-- | Represents result from a listing of incomplete uploads to a
403-- bucket.
471-- bucket.
404−data ListUploadsResult = ListUploadsResult {
472+data ListUploadsResult = ListUploadsResult
405− lurHasMore :: Bool
473+ { lurHasMore :: Bool,
406− , lurNextKey :: Maybe Text
474+ lurNextKey :: Maybe Text,
407− , lurNextUpload :: Maybe Text
475+ lurNextUpload :: Maybe Text,
408− , lurUploads :: [(Object, UploadId, UTCTime)]
476+ lurUploads :: [(Object, UploadId, UTCTime)],
409− , lurCPrefixes :: [Text]
477+ lurCPrefixes :: [Text]
478+ }
410− } deriving (Show, Eq)
479+ deriving stock (Show, Eq)
411
480
412-- | Represents information about a multipart upload.
481-- | Represents information about a multipart upload.
413−data UploadInfo = UploadInfo {
482+data UploadInfo = UploadInfo
414− uiKey :: Object
483+ { uiKey :: Object,
415− , uiUploadId :: UploadId
484+ uiUploadId :: UploadId,
416− , uiInitTime :: UTCTime
485+ uiInitTime :: UTCTime,
417− , uiSize :: Int64
486+ uiSize :: Int64
487+ }
418− } deriving (Show, Eq)
488+ deriving stock (Show, Eq)
419
489
420-- | Represents result from a listing of objects in a bucket.
490-- | Represents result from a listing of objects in a bucket.
421−data ListObjectsResult = ListObjectsResult {
491+data ListObjectsResult = ListObjectsResult
422− lorHasMore :: Bool
492+ { lorHasMore :: Bool,
423− , lorNextToken :: Maybe Text
493+ lorNextToken :: Maybe Text,
424− , lorObjects :: [ObjectInfo]
494+ lorObjects :: [ObjectInfo],
425− , lorCPrefixes :: [Text]
495+ lorCPrefixes :: [Text]
496+ }
426− } deriving (Show, Eq)
497+ deriving stock (Show, Eq)
427
498
428-- | Represents result from a listing of objects version 1 in a bucket.
499-- | Represents result from a listing of objects version 1 in a bucket.
429−data ListObjectsV1Result = ListObjectsV1Result {
500+data ListObjectsV1Result = ListObjectsV1Result
430− lorHasMore' :: Bool
501+ { lorHasMore' :: Bool,
431− , lorNextMarker :: Maybe Text
502+ lorNextMarker :: Maybe Text,
432− , lorObjects' :: [ObjectInfo]
503+ lorObjects' :: [ObjectInfo],
433− , lorCPrefixes' :: [Text]
504+ lorCPrefixes' :: [Text]
505+ }
434− } deriving (Show, Eq)
506+ deriving stock (Show, Eq)
435
507
436-- | Represents information about an object.
508-- | Represents information about an object.
437data ObjectInfo = ObjectInfo
509data ObjectInfo = ObjectInfo
510+ { -- | Object key
438− { oiObject :: Object -- ^ Object key
511+ oiObject :: Object,
512+ -- | Modification time of the object
439− , oiModTime :: UTCTime -- ^ Mdification time of the object
513+ oiModTime :: UTCTime,
514+ -- | ETag of the object
440− , oiETag :: ETag -- ^ ETag of the object
515+ oiETag :: ETag,
441− , oiSize :: Int64 -- ^ Size of the object in bytes
516+ -- | Size of the object in bytes
517+ oiSize :: Int64,
518+ -- | A map of user-metadata
519+ -- pairs stored with an
520+ -- object (keys will not
521+ -- have the @X-Amz-Meta-@
522+ -- prefix).
523+ oiUserMetadata :: H.HashMap Text Text,
442− , oiMetadata :: Map.Map Text Text -- ^ A map of the metadata
524+ -- | A map of metadata
443− -- key-value pairs
525+ -- key-value pairs (not
526+ -- including the
527+ -- user-metadata pairs)
528+ oiMetadata :: H.HashMap Text Text
529+ }
444− } deriving (Show, Eq)
530+ deriving stock (Show, Eq)
445
531
446-- | Represents source object in server-side copy object
532-- | Represents source object in server-side copy object
447data SourceInfo = SourceInfo
533data SourceInfo = SourceInfo
448− { srcBucket :: Text -- ^ Bucket containing the source object
534+ { -- | Bucket containing the source object
535+ srcBucket :: Text,
449− , srcObject :: Text -- ^ Source object key
536+ -- | Source object key
537+ srcObject :: Text,
450− , srcRange :: Maybe (Int64, Int64) -- ^ Source object
538+ -- | Source object
451− -- byte-range
539+ -- byte-range
452− -- (inclusive)
540+ -- (inclusive)
541+ srcRange :: Maybe (Int64, Int64),
453− , srcIfMatch :: Maybe Text -- ^ ETag condition on source -
542+ -- | ETag condition on source -
454− -- object is copied only if the
543+ -- object is copied only if the
455− -- source object's ETag matches
544+ -- source object's ETag matches
456− -- this value.
545+ -- this value.
546+ srcIfMatch :: Maybe Text,
547+ -- | ETag not match condition
548+ -- on source - object is copied
549+ -- if ETag does not match this
550+ -- value.
457− , srcIfNoneMatch :: Maybe Text -- ^ ETag not match condition
551+ srcIfNoneMatch :: Maybe Text,
458− -- on source - object is copied
552+ -- | Copy source object only
459− -- if ETag does not match this
553+ -- if the source has been
460− -- value.
554+ -- modified since this time.
461− , srcIfModifiedSince :: Maybe UTCTime -- ^ Copy source object only
555+ srcIfModifiedSince :: Maybe UTCTime,
462− -- if the source has been
463− -- modified since this time.
464− , srcIfUnmodifiedSince :: Maybe UTCTime -- ^ Copy source object only
556+ -- | Copy source object only
465− -- if the source has been
557+ -- if the source has been
466− -- un-modified since this
558+ -- un-modified since this
467− -- given time.
559+ -- given time.
560+ srcIfUnmodifiedSince :: Maybe UTCTime
561+ }
468− } deriving (Show, Eq)
562+ deriving stock (Show, Eq)
469
563
470-- | Provide a default for `SourceInfo`
564-- | Provide a default for `SourceInfo`
471defaultSourceInfo :: SourceInfo
565defaultSourceInfo :: SourceInfo
@@ -473,9 +567,12 @@ defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing
473
567
474-- | Represents destination object in server-side copy object
568-- | Represents destination object in server-side copy object
475data DestinationInfo = DestinationInfo
569data DestinationInfo = DestinationInfo
476− { dstBucket :: Text -- ^ Destination bucket
570+ { -- | Destination bucket
571+ dstBucket :: Text,
477− , dstObject :: Text -- ^ Destination object key
572+ -- | Destination object key
573+ dstObject :: Text
574+ }
478− } deriving (Show, Eq)
575+ deriving stock (Show, Eq)
479
576
480-- | Provide a default for `DestinationInfo`
577-- | Provide a default for `DestinationInfo`
481defaultDestinationInfo :: DestinationInfo
578defaultDestinationInfo :: DestinationInfo
@@ -484,88 +581,106 @@ defaultDestinationInfo = DestinationInfo "" ""
484-- | Data type for options when getting an object from the
581-- | Data type for options when getting an object from the
485-- service. Start with the empty `defaultGetObjectOptions` and modify
582-- service. Start with the empty `defaultGetObjectOptions` and modify
486-- it using the goo* functions.
583-- it using the goo* functions.
487−data GetObjectOptions = GetObjectOptions {
584+data GetObjectOptions = GetObjectOptions
488− -- | Set object's data of given offset begin and end,
585+ { -- | Set object's data of given offset begin and end,
489 -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
586 -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
490− gooRange :: Maybe ByteRange
587+ gooRange :: Maybe ByteRange,
491 -- | Set matching ETag condition, GetObject which matches the following
588 -- | Set matching ETag condition, GetObject which matches the following
492 -- ETag.
589 -- ETag.
493− , gooIfMatch :: Maybe ETag
590+ gooIfMatch :: Maybe ETag,
494 -- | Set matching ETag none condition, GetObject which does not match
591 -- | Set matching ETag none condition, GetObject which does not match
495 -- the following ETag.
592 -- the following ETag.
496− , gooIfNoneMatch :: Maybe ETag
593+ gooIfNoneMatch :: Maybe ETag,
497 -- | Set object unmodified condition, GetObject unmodified since given time.
594 -- | Set object unmodified condition, GetObject unmodified since given time.
498− , gooIfUnmodifiedSince :: Maybe UTCTime
595+ gooIfUnmodifiedSince :: Maybe UTCTime,
499 -- | Set object modified condition, GetObject modified since given time.
596 -- | Set object modified condition, GetObject modified since given time.
500− , gooIfModifiedSince :: Maybe UTCTime
597+ gooIfModifiedSince :: Maybe UTCTime,
501 -- | Specify SSE-C key
598 -- | Specify SSE-C key
502− , gooSSECKey :: Maybe SSECKey
599+ gooSSECKey :: Maybe SSECKey
503 }
600 }
504
601
505-- | Provide default `GetObjectOptions`.
602-- | Provide default `GetObjectOptions`.
506defaultGetObjectOptions :: GetObjectOptions
603defaultGetObjectOptions :: GetObjectOptions
507defaultGetObjectOptions =
604defaultGetObjectOptions =
508− GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
605+ GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
509
606
510gooToHeaders :: GetObjectOptions -> [HT.Header]
607gooToHeaders :: GetObjectOptions -> [HT.Header]
511−gooToHeaders goo = rangeHdr ++ zip names values
608+gooToHeaders goo =
609+ rangeHdr
610+ ++ zip names values
512− ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
611+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
513 where
612 where
613+ names =
514− names = ["If-Match",
614+ [ "If-Match",
515− "If-None-Match",
615+ "If-None-Match",
516− "If-Unmodified-Since",
616+ "If-Unmodified-Since",
517− "If-Modified-Since"]
617+ "If-Modified-Since"
618+ ]
619+ values =
620+ mapMaybe
518− values = mapMaybe (fmap encodeUtf8 . (goo &))
621+ (fmap encodeUtf8 . (goo &))
622+ [ gooIfMatch,
519− [gooIfMatch, gooIfNoneMatch,
623+ gooIfNoneMatch,
520− fmap formatRFC1123 . gooIfUnmodifiedSince,
624+ fmap formatRFC1123 . gooIfUnmodifiedSince,
521− fmap formatRFC1123 . gooIfModifiedSince]
625+ fmap formatRFC1123 . gooIfModifiedSince
626+ ]
627+ rangeHdr =
522− rangeHdr = maybe [] (\a -> [(hRange, HT.renderByteRanges [a])])
628+ maybe [] (\a -> [(hRange, HT.renderByteRanges [a])]) $
523− $ gooRange goo
629+ gooRange goo
524−
630+
631+-- | Data type returned by 'getObject' representing the object being
632+-- retrieved. Use the @gor*@ functions to access its contents.
633+data GetObjectResponse = GetObjectResponse
634+ { -- | ObjectInfo of the object being retrieved.
635+ gorObjectInfo :: ObjectInfo,
636+ -- | A conduit of the bytes of the object.
637+ gorObjectStream :: C.ConduitM () ByteString Minio ()
638+ }
525
639
526-- | A data-type for events that can occur in the object storage
640-- | A data-type for events that can occur in the object storage
527-- server. Reference:
641-- server. Reference:
528-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
642-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
643+data Event
529−data Event = ObjectCreated
644+ = ObjectCreated
530− | ObjectCreatedPut
645+ | ObjectCreatedPut
531− | ObjectCreatedPost
646+ | ObjectCreatedPost
532− | ObjectCreatedCopy
647+ | ObjectCreatedCopy
533− | ObjectCreatedMultipartUpload
648+ | ObjectCreatedMultipartUpload
534− | ObjectRemoved
649+ | ObjectRemoved
535− | ObjectRemovedDelete
650+ | ObjectRemovedDelete
536− | ObjectRemovedDeleteMarkerCreated
651+ | ObjectRemovedDeleteMarkerCreated
537− | ReducedRedundancyLostObject
652+ | ReducedRedundancyLostObject
538− deriving (Eq)
653+ deriving stock (Eq, Show)
539−
654+
540−instance Show Event where
655+instance ToText Event where
541− show ObjectCreated = "s3:ObjectCreated:*"
656+ toText ObjectCreated = "s3:ObjectCreated:*"
542− show ObjectCreatedPut = "s3:ObjectCreated:Put"
657+ toText ObjectCreatedPut = "s3:ObjectCreated:Put"
543− show ObjectCreatedPost = "s3:ObjectCreated:Post"
658+ toText ObjectCreatedPost = "s3:ObjectCreated:Post"
544− show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
659+ toText ObjectCreatedCopy = "s3:ObjectCreated:Copy"
545− show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
660+ toText ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
546− show ObjectRemoved = "s3:ObjectRemoved:*"
661+ toText ObjectRemoved = "s3:ObjectRemoved:*"
547− show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
662+ toText ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
548− show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
663+ toText ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
549− show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
664+ toText ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
550
665
551textToEvent :: Text -> Maybe Event
666textToEvent :: Text -> Maybe Event
552textToEvent t = case t of
667textToEvent t = case t of
553− "s3:ObjectCreated:*" -> Just ObjectCreated
668+ "s3:ObjectCreated:*" -> Just ObjectCreated
554− "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
669+ "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
555− "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
670+ "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
556− "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
671+ "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
557− "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
672+ "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
558− "s3:ObjectRemoved:*" -> Just ObjectRemoved
673+ "s3:ObjectRemoved:*" -> Just ObjectRemoved
559− "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
674+ "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
560 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
675 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
561− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
676+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
562− _ -> Nothing
677+ _ -> Nothing
563−
564
678
565-- | Filter data type - part of notification configuration
679-- | Filter data type - part of notification configuration
566−data Filter = Filter
680+newtype Filter = Filter
567 { fFilter :: FilterKey
681 { fFilter :: FilterKey
682+ }
568− } deriving (Show, Eq)
683+ deriving stock (Show, Eq)
569
684
570-- | defaultFilter is empty, used to create a notification
685-- | defaultFilter is empty, used to create a notification
571-- configuration.
686-- configuration.
@@ -573,9 +688,10 @@ defaultFilter :: Filter
573defaultFilter = Filter defaultFilterKey
688defaultFilter = Filter defaultFilterKey
574
689
575-- | FilterKey contains FilterRules, and is part of a Filter.
690-- | FilterKey contains FilterRules, and is part of a Filter.
576−data FilterKey = FilterKey
691+newtype FilterKey = FilterKey
577 { fkKey :: FilterRules
692 { fkKey :: FilterRules
693+ }
578− } deriving (Show, Eq)
694+ deriving stock (Show, Eq)
579
695
580-- | defaultFilterKey is empty, used to create notification
696-- | defaultFilterKey is empty, used to create notification
581-- configuration.
697-- configuration.
@@ -583,16 +699,16 @@ defaultFilterKey :: FilterKey
583defaultFilterKey = FilterKey defaultFilterRules
699defaultFilterKey = FilterKey defaultFilterRules
584
700
585-- | FilterRules represents a collection of `FilterRule`s.
701-- | FilterRules represents a collection of `FilterRule`s.
586−data FilterRules = FilterRules
702+newtype FilterRules = FilterRules
587 { frFilterRules :: [FilterRule]
703 { frFilterRules :: [FilterRule]
704+ }
588− } deriving (Show, Eq)
705+ deriving stock (Show, Eq)
589
706
590-- | defaultFilterRules is empty, used to create notification
707-- | defaultFilterRules is empty, used to create notification
591-- configuration.
708-- configuration.
592defaultFilterRules :: FilterRules
709defaultFilterRules :: FilterRules
593defaultFilterRules = FilterRules []
710defaultFilterRules = FilterRules []
594
711
595−
596-- | A filter rule that can act based on the suffix or prefix of an
712-- | A filter rule that can act based on the suffix or prefix of an
597-- object. As an example, let's create two filter rules:
713-- object. As an example, let's create two filter rules:
598--
714--
@@ -603,9 +719,10 @@ defaultFilterRules = FilterRules []
603-- for objects having a suffix of ".jpg", and the @prefixRule@
719-- for objects having a suffix of ".jpg", and the @prefixRule@
604-- restricts it to objects having a prefix of "images/".
720-- restricts it to objects having a prefix of "images/".
605data FilterRule = FilterRule
721data FilterRule = FilterRule
606− { frName :: Text
722+ { frName :: Text,
607− , frValue :: Text
723+ frValue :: Text
724+ }
608− } deriving (Show, Eq)
725+ deriving stock (Show, Eq)
609
726
610-- | Arn is an alias of Text
727-- | Arn is an alias of Text
611type Arn = Text
728type Arn = Text
@@ -614,11 +731,12 @@ type Arn = Text
614-- notification system. It could represent a Queue, Topic or Lambda
731-- notification system. It could represent a Queue, Topic or Lambda
615-- Function configuration.
732-- Function configuration.
616data NotificationConfig = NotificationConfig
733data NotificationConfig = NotificationConfig
617− { ncId :: Text
734+ { ncId :: Text,
618− , ncArn :: Arn
735+ ncArn :: Arn,
619− , ncEvents :: [Event]
736+ ncEvents :: [Event],
620− , ncFilter :: Filter
737+ ncFilter :: Filter
738+ }
621− } deriving (Show, Eq)
739+ deriving stock (Show, Eq)
622
740
623-- | A data-type to represent bucket notification configuration. It is
741-- | A data-type to represent bucket notification configuration. It is
624-- a collection of queue, topic or lambda function configurations. The
742-- a collection of queue, topic or lambda function configurations. The
@@ -626,16 +744,16 @@ data NotificationConfig = NotificationConfig
626-- described at
744-- described at
627-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
745-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
628data Notification = Notification
746data Notification = Notification
629− { nQueueConfigurations :: [NotificationConfig]
747+ { nQueueConfigurations :: [NotificationConfig],
630− , nTopicConfigurations :: [NotificationConfig]
748+ nTopicConfigurations :: [NotificationConfig],
631− , nCloudFunctionConfigurations :: [NotificationConfig]
749+ nCloudFunctionConfigurations :: [NotificationConfig]
750+ }
632− } deriving (Eq, Show)
751+ deriving stock (Show, Eq)
633
752
634-- | The default notification configuration is empty.
753-- | The default notification configuration is empty.
635defaultNotification :: Notification
754defaultNotification :: Notification
636defaultNotification = Notification [] [] []
755defaultNotification = Notification [] [] []
637
756
638−
639--------------------------------------------------------------------------
757--------------------------------------------------------------------------
640-- Select API Related Types
758-- Select API Related Types
641--------------------------------------------------------------------------
759--------------------------------------------------------------------------
@@ -643,38 +761,42 @@ defaultNotification = Notification [] [] []
643-- | SelectRequest represents the Select API call. Use the
761-- | SelectRequest represents the Select API call. Use the
644-- `selectRequest` function to create a value of this type.
762-- `selectRequest` function to create a value of this type.
645data SelectRequest = SelectRequest
763data SelectRequest = SelectRequest
646− { srExpression :: Text
764+ { srExpression :: Text,
647− , srExpressionType :: ExpressionType
765+ srExpressionType :: ExpressionType,
648− , srInputSerialization :: InputSerialization
766+ srInputSerialization :: InputSerialization,
649− , srOutputSerialization :: OutputSerialization
767+ srOutputSerialization :: OutputSerialization,
650− , srRequestProgressEnabled :: Maybe Bool
768+ srRequestProgressEnabled :: Maybe Bool
769+ }
651− } deriving (Eq, Show)
770+ deriving stock (Show, Eq)
652
771
653data ExpressionType = SQL
772data ExpressionType = SQL
654− deriving (Eq, Show)
773+ deriving stock (Show, Eq)
655
774
656-- | InputSerialization represents format information of the input
775-- | InputSerialization represents format information of the input
657-- object being queried. Use one of the smart constructors such as
776-- object being queried. Use one of the smart constructors such as
658-- `defaultCsvInput` as a starting value, and add compression info
777-- `defaultCsvInput` as a starting value, and add compression info
659-- using `setInputCompressionType`
778-- using `setInputCompressionType`
660data InputSerialization = InputSerialization
779data InputSerialization = InputSerialization
661− { isCompressionType :: Maybe CompressionType
780+ { isCompressionType :: Maybe CompressionType,
662− , isFormatInfo :: InputFormatInfo
781+ isFormatInfo :: InputFormatInfo
782+ }
663− } deriving (Eq, Show)
783+ deriving stock (Show, Eq)
664
784
665-- | Data type representing the compression setting in a Select
785-- | Data type representing the compression setting in a Select
666-- request.
786-- request.
667−data CompressionType = CompressionTypeNone
787+data CompressionType
788+ = CompressionTypeNone
668− | CompressionTypeGzip
789+ | CompressionTypeGzip
669− | CompressionTypeBzip2
790+ | CompressionTypeBzip2
670− deriving (Eq, Show)
791+ deriving stock (Show, Eq)
671
792
672-- | Data type representing input object format information in a
793-- | Data type representing input object format information in a
673-- Select request.
794-- Select request.
795+data InputFormatInfo
674−data InputFormatInfo = InputFormatCSV CSVInputProp
796+ = InputFormatCSV CSVInputProp
675− | InputFormatJSON JSONInputProp
797+ | InputFormatJSON JSONInputProp
676− | InputFormatParquet
798+ | InputFormatParquet
677− deriving (Eq, Show)
799+ deriving stock (Show, Eq)
678
800
679-- | defaultCsvInput returns InputSerialization with default CSV
801-- | defaultCsvInput returns InputSerialization with default CSV
680-- format, and without any compression setting.
802-- format, and without any compression setting.
@@ -684,14 +806,18 @@ defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
684-- | linesJsonInput returns InputSerialization with JSON line based
806-- | linesJsonInput returns InputSerialization with JSON line based
685-- format with no compression setting.
807-- format with no compression setting.
686linesJsonInput :: InputSerialization
808linesJsonInput :: InputSerialization
809+linesJsonInput =
687−linesJsonInput = InputSerialization Nothing
810+ InputSerialization
811+ Nothing
688− (InputFormatJSON $ JSONInputProp JSONTypeLines)
812+ (InputFormatJSON $ JSONInputProp JSONTypeLines)
689
813
690-- | documentJsonInput returns InputSerialization with JSON document
814-- | documentJsonInput returns InputSerialization with JSON document
691-- based format with no compression setting.
815-- based format with no compression setting.
692documentJsonInput :: InputSerialization
816documentJsonInput :: InputSerialization
817+documentJsonInput =
693−documentJsonInput = InputSerialization Nothing
818+ InputSerialization
819+ Nothing
694− (InputFormatJSON $ JSONInputProp JSONTypeDocument)
820+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)
695
821
696-- | defaultParquetInput returns InputSerialization with Parquet
822-- | defaultParquetInput returns InputSerialization with Parquet
697-- format, and no compression setting.
823-- format, and no compression setting.
@@ -700,12 +826,14 @@ defaultParquetInput = InputSerialization Nothing InputFormatParquet
700
826
701-- | setInputCompressionType sets the compression type for the input
827-- | setInputCompressionType sets the compression type for the input
702-- of the SelectRequest
828-- of the SelectRequest
703−setInputCompressionType :: CompressionType -> SelectRequest
829+setInputCompressionType ::
830+ CompressionType ->
831+ SelectRequest ->
704− -> SelectRequest
832+ SelectRequest
705setInputCompressionType c i =
833setInputCompressionType c i =
706− let is = srInputSerialization i
834+ let is = srInputSerialization i
707− is' = is { isCompressionType = Just c }
835+ is' = is {isCompressionType = Just c}
708− in i { srInputSerialization = is' }
836+ in i {srInputSerialization = is'}
709
837
710-- | defaultCsvOutput returns OutputSerialization with default CSV
838-- | defaultCsvOutput returns OutputSerialization with default CSV
711-- format.
839-- format.
@@ -722,40 +850,42 @@ defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
722-- SelectRequest with the SQL query text given by @query@, the input
850-- SelectRequest with the SQL query text given by @query@, the input
723-- serialization settings (compression format and format information)
851-- serialization settings (compression format and format information)
724-- @inputSer@ and the output serialization settings @outputSer@.
852-- @inputSer@ and the output serialization settings @outputSer@.
853+selectRequest ::
854+ Text ->
855+ InputSerialization ->
725−selectRequest :: Text -> InputSerialization -> OutputSerialization
856+ OutputSerialization ->
726− -> SelectRequest
857+ SelectRequest
727selectRequest sqlQuery inputSer outputSer =
858selectRequest sqlQuery inputSer outputSer =
859+ SelectRequest
728− SelectRequest { srExpression = sqlQuery
860+ { srExpression = sqlQuery,
729− , srExpressionType = SQL
861+ srExpressionType = SQL,
730− , srInputSerialization = inputSer
862+ srInputSerialization = inputSer,
731− , srOutputSerialization = outputSer
863+ srOutputSerialization = outputSer,
732− , srRequestProgressEnabled = Nothing
864+ srRequestProgressEnabled = Nothing
733− }
865+ }
734
866
735-- | setRequestProgressEnabled sets the flag for turning on progress
867-- | setRequestProgressEnabled sets the flag for turning on progress
736-- messages when the Select response is being streamed back to the
868-- messages when the Select response is being streamed back to the
737-- client.
869-- client.
738setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
870setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
739setRequestProgressEnabled enabled sr =
871setRequestProgressEnabled enabled sr =
740− sr { srRequestProgressEnabled = Just enabled }
872+ sr {srRequestProgressEnabled = Just enabled}
741
873
742type CSVInputProp = CSVProp
874type CSVInputProp = CSVProp
743
875
744-- | CSVProp represents CSV format properties. It is built up using
876-- | CSVProp represents CSV format properties. It is built up using
745-- the Monoid instance.
877-- the Monoid instance.
746−data CSVProp = CSVProp (H.HashMap Text Text)
878+newtype CSVProp = CSVProp (H.HashMap Text Text)
747− deriving (Eq, Show)
879+ deriving stock (Show, Eq)
748
880
749−#if (__GLASGOW_HASKELL__ >= 804)
750instance Semigroup CSVProp where
881instance Semigroup CSVProp where
751− (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
882+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
752−#endif
753
883
754instance Monoid CSVProp where
884instance Monoid CSVProp where
755− mempty = CSVProp mempty
885+ mempty = CSVProp mempty
756−#if (__GLASGOW_HASKELL__ < 804)
886+
757− mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
887+csvPropsList :: CSVProp -> [(Text, Text)]
758−#endif
888+csvPropsList (CSVProp h) = sort $ H.toList h
759
889
760defaultCSVProp :: CSVProp
890defaultCSVProp :: CSVProp
761defaultCSVProp = mempty
891defaultCSVProp = mempty
@@ -779,18 +909,21 @@ quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
779-- | FileHeaderInfo specifies information about column headers for CSV
909-- | FileHeaderInfo specifies information about column headers for CSV
780-- format.
910-- format.
781data FileHeaderInfo
911data FileHeaderInfo
782− = FileHeaderNone -- ^ No column headers are present
912+ = -- | No column headers are present
913+ FileHeaderNone
783− | FileHeaderUse -- ^ Headers are present and they should be used
914+ | -- | Headers are present and they should be used
915+ FileHeaderUse
784− | FileHeaderIgnore -- ^ Header are present, but should be ignored
916+ | -- | Header are present, but should be ignored
917+ FileHeaderIgnore
785− deriving (Eq, Show)
918+ deriving stock (Show, Eq)
786
919
787-- | Specify the CSV file header info property.
920-- | Specify the CSV file header info property.
788fileHeaderInfo :: FileHeaderInfo -> CSVProp
921fileHeaderInfo :: FileHeaderInfo -> CSVProp
789−fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString
922+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toStr
790 where
923 where
791− toString FileHeaderNone = "NONE"
924+ toStr FileHeaderNone = "NONE"
792− toString FileHeaderUse = "USE"
925+ toStr FileHeaderUse = "USE"
793− toString FileHeaderIgnore = "IGNORE"
926+ toStr FileHeaderIgnore = "IGNORE"
794
927
795-- | Specify the CSV comment character property. Lines starting with
928-- | Specify the CSV comment character property. Lines starting with
796-- this character are ignored by the server.
929-- this character are ignored by the server.
@@ -803,64 +936,69 @@ allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter"
803
936
804-- | Set the CSV format properties in the InputSerialization.
937-- | Set the CSV format properties in the InputSerialization.
805setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
938setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
806−setInputCSVProps p is = is { isFormatInfo = InputFormatCSV p }
939+setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
807
940
808-- | Set the CSV format properties in the OutputSerialization.
941-- | Set the CSV format properties in the OutputSerialization.
809outputCSVFromProps :: CSVProp -> OutputSerialization
942outputCSVFromProps :: CSVProp -> OutputSerialization
810−outputCSVFromProps p = OutputSerializationCSV p
943+outputCSVFromProps = OutputSerializationCSV
811
944
812−data JSONInputProp = JSONInputProp { jsonipType :: JSONType }
945+newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
813− deriving (Eq, Show)
946+ deriving stock (Show, Eq)
814
947
815data JSONType = JSONTypeDocument | JSONTypeLines
948data JSONType = JSONTypeDocument | JSONTypeLines
816− deriving (Eq, Show)
949+ deriving stock (Show, Eq)
817−
818
950
819-- | OutputSerialization represents output serialization settings for
951-- | OutputSerialization represents output serialization settings for
820-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
952-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
821-- a starting point.
953-- a starting point.
954+data OutputSerialization
822−data OutputSerialization = OutputSerializationJSON JSONOutputProp
955+ = OutputSerializationJSON JSONOutputProp
823− | OutputSerializationCSV CSVOutputProp
956+ | OutputSerializationCSV CSVOutputProp
824− deriving (Eq, Show)
957+ deriving stock (Show, Eq)
825
958
826type CSVOutputProp = CSVProp
959type CSVOutputProp = CSVProp
827
960
828-- | quoteFields is an output serialization parameter
961-- | quoteFields is an output serialization parameter
829quoteFields :: QuoteFields -> CSVProp
962quoteFields :: QuoteFields -> CSVProp
963+quoteFields q = CSVProp $
830−quoteFields q = CSVProp $ H.singleton "QuoteFields" $
964+ H.singleton "QuoteFields" $
831− case q of
965+ case q of
832− QuoteFieldsAsNeeded -> "ASNEEDED"
966+ QuoteFieldsAsNeeded -> "ASNEEDED"
833− QuoteFieldsAlways -> "ALWAYS"
967+ QuoteFieldsAlways -> "ALWAYS"
834
968
835-- | Represent the QuoteField setting.
969-- | Represent the QuoteField setting.
836data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
970data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
837− deriving (Eq, Show)
971+ deriving stock (Show, Eq)
838
972
839−data JSONOutputProp = JSONOutputProp { jsonopRecordDelimiter :: Maybe Text }
973+newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
840− deriving (Eq, Show)
974+ deriving stock (Show, Eq)
841
975
842-- | Set the output record delimiter for JSON format
976-- | Set the output record delimiter for JSON format
843outputJSONFromRecordDelimiter :: Text -> OutputSerialization
977outputJSONFromRecordDelimiter :: Text -> OutputSerialization
844outputJSONFromRecordDelimiter t =
978outputJSONFromRecordDelimiter t =
845− OutputSerializationJSON (JSONOutputProp $ Just t)
979+ OutputSerializationJSON (JSONOutputProp $ Just t)
846
980
847-- Response related types
981-- Response related types
848
982
849-- | An EventMessage represents each kind of message received from the server.
983-- | An EventMessage represents each kind of message received from the server.
984+data EventMessage
850−data EventMessage = ProgressEventMessage { emProgress :: Progress }
985+ = ProgressEventMessage Progress
851− | StatsEventMessage { emStats :: Stats }
986+ | StatsEventMessage Stats
852− | RequestLevelErrorMessage { emErrorCode :: Text
987+ | RequestLevelErrorMessage
853− , emErrorMessage :: Text
988+ Text
854− }
989+ -- ^ Error code
990+ Text
991+ -- ^ Error message
855− | RecordPayloadEventMessage { emPayloadBytes :: ByteString }
992+ | RecordPayloadEventMessage ByteString
856− deriving (Eq, Show)
993+ deriving stock (Show, Eq)
857−
994+
858−data MsgHeaderName = MessageType
995+data MsgHeaderName
996+ = MessageType
859− | EventType
997+ | EventType
860− | ContentType
998+ | ContentType
861− | ErrorCode
999+ | ErrorCode
862− | ErrorMessage
1000+ | ErrorMessage
863− deriving (Eq, Show)
1001+ deriving stock (Show, Eq)
864
1002
865msgHeaderValueType :: Word8
1003msgHeaderValueType :: Word8
866msgHeaderValueType = 7
1004msgHeaderValueType = 7
@@ -868,11 +1006,12 @@ msgHeaderValueType = 7
868type MessageHeader = (MsgHeaderName, Text)
1006type MessageHeader = (MsgHeaderName, Text)
869
1007
870-- | Represent the progress event returned in the Select response.
1008-- | Represent the progress event returned in the Select response.
871−data Progress = Progress { pBytesScanned :: Int64
1009+data Progress = Progress
1010+ { pBytesScanned :: Int64,
872− , pBytesProcessed :: Int64
1011+ pBytesProcessed :: Int64,
873− , pBytesReturned :: Int64
1012+ pBytesReturned :: Int64
874− }
1013+ }
875− deriving (Eq, Show)
1014+ deriving stock (Show, Eq)
876
1015
877-- | Represent the stats event returned at the end of the Select
1016-- | Represent the stats event returned at the end of the Select
878-- response.
1017-- response.
@@ -885,86 +1024,89 @@ type Stats = Progress
885-- | Represents different kinds of payload that are used with S3 API
1024-- | Represents different kinds of payload that are used with S3 API
886-- requests.
1025-- requests.
887data Payload
1026data Payload
888− = PayloadBS ByteString
1027+ = PayloadBS ByteString
889− | PayloadH Handle Int64 Int64 -- file handle, offset and length
1028+ | PayloadH Handle Int64 Int64 -- file handle, offset and length
890− | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
1029+ | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
891
1030
892defaultPayload :: Payload
1031defaultPayload :: Payload
893defaultPayload = PayloadBS ""
1032defaultPayload = PayloadBS ""
894
1033
895−data AdminReqInfo = AdminReqInfo {
1034+data AdminReqInfo = AdminReqInfo
896− ariMethod :: Method
1035+ { ariMethod :: Method,
897− , ariPayloadHash :: Maybe ByteString
1036+ ariPayloadHash :: Maybe ByteString,
898− , ariPayload :: Payload
1037+ ariPayload :: Payload,
899− , ariPath :: ByteString
1038+ ariPath :: ByteString,
900− , ariHeaders :: [Header]
1039+ ariHeaders :: [Header],
901− , ariQueryParams :: Query
1040+ ariQueryParams :: Query
902 }
1041 }
903
1042
904data S3ReqInfo = S3ReqInfo
1043data S3ReqInfo = S3ReqInfo
905− { riMethod :: Method
1044+ { riMethod :: Method,
906− , riBucket :: Maybe Bucket
1045+ riBucket :: Maybe Bucket,
907− , riObject :: Maybe Object
1046+ riObject :: Maybe Object,
908− , riQueryParams :: Query
1047+ riQueryParams :: Query,
909− , riHeaders :: [Header]
1048+ riHeaders :: [Header],
910− , riPayload :: Payload
1049+ riPayload :: Payload,
911− , riPayloadHash :: Maybe ByteString
1050+ riPayloadHash :: Maybe ByteString,
912− , riRegion :: Maybe Region
1051+ riRegion :: Maybe Region,
913− , riNeedsLocation :: Bool
1052+ riNeedsLocation :: Bool,
1053+ riPresignExpirySecs :: Maybe UrlExpiry
914 }
1054 }
915
1055
916defaultS3ReqInfo :: S3ReqInfo
1056defaultS3ReqInfo :: S3ReqInfo
917−defaultS3ReqInfo = S3ReqInfo HT.methodGet Nothing Nothing
1057+defaultS3ReqInfo =
1058+ S3ReqInfo
1059+ HT.methodGet
1060+ Nothing
1061+ Nothing
1062+ []
1063+ []
918− [] [] defaultPayload Nothing Nothing True
1064+ defaultPayload
1065+ Nothing
1066+ Nothing
1067+ True
1068+ Nothing
919
1069
920getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
1070getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
921getS3Path b o =
1071getS3Path b o =
922− let segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
1072+ let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
923− in
924− B.concat ["/", B.intercalate "/" segments]
1073+ in B.concat ["/", B.intercalate "/" segments]
925−
926−-- | Time to expire for a presigned URL. It interpreted as a number of
927−-- seconds. The maximum duration that can be specified is 7 days.
928−type UrlExpiry = Int
929
1074
930−type RegionMap = Map.Map Bucket Region
1075+type RegionMap = H.HashMap Bucket Region
931
1076
932-- | The Minio Monad - all computations accessing object storage
1077-- | The Minio Monad - all computations accessing object storage
933-- happens in it.
1078-- happens in it.
934−newtype Minio a = Minio {
1079+newtype Minio a = Minio
935− unMinio :: ReaderT MinioConn (ResourceT IO) a
1080+ { unMinio :: ReaderT MinioConn (ResourceT IO) a
936 }
1081 }
937− deriving (
1082+ deriving newtype
938− Functor
1083+ ( Functor,
939− , Applicative
1084+ Applicative,
940− , Monad
1085+ Monad,
941− , MonadIO
1086+ MonadIO,
942− , MonadReader MinioConn
1087+ MonadReader MinioConn,
943− , MonadResource
1088+ MonadResource,
1089+ MonadUnliftIO
944 )
1090 )
945
1091
946−instance MonadUnliftIO Minio where
947− askUnliftIO = Minio $ ReaderT $ \r ->
948− withUnliftIO $ \u ->
949− return (UnliftIO (unliftIO u . flip runReaderT r . unMinio))
950−
951-- | MinioConn holds connection info and a connection pool to allow
1092-- | MinioConn holds connection info and a connection pool to allow
952-- for efficient resource re-use.
1093-- for efficient resource re-use.
953data MinioConn = MinioConn
1094data MinioConn = MinioConn
954− { mcConnInfo :: ConnectInfo
1095+ { mcConnInfo :: ConnectInfo,
955− , mcConnManager :: NC.Manager
1096+ mcConnManager :: NC.Manager,
956− , mcRegionMap :: MVar RegionMap
1097+ mcRegionMap :: MVar RegionMap
957 }
1098 }
958
1099
959class HasSvcNamespace env where
1100class HasSvcNamespace env where
960 getSvcNamespace :: env -> Text
1101 getSvcNamespace :: env -> Text
961
1102
962instance HasSvcNamespace MinioConn where
1103instance HasSvcNamespace MinioConn where
1104+ getSvcNamespace env =
963− getSvcNamespace env = let host = connectHost $ mcConnInfo env
1105+ let host = connectHost $ mcConnInfo env
964− in if | host == "storage.googleapis.com" ->
1106+ in ( if host == "storage.googleapis.com"
965− "http://doc.s3.amazonaws.com/2006-03-01"
1107+ then "http://doc.s3.amazonaws.com/2006-03-01"
966− | otherwise ->
967− "http://s3.amazonaws.com/doc/2006-03-01/"
1108+ else "http://s3.amazonaws.com/doc/2006-03-01/"
1109+ )
968
1110
969-- | Takes connection information and returns a connection object to
1111-- | Takes connection information and returns a connection object to
970-- be passed to 'runMinio'. The returned value can be kept in the
1112-- be passed to 'runMinio'. The returned value can be kept in the
@@ -972,11 +1114,16 @@ instance HasSvcNamespace MinioConn where
972-- object storage is accessed.
1114-- object storage is accessed.
973connect :: ConnectInfo -> IO MinioConn
1115connect :: ConnectInfo -> IO MinioConn
974connect ci = do
1116connect ci = do
1117+ let settings
975− let settings | connectIsSecure ci && connectDisableTLSCertValidation ci =
1118+ | connectIsSecure ci && connectDisableTLSCertValidation ci =
1119+ let badTlsSettings =
1120+ case def of
976− let badTlsSettings = Conn.TLSSettingsSimple True False False
1121+ tlsSettings@Conn.TLSSettingsSimple {} ->
1122+ tlsSettings {Conn.settingDisableCertificateValidation = True}
1123+ _ -> def
977− in TLS.mkManagerSettings badTlsSettings Nothing
1124+ in TLS.mkManagerSettings badTlsSettings Nothing
978− | connectIsSecure ci = NC.tlsManagerSettings
1125+ | connectIsSecure ci = NC.tlsManagerSettings
979− | otherwise = defaultManagerSettings
1126+ | otherwise = defaultManagerSettings
980 mgr <- NC.newManager settings
1127 mgr <- NC.newManager settings
981 mkMinioConn ci mgr
1128 mkMinioConn ci mgr
982
1129
@@ -984,35 +1131,61 @@ connect ci = do
984-- `MinioConn`. This reuses connections, but otherwise it is similar
1131-- `MinioConn`. This reuses connections, but otherwise it is similar
985-- to `runMinio`.
1132-- to `runMinio`.
986runMinioWith :: MinioConn -> Minio a -> IO (Either MinioErr a)
1133runMinioWith :: MinioConn -> Minio a -> IO (Either MinioErr a)
987−runMinioWith conn m = runResourceT . flip runReaderT conn . unMinio $
1134+runMinioWith conn m = runResourceT $ runMinioResWith conn m
988− fmap Right m `U.catches`
989− [ U.Handler handlerServiceErr
990− , U.Handler handlerHE
991− , U.Handler handlerFE
992− , U.Handler handlerValidation
993− ]
994− where
995− handlerServiceErr = return . Left . MErrService
996− handlerHE = return . Left . MErrHTTP
997− handlerFE = return . Left . MErrIO
998− handlerValidation = return . Left . MErrValidation
999
1135
1000-- | Given `ConnectInfo` and a HTTP connection manager, create a
1136-- | Given `ConnectInfo` and a HTTP connection manager, create a
1001-- `MinioConn`.
1137-- `MinioConn`.
1002mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
1138mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
1003mkMinioConn ci mgr = do
1139mkMinioConn ci mgr = do
1004− rMapMVar <- M.newMVar Map.empty
1140+ rMapMVar <- M.newMVar H.empty
1005− return $ MinioConn ci mgr rMapMVar
1141+ return $ MinioConn ci mgr rMapMVar
1006
1142
1007-- | Run the Minio action and return the result or an error.
1143-- | Run the Minio action and return the result or an error.
1008runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
1144runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
1009runMinio ci m = do
1145runMinio ci m = do
1010 conn <- connect ci
1146 conn <- connect ci
1011− runMinioWith conn m
1147+ runResourceT $ runMinioResWith conn m
1148+
1149+-- | Similar to 'runMinioWith'. Allows applications to allocate/release
1150+-- its resources along side MinIO's internal resources.
1151+runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)
1152+runMinioResWith conn m =
1153+ flip runReaderT conn . unMinio $
1154+ fmap Right m
1155+ `U.catches` [ U.Handler handlerServiceErr,
1156+ U.Handler handlerHE,
1157+ U.Handler handlerFE,
1158+ U.Handler handlerValidation
1159+ ]
1160+ where
1161+ handlerServiceErr = return . Left . MErrService
1162+ handlerHE = return . Left . MErrHTTP
1163+ handlerFE = return . Left . MErrIO
1164+ handlerValidation = return . Left . MErrValidation
1012
1165
1013−s3Name :: Text -> Text -> Name
1166+-- | Similar to 'runMinio'. Allows applications to allocate/release
1014−s3Name ns s = Name s (Just ns) Nothing
1167+-- its resources along side MinIO's internal resources.
1168+runMinioRes :: ConnectInfo -> Minio a -> ResourceT IO (Either MinioErr a)
1169+runMinioRes ci m = do
1170+ conn <- liftIO $ connect ci
1171+ runMinioResWith conn m
1015
1172
1016-- | Format as per RFC 1123.
1173-- | Format as per RFC 1123.
1017formatRFC1123 :: UTCTime -> T.Text
1174formatRFC1123 :: UTCTime -> T.Text
1018formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1175formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1176+
1177+lookupRegionCache :: Bucket -> Minio (Maybe Region)
1178+lookupRegionCache b = do
1179+ rMVar <- asks mcRegionMap
1180+ rMap <- UM.readMVar rMVar
1181+ return $ H.lookup b rMap
1182+
1183+addToRegionCache :: Bucket -> Region -> Minio ()
1184+addToRegionCache b region = do
1185+ rMVar <- asks mcRegionMap
1186+ UM.modifyMVar_ rMVar $ return . H.insert b region
1187+
1188+deleteFromRegionCache :: Bucket -> Minio ()
1189+deleteFromRegionCache b = do
1190+ rMVar <- asks mcRegionMap
1191+ UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/Data/ByteString.hs+19−18
@@ -13,23 +13,21 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
17{-# LANGUAGE FlexibleInstances #-}
16{-# LANGUAGE FlexibleInstances #-}
17+
18module Network.Minio.Data.ByteString
18module Network.Minio.Data.ByteString
19− (
20− stripBS
19+ ( stripBS,
21− , UriEncodable(..)
20+ UriEncodable (..),
21+ )
22− ) where
22+where
23
23
24import qualified Data.ByteString as B
24import qualified Data.ByteString as B
25import qualified Data.ByteString.Builder as BB
25import qualified Data.ByteString.Builder as BB
26import qualified Data.ByteString.Char8 as BC8
26import qualified Data.ByteString.Char8 as BC8
27import qualified Data.ByteString.Lazy as LB
27import qualified Data.ByteString.Lazy as LB
28−import Data.Char (isSpace, toUpper, isAsciiUpper, isAsciiLower, isDigit)
28+import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace, toUpper)
29import qualified Data.Text as T
29import qualified Data.Text as T
30−import Numeric (showHex)
30+import Numeric (showHex)
31−
32−import Lib.Prelude
33
31
34stripBS :: ByteString -> ByteString
32stripBS :: ByteString -> ByteString
35stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
33stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
@@ -39,8 +37,10 @@ class UriEncodable s where
39
37
40instance UriEncodable [Char] where
38instance UriEncodable [Char] where
41 uriEncode encodeSlash payload =
39 uriEncode encodeSlash payload =
40+ LB.toStrict $
42− LB.toStrict $ BB.toLazyByteString $ mconcat $
41+ BB.toLazyByteString $
42+ mconcat $
43− map (`uriEncodeChar` encodeSlash) payload
43+ map (`uriEncodeChar` encodeSlash) payload
44
44
45instance UriEncodable ByteString where
45instance UriEncodable ByteString where
46 -- assumes that uriEncode is passed ASCII encoded strings.
46 -- assumes that uriEncode is passed ASCII encoded strings.
@@ -59,16 +59,17 @@ uriEncodeChar '/' True = BB.byteString "%2F"
59uriEncodeChar '/' False = BB.char7 '/'
59uriEncodeChar '/' False = BB.char7 '/'
60uriEncodeChar ch _
60uriEncodeChar ch _
61 | isAsciiUpper ch
61 | isAsciiUpper ch
62− || isAsciiLower ch
62+ || isAsciiLower ch
63− || isDigit ch
63+ || isDigit ch
64− || (ch == '_')
64+ || (ch == '_')
65− || (ch == '-')
65+ || (ch == '-')
66− || (ch == '.')
66+ || (ch == '.')
67+ || (ch == '~') =
67− || (ch == '~') = BB.char7 ch
68+ BB.char7 ch
68 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
69 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
69 where
70 where
70 f :: Word8 -> BB.Builder
71 f :: Word8 -> BB.Builder
71 f n = BB.char7 '%' <> BB.string7 hexStr
72 f n = BB.char7 '%' <> BB.string7 hexStr
72 where
73 where
73 hexStr = map toUpper $ showHex q $ showHex r ""
74 hexStr = map toUpper $ showHex q $ showHex r ""
74− (q, r) = divMod (fromIntegral n) (16::Word8)
75+ (q, r) = divMod n (16 :: Word8)
src/Network/Minio/Data/Crypto.hs+32−33
@@ -15,55 +15,54 @@
15--
15--
16
16
17module Network.Minio.Data.Crypto
17module Network.Minio.Data.Crypto
18− (
19− hashSHA256
18+ ( hashSHA256,
20− , hashSHA256FromSource
19+ hashSHA256FromSource,
21−
22− , hashMD5
20+ hashMD5,
23− , hashMD5ToBase64
21+ hashMD5ToBase64,
24− , hashMD5FromSource
22+ hashMD5FromSource,
25−
26− , hmacSHA256
23+ hmacSHA256,
27− , hmacSHA256RawBS
24+ hmacSHA256RawBS,
28− , digestToBS
25+ digestToBS,
29− , digestToBase16
26+ digestToBase16,
30−
31− , encodeToBase64
27+ encodeToBase64,
28+ )
32− ) where
29+where
33−
30+
34−import Crypto.Hash (Digest, MD5 (..), SHA256 (..),
31+import Crypto.Hash
32+ ( Digest,
33+ MD5 (..),
34+ SHA256 (..),
35− hashWith)
35+ hashWith,
36+ )
36−import Crypto.Hash.Conduit (sinkHash)
37+import Crypto.Hash.Conduit (sinkHash)
37−import Crypto.MAC.HMAC (HMAC, hmac)
38+import Crypto.MAC.HMAC (HMAC, hmac)
38−import Data.ByteArray (ByteArrayAccess, convert)
39+import Data.ByteArray (ByteArrayAccess, convert)
39−import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
40+import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
40−import qualified Data.Conduit as C
41+import qualified Data.Conduit as C
41−
42−import Lib.Prelude
43
42
44hashSHA256 :: ByteString -> ByteString
43hashSHA256 :: ByteString -> ByteString
45hashSHA256 = digestToBase16 . hashWith SHA256
44hashSHA256 = digestToBase16 . hashWith SHA256
46
45
47−hashSHA256FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
46+hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
48hashSHA256FromSource src = do
47hashSHA256FromSource src = do
49 digest <- C.connect src sinkSHA256Hash
48 digest <- C.connect src sinkSHA256Hash
50 return $ digestToBase16 digest
49 return $ digestToBase16 digest
51 where
50 where
52 -- To help with type inference
51 -- To help with type inference
53− sinkSHA256Hash :: Monad m => C.ConduitM ByteString Void m (Digest SHA256)
52+ sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
54 sinkSHA256Hash = sinkHash
53 sinkSHA256Hash = sinkHash
55
54
56-- Returns MD5 hash hex encoded.
55-- Returns MD5 hash hex encoded.
57hashMD5 :: ByteString -> ByteString
56hashMD5 :: ByteString -> ByteString
58hashMD5 = digestToBase16 . hashWith MD5
57hashMD5 = digestToBase16 . hashWith MD5
59
58
60−hashMD5FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
59+hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
61hashMD5FromSource src = do
60hashMD5FromSource src = do
62 digest <- C.connect src sinkMD5Hash
61 digest <- C.connect src sinkMD5Hash
63 return $ digestToBase16 digest
62 return $ digestToBase16 digest
64 where
63 where
65 -- To help with type inference
64 -- To help with type inference
66− sinkMD5Hash :: Monad m => C.ConduitM ByteString Void m (Digest MD5)
65+ sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
67 sinkMD5Hash = sinkHash
66 sinkMD5Hash = sinkHash
68
67
69hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
68hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -72,15 +71,15 @@ hmacSHA256 message key = hmac key message
72hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
71hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
73hmacSHA256RawBS message key = convert $ hmacSHA256 message key
72hmacSHA256RawBS message key = convert $ hmacSHA256 message key
74
73
75−digestToBS :: ByteArrayAccess a => a -> ByteString
74+digestToBS :: (ByteArrayAccess a) => a -> ByteString
76digestToBS = convert
75digestToBS = convert
77
76
78−digestToBase16 :: ByteArrayAccess a => a -> ByteString
77+digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
79digestToBase16 = convertToBase Base16
78digestToBase16 = convertToBase Base16
80
79
81-- Returns MD5 hash base 64 encoded.
80-- Returns MD5 hash base 64 encoded.
82−hashMD5ToBase64 :: ByteArrayAccess a => a -> ByteString
81+hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
83hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
82hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
84
83
85−encodeToBase64 :: ByteArrayAccess a => a -> ByteString
84+encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
86encodeToBase64 = convertToBase Base64
85encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+17−13
@@ -15,20 +15,24 @@
15--
15--
16
16
17module Network.Minio.Data.Time
17module Network.Minio.Data.Time
18− (
19− awsTimeFormat
18+ ( awsTimeFormat,
20− , awsTimeFormatBS
19+ awsTimeFormatBS,
21− , awsDateFormat
20+ awsDateFormat,
22− , awsDateFormatBS
21+ awsDateFormatBS,
23− , awsParseTime
22+ awsParseTime,
24− , iso8601TimeFormat
23+ iso8601TimeFormat,
24+ UrlExpiry,
25+ )
25− ) where
26+where
26−
27+
27−
28−import Data.ByteString.Char8 (pack)
28+import Data.ByteString.Char8 (pack)
29import qualified Data.Time as Time
29import qualified Data.Time as Time
30+import Data.Time.Format.ISO8601 (iso8601Show)
31+import Lib.Prelude
30
32
31−import Lib.Prelude
33+-- | Time to expire for a presigned URL. It interpreted as a number of
34+-- seconds. The maximum duration that can be specified is 7 days.
35+type UrlExpiry = Int
32
36
33awsTimeFormat :: UTCTime -> [Char]
37awsTimeFormat :: UTCTime -> [Char]
34awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
38awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
@@ -46,4 +50,4 @@ awsParseTime :: [Char] -> Maybe UTCTime
46awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
50awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
47
51
48iso8601TimeFormat :: UTCTime -> [Char]
52iso8601TimeFormat :: UTCTime -> [Char]
49−iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
53+iso8601TimeFormat = iso8601Show
src/Network/Minio/Errors.hs+59−52
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,76 +14,83 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17−module Network.Minio.Errors where
17+module Network.Minio.Errors
18+ ( MErrV (..),
19+ ServiceErr (..),
20+ MinioErr (..),
21+ toServiceErr,
22+ )
23+where
18
24
19−import Control.Exception
25+import Control.Exception (IOException)
20import qualified Network.HTTP.Conduit as NC
26import qualified Network.HTTP.Conduit as NC
21
27
22−import Lib.Prelude
23−
24−
25---------------------------------
28---------------------------------
26-- Errors
29-- Errors
27---------------------------------
30---------------------------------
31+
28-- | Various validation errors
32-- | Various validation errors
33+data MErrV
29−data MErrV = MErrVSinglePUTSizeExceeded Int64
34+ = MErrVSinglePUTSizeExceeded Int64
30− | MErrVPutSizeExceeded Int64
35+ | MErrVPutSizeExceeded Int64
31− | MErrVETagHeaderNotFound
36+ | MErrVETagHeaderNotFound
32− | MErrVInvalidObjectInfoResponse
37+ | MErrVInvalidObjectInfoResponse
33− | MErrVInvalidSrcObjSpec Text
38+ | MErrVInvalidSrcObjSpec Text
34− | MErrVInvalidSrcObjByteRange (Int64, Int64)
39+ | MErrVInvalidSrcObjByteRange (Int64, Int64)
35− | MErrVCopyObjSingleNoRangeAccepted
40+ | MErrVCopyObjSingleNoRangeAccepted
36− | MErrVRegionNotSupported Text
41+ | MErrVRegionNotSupported Text
37− | MErrVXmlParse Text
42+ | MErrVXmlParse Text
38− | MErrVInvalidBucketName Text
43+ | MErrVInvalidBucketName Text
39− | MErrVInvalidObjectName Text
44+ | MErrVInvalidObjectName Text
40− | MErrVInvalidUrlExpiry Int
45+ | MErrVInvalidUrlExpiry Int
41− | MErrVJsonParse Text
46+ | MErrVJsonParse Text
42− | MErrVInvalidHealPath
47+ | MErrVInvalidHealPath
43− | MErrVMissingCredentials
48+ | MErrVMissingCredentials
44− | MErrVInvalidEncryptionKeyLength
49+ | MErrVInvalidEncryptionKeyLength
45− | MErrVStreamingBodyUnexpectedEOF
50+ | MErrVStreamingBodyUnexpectedEOF
46− | MErrVUnexpectedPayload
51+ | MErrVUnexpectedPayload
52+ | MErrVSTSEndpointNotFound
47− deriving (Show, Eq)
53+ deriving stock (Show, Eq)
48
54
49instance Exception MErrV
55instance Exception MErrV
50
56
51-- | Errors returned by S3 compatible service
57-- | Errors returned by S3 compatible service
58+data ServiceErr
52−data ServiceErr = BucketAlreadyExists
59+ = BucketAlreadyExists
53− | BucketAlreadyOwnedByYou
60+ | BucketAlreadyOwnedByYou
54− | NoSuchBucket
61+ | NoSuchBucket
55− | InvalidBucketName
62+ | InvalidBucketName
56− | NoSuchKey
63+ | NoSuchKey
57− | SelectErr Text Text
64+ | SelectErr Text Text
58− | ServiceErr Text Text
65+ | ServiceErr Text Text
59− deriving (Show, Eq)
66+ deriving stock (Show, Eq)
60
67
61instance Exception ServiceErr
68instance Exception ServiceErr
62
69
63toServiceErr :: Text -> Text -> ServiceErr
70toServiceErr :: Text -> Text -> ServiceErr
64−toServiceErr "NoSuchKey" _ = NoSuchKey
71+toServiceErr "NoSuchKey" _ = NoSuchKey
65−toServiceErr "NoSuchBucket" _ = NoSuchBucket
72+toServiceErr "NoSuchBucket" _ = NoSuchBucket
66−toServiceErr "InvalidBucketName" _ = InvalidBucketName
73+toServiceErr "InvalidBucketName" _ = InvalidBucketName
67toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
74toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
68−toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
75+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
69−toServiceErr code message = ServiceErr code message
76+toServiceErr code message = ServiceErr code message
70−
71
77
72-- | Errors thrown by the library
78-- | Errors thrown by the library
79+data MinioErr
73−data MinioErr = MErrHTTP NC.HttpException
80+ = MErrHTTP NC.HttpException
74− | MErrIO IOException
81+ | MErrIO IOException
75− | MErrService ServiceErr
82+ | MErrService ServiceErr
76− | MErrValidation MErrV
83+ | MErrValidation MErrV
77− deriving (Show)
84+ deriving stock (Show)
78
85
79instance Eq MinioErr where
86instance Eq MinioErr where
80− MErrHTTP _ == MErrHTTP _ = True
87+ MErrHTTP _ == MErrHTTP _ = True
81− MErrHTTP _ == _ = False
88+ MErrHTTP _ == _ = False
82− MErrIO _ == MErrIO _ = True
89+ MErrIO _ == MErrIO _ = True
83− MErrIO _ == _ = False
90+ MErrIO _ == _ = False
84− MErrService a == MErrService b = a == b
91+ MErrService a == MErrService b = a == b
85− MErrService _ == _ = False
92+ MErrService _ == _ = False
86− MErrValidation a == MErrValidation b = a == b
93+ MErrValidation a == MErrValidation b = a == b
87− MErrValidation _ == _ = False
94+ MErrValidation _ == _ = False
88
95
89instance Exception MinioErr
96instance Exception MinioErr
src/Network/Minio/JsonParser.hs+23−16
@@ -15,28 +15,35 @@
15--
15--
16
16
17module Network.Minio.JsonParser
17module Network.Minio.JsonParser
18− (
19− parseErrResponseJSON
18+ ( parseErrResponseJSON,
19+ )
20− ) where
20+where
21
21
22−import Data.Aeson (FromJSON, eitherDecode, parseJSON,
22+import Data.Aeson
23+ ( FromJSON,
24+ eitherDecode,
25+ parseJSON,
23− withObject, (.:))
26+ withObject,
27+ (.:),
28+ )
24−import qualified Data.Text as T
29+import qualified Data.Text as T
30+import Lib.Prelude
31+import Network.Minio.Errors
25
32
26−import Lib.Prelude
33+data AdminErrJSON = AdminErrJSON
34+ { aeCode :: Text,
35+ aeMessage :: Text
36+ }
37+ deriving stock (Eq, Show)
27
38
28−import Network.Minio.Errors
29−
30−data AdminErrJSON = AdminErrJSON { aeCode :: Text
31− , aeMessage :: Text
32− } deriving (Eq, Show)
33instance FromJSON AdminErrJSON where
39instance FromJSON AdminErrJSON where
34− parseJSON = withObject "AdminErrJSON" $ \v -> AdminErrJSON
40+ parseJSON = withObject "AdminErrJSON" $ \v ->
41+ AdminErrJSON
35− <$> v .: "Code"
42+ <$> v .: "Code"
36− <*> v .: "Message"
43+ <*> v .: "Message"
37
44
38parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
45parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
39parseErrResponseJSON jsondata =
46parseErrResponseJSON jsondata =
40 case eitherDecode jsondata of
47 case eitherDecode jsondata of
41 Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
48 Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
42− Left err -> throwIO $ MErrVJsonParse $ T.pack err
49+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs+98−38
@@ -16,20 +16,50 @@
16
16
17module Network.Minio.ListOps where
17module Network.Minio.ListOps where
18
18
19−import qualified Data.Conduit as C
19+import qualified Data.Conduit as C
20import qualified Data.Conduit.Combinators as CC
20import qualified Data.Conduit.Combinators as CC
21−import qualified Data.Conduit.List as CL
21+import qualified Data.Conduit.List as CL
22−
23−import Lib.Prelude
24−
25−import Network.Minio.Data
22+import Network.Minio.Data
23+ ( Bucket,
24+ ListObjectsResult
25+ ( lorCPrefixes,
26+ lorHasMore,
27+ lorNextToken,
28+ lorObjects
29+ ),
30+ ListObjectsV1Result
31+ ( lorCPrefixes',
32+ lorHasMore',
33+ lorNextMarker,
34+ lorObjects'
35+ ),
36+ ListPartsResult (lprHasMore, lprNextPart, lprParts),
37+ ListUploadsResult
38+ ( lurHasMore,
39+ lurNextKey,
40+ lurNextUpload,
41+ lurUploads
42+ ),
43+ Minio,
44+ Object,
45+ ObjectInfo,
46+ ObjectPartInfo (opiSize),
47+ UploadId,
48+ UploadInfo (UploadInfo),
49+ )
26−import Network.Minio.S3API
50+import Network.Minio.S3API
51+ ( listIncompleteParts',
52+ listIncompleteUploads',
53+ listObjects',
54+ listObjectsV1',
55+ )
27
56
28-- | Represents a list output item - either an object or an object
57-- | Represents a list output item - either an object or an object
29-- prefix (i.e. a directory).
58-- prefix (i.e. a directory).
59+data ListItem
30−data ListItem = ListItemObject ObjectInfo
60+ = ListItemObject ObjectInfo
31− | ListItemPrefix Text
61+ | ListItemPrefix Text
32− deriving (Show, Eq)
62+ deriving stock (Show, Eq)
33
63
34-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
64-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
35-- similar to a file system tree traversal.
65-- similar to a file system tree traversal.
@@ -48,73 +78,103 @@ listObjects bucket prefix recurse = loop Nothing
48 where
78 where
49 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
79 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
50 loop nextToken = do
80 loop nextToken = do
51− let
52− delimiter = bool (Just "/") Nothing recurse
81+ let delimiter = bool (Just "/") Nothing recurse
53
82
54 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
83 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
55 CL.sourceList $ map ListItemObject $ lorObjects res
84 CL.sourceList $ map ListItemObject $ lorObjects res
56 unless recurse $
85 unless recurse $
86+ CL.sourceList $
57− CL.sourceList $ map ListItemPrefix $ lorCPrefixes res
87+ map ListItemPrefix $
88+ lorCPrefixes res
58 when (lorHasMore res) $
89 when (lorHasMore res) $
59 loop (lorNextToken res)
90 loop (lorNextToken res)
60
91
61-- | Lists objects - similar to @listObjects@, however uses the older
92-- | Lists objects - similar to @listObjects@, however uses the older
62-- V1 AWS S3 API. Prefer @listObjects@ to this.
93-- V1 AWS S3 API. Prefer @listObjects@ to this.
63−listObjectsV1 :: Bucket -> Maybe Text -> Bool
94+listObjectsV1 ::
95+ Bucket ->
96+ Maybe Text ->
97+ Bool ->
64− -> C.ConduitM () ListItem Minio ()
98+ C.ConduitM () ListItem Minio ()
65listObjectsV1 bucket prefix recurse = loop Nothing
99listObjectsV1 bucket prefix recurse = loop Nothing
66 where
100 where
67 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
101 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
68 loop nextMarker = do
102 loop nextMarker = do
69− let
70− delimiter = bool (Just "/") Nothing recurse
103+ let delimiter = bool (Just "/") Nothing recurse
71
104
72 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
105 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
73 CL.sourceList $ map ListItemObject $ lorObjects' res
106 CL.sourceList $ map ListItemObject $ lorObjects' res
74 unless recurse $
107 unless recurse $
108+ CL.sourceList $
75− CL.sourceList $ map ListItemPrefix $ lorCPrefixes' res
109+ map ListItemPrefix $
110+ lorCPrefixes' res
76 when (lorHasMore' res) $
111 when (lorHasMore' res) $
77 loop (lorNextMarker res)
112 loop (lorNextMarker res)
78
113
79-- | List incomplete uploads in a bucket matching the given prefix. If
114-- | List incomplete uploads in a bucket matching the given prefix. If
80-- recurse is set to True incomplete uploads for the given prefix are
115-- recurse is set to True incomplete uploads for the given prefix are
81-- recursively listed.
116-- recursively listed.
82−listIncompleteUploads :: Bucket -> Maybe Text -> Bool
117+listIncompleteUploads ::
118+ Bucket ->
119+ Maybe Text ->
120+ Bool ->
83− -> C.ConduitM () UploadInfo Minio ()
121+ C.ConduitM () UploadInfo Minio ()
84listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
122listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
85 where
123 where
86 loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
124 loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
87 loop nextKeyMarker nextUploadIdMarker = do
125 loop nextKeyMarker nextUploadIdMarker = do
88− let
89− delimiter = bool (Just "/") Nothing recurse
126+ let delimiter = bool (Just "/") Nothing recurse
90−
127+
128+ res <-
129+ lift $
91− res <- lift $ listIncompleteUploads' bucket prefix delimiter
130+ listIncompleteUploads'
131+ bucket
132+ prefix
133+ delimiter
134+ nextKeyMarker
92− nextKeyMarker nextUploadIdMarker Nothing
135+ nextUploadIdMarker
93−
136+ Nothing
137+
138+ aggrSizes <- lift $
94− aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do
139+ forM (lurUploads res) $ \(uKey, uId, _) -> do
140+ partInfos <-
141+ C.runConduit $
95− partInfos <- C.runConduit $ listIncompleteParts bucket uKey uId
142+ listIncompleteParts bucket uKey uId
96− C..| CC.sinkList
143+ C..| CC.sinkList
97− return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
144+ return $ foldl' (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
98
145
99 CL.sourceList $
146 CL.sourceList $
147+ zipWith
148+ ( curry
100− map (\((uKey, uId, uInitTime), size) ->
149+ ( \((uKey, uId, uInitTime), size) ->
101− UploadInfo uKey uId uInitTime size
150+ UploadInfo uKey uId uInitTime size
151+ )
152+ )
102− ) $ zip (lurUploads res) aggrSizes
153+ (lurUploads res)
154+ aggrSizes
103
155
104 when (lurHasMore res) $
156 when (lurHasMore res) $
105 loop (lurNextKey res) (lurNextUpload res)
157 loop (lurNextKey res) (lurNextUpload res)
106
158
107−
108-- | List object parts of an ongoing multipart upload for given
159-- | List object parts of an ongoing multipart upload for given
109-- bucket, object and uploadId.
160-- bucket, object and uploadId.
110−listIncompleteParts :: Bucket -> Object -> UploadId
161+listIncompleteParts ::
162+ Bucket ->
163+ Object ->
164+ UploadId ->
111− -> C.ConduitM () ObjectPartInfo Minio ()
165+ C.ConduitM () ObjectPartInfo Minio ()
112listIncompleteParts bucket object uploadId = loop Nothing
166listIncompleteParts bucket object uploadId = loop Nothing
113 where
167 where
114 loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
168 loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
115 loop nextPartMarker = do
169 loop nextPartMarker = do
170+ res <-
171+ lift $
116− res <- lift $ listIncompleteParts' bucket object uploadId Nothing
172+ listIncompleteParts'
173+ bucket
174+ object
175+ uploadId
176+ Nothing
117− nextPartMarker
177+ nextPartMarker
118 CL.sourceList $ lprParts res
178 CL.sourceList $ lprParts res
119 when (lprHasMore res) $
179 when (lprHasMore res) $
120 loop (show <$> lprNextPart res)
180 loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs+215−152
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,45 +13,51 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16+{-# LANGUAGE CPP #-}
16
17
17module Network.Minio.PresignedOperations
18module Network.Minio.PresignedOperations
18− ( UrlExpiry
19+ ( UrlExpiry,
19− , makePresignedUrl
20+ makePresignedUrl,
20− , presignedPutObjectUrl
21+ presignedPutObjectUrl,
21− , presignedGetObjectUrl
22+ presignedGetObjectUrl,
22− , presignedHeadObjectUrl
23+ presignedHeadObjectUrl,
23−
24− , PostPolicyCondition(..)
24+ PostPolicyCondition (..),
25− , ppCondBucket
25+ ppCondBucket,
26− , ppCondContentLengthRange
26+ ppCondContentLengthRange,
27− , ppCondContentType
27+ ppCondContentType,
28− , ppCondKey
28+ ppCondKey,
29− , ppCondKeyStartsWith
29+ ppCondKeyStartsWith,
30− , ppCondSuccessActionStatus
30+ ppCondSuccessActionStatus,
31−
32− , PostPolicy(..)
31+ PostPolicy (..),
33− , PostPolicyError(..)
32+ PostPolicyError (..),
34− , newPostPolicy
33+ newPostPolicy,
35− , showPostPolicy
34+ showPostPolicy,
36− , presignedPostPolicy
35+ presignedPostPolicy,
36+ )
37− ) where
37+where
38−
38+
39−import Data.Aeson ((.=))
39+import Data.Aeson ((.=))
40−import qualified Data.Aeson as Json
40+import qualified Data.Aeson as Json
41−import Data.ByteString.Builder (byteString, toLazyByteString)
41+import Data.ByteString.Builder (byteString, toLazyByteString)
42−import qualified Data.Map.Strict as Map
42+import qualified Data.HashMap.Strict as H
43−import qualified Data.Text as T
43+import qualified Data.Text as T
44−import qualified Data.Time as Time
44+import qualified Data.Time as Time
45−import qualified Network.HTTP.Conduit as NC
46−import qualified Network.HTTP.Types as HT
47−import Network.HTTP.Types.Header (hHost)
48−
49−import Lib.Prelude
45+import Lib.Prelude
50−
46+import qualified Network.HTTP.Client as NClient
51−import Network.Minio.Data
47+import qualified Network.HTTP.Types as HT
48+import Network.Minio.API (buildRequest)
49+import Network.Minio.Credentials
50+import Network.Minio.Data
52−import Network.Minio.Data.Time
51+import Network.Minio.Data.Time
53−import Network.Minio.Errors
52+import Network.Minio.Errors
54−import Network.Minio.Sign.V4
53+import Network.Minio.Sign.V4
54+import Network.URI (uriToString)
55+
56+{- ORMOLU_DISABLE -}
57+#if MIN_VERSION_aeson(2,0,0)
58+import qualified Data.Aeson.Key as A
59+#endif
60+{- ORMOLU_ENABLE -}
55
61
56-- | Generate a presigned URL. This function allows for advanced usage
62-- | Generate a presigned URL. This function allows for advanced usage
57-- - for simple cases prefer the `presigned*Url` functions.
63-- - for simple cases prefer the `presigned*Url` functions.
@@ -61,42 +67,36 @@ import Network.Minio.Sign.V4
61--
67--
62-- All extra query parameters or headers are signed, and therefore are
68-- All extra query parameters or headers are signed, and therefore are
63-- required to be sent when the generated URL is actually used.
69-- required to be sent when the generated URL is actually used.
64−makePresignedUrl :: UrlExpiry -> HT.Method -> Maybe Bucket -> Maybe Object
70+makePresignedUrl ::
71+ UrlExpiry ->
72+ HT.Method ->
73+ Maybe Bucket ->
74+ Maybe Object ->
75+ Maybe Region ->
76+ HT.Query ->
65− -> Maybe Region -> HT.Query -> HT.RequestHeaders
77+ HT.RequestHeaders ->
66− -> Minio ByteString
78+ Minio ByteString
67makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
79makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
68− when (expiry > 7*24*3600 || expiry < 0) $
80+ when (expiry > 7 * 24 * 3600 || expiry < 0) $
81+ throwIO $
69− throwIO $ MErrVInvalidUrlExpiry expiry
82+ MErrVInvalidUrlExpiry expiry
70−
83+
71− ci <- asks mcConnInfo
72−
73− let
84+ let s3ri =
74− hostHeader = (hHost, getHostAddr ci)
85+ defaultS3ReqInfo
75− req = NC.defaultRequest {
86+ { riPresignExpirySecs = Just expiry,
76− NC.method = method
87+ riMethod = method,
77− , NC.secure = connectIsSecure ci
88+ riBucket = bucket,
78− , NC.host = encodeUtf8 $ connectHost ci
89+ riObject = object,
79− , NC.port = connectPort ci
90+ riRegion = region,
80− , NC.path = getS3Path bucket object
91+ riQueryParams = extraQuery,
81− , NC.requestHeaders = hostHeader : extraHeaders
92+ riHeaders = extraHeaders
82− , NC.queryString = HT.renderQuery True extraQuery
83− }
93+ }
84− ts <- liftIO Time.getCurrentTime
94+
85−
95+ req <- buildRequest s3ri
86− let sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
96+ let uri = NClient.getUri req
87− ts region (Just expiry) Nothing
97+ uriString = uriToString identity uri ""
88−
98+
89− signPairs = signV4 sp req
99+ return $ encodeUtf8 uriString
90−
91− qpToAdd = (fmap . fmap) Just signPairs
92− queryStr = HT.renderQueryBuilder True
93− ((HT.parseQuery $ NC.queryString req) ++ qpToAdd)
94− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
95−
96− return $ toS $ toLazyByteString $ scheme
97− <> byteString (getHostAddr ci)
98− <> byteString (getS3Path bucket object)
99− <> queryStr
100
100
101-- | Generate a URL with authentication signature to PUT (upload) an
101-- | Generate a URL with authentication signature to PUT (upload) an
102-- object. Any extra headers if passed, are signed, and so they are
102-- object. Any extra headers if passed, are signed, and so they are
@@ -105,12 +105,22 @@ makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
105--
105--
106-- For a list of possible headers to pass, please refer to the PUT
106-- For a list of possible headers to pass, please refer to the PUT
107-- object REST API AWS S3 documentation.
107-- object REST API AWS S3 documentation.
108−presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders
108+presignedPutObjectUrl ::
109+ Bucket ->
110+ Object ->
111+ UrlExpiry ->
112+ HT.RequestHeaders ->
109− -> Minio ByteString
113+ Minio ByteString
110presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
114presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
111 region <- asks (Just . connectRegion . mcConnInfo)
115 region <- asks (Just . connectRegion . mcConnInfo)
112− makePresignedUrl expirySeconds HT.methodPut
116+ makePresignedUrl
117+ expirySeconds
118+ HT.methodPut
119+ (Just bucket)
120+ (Just object)
121+ region
122+ []
113− (Just bucket) (Just object) region [] extraHeaders
123+ extraHeaders
114
124
115-- | Generate a URL with authentication signature to GET (download) an
125-- | Generate a URL with authentication signature to GET (download) an
116-- object. All extra query parameters and headers passed here will be
126-- object. All extra query parameters and headers passed here will be
@@ -121,12 +131,23 @@ presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
121--
131--
122-- For a list of possible request parameters and headers, please refer
132-- For a list of possible request parameters and headers, please refer
123-- to the GET object REST API AWS S3 documentation.
133-- to the GET object REST API AWS S3 documentation.
124−presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query
134+presignedGetObjectUrl ::
135+ Bucket ->
136+ Object ->
137+ UrlExpiry ->
138+ HT.Query ->
125− -> HT.RequestHeaders -> Minio ByteString
139+ HT.RequestHeaders ->
140+ Minio ByteString
126presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
141presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
127 region <- asks (Just . connectRegion . mcConnInfo)
142 region <- asks (Just . connectRegion . mcConnInfo)
128− makePresignedUrl expirySeconds HT.methodGet
143+ makePresignedUrl
144+ expirySeconds
145+ HT.methodGet
146+ (Just bucket)
147+ (Just object)
148+ region
149+ extraQuery
129− (Just bucket) (Just object) region extraQuery extraHeaders
150+ extraHeaders
130
151
131-- | Generate a URL with authentication signature to make a HEAD
152-- | Generate a URL with authentication signature to make a HEAD
132-- request on an object. This is used to fetch metadata about an
153-- request on an object. This is used to fetch metadata about an
@@ -135,50 +156,74 @@ presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
135--
156--
136-- For a list of possible headers to pass, please refer to the HEAD
157-- For a list of possible headers to pass, please refer to the HEAD
137-- object REST API AWS S3 documentation.
158-- object REST API AWS S3 documentation.
138−presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry
159+presignedHeadObjectUrl ::
160+ Bucket ->
161+ Object ->
162+ UrlExpiry ->
139− -> HT.RequestHeaders -> Minio ByteString
163+ HT.RequestHeaders ->
164+ Minio ByteString
140presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
165presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
141 region <- asks (Just . connectRegion . mcConnInfo)
166 region <- asks (Just . connectRegion . mcConnInfo)
142− makePresignedUrl expirySeconds HT.methodHead
167+ makePresignedUrl
168+ expirySeconds
169+ HT.methodHead
170+ (Just bucket)
171+ (Just object)
172+ region
173+ []
143− (Just bucket) (Just object) region [] extraHeaders
174+ extraHeaders
144
175
145-- | Represents individual conditions in a Post Policy document.
176-- | Represents individual conditions in a Post Policy document.
146−data PostPolicyCondition = PPCStartsWith Text Text
177+data PostPolicyCondition
178+ = PPCStartsWith Text Text
147− | PPCEquals Text Text
179+ | PPCEquals Text Text
148− | PPCRange Text Int64 Int64
180+ | PPCRange Text Int64 Int64
149− deriving (Show, Eq)
181+ deriving stock (Show, Eq)
150
182
183+{- ORMOLU_DISABLE -}
151instance Json.ToJSON PostPolicyCondition where
184instance Json.ToJSON PostPolicyCondition where
152 toJSON (PPCStartsWith k v) = Json.toJSON ["starts-with", k, v]
185 toJSON (PPCStartsWith k v) = Json.toJSON ["starts-with", k, v]
186+#if MIN_VERSION_aeson(2,0,0)
187+ toJSON (PPCEquals k v) = Json.object [(A.fromText k) .= v]
188+#else
153 toJSON (PPCEquals k v) = Json.object [k .= v]
189 toJSON (PPCEquals k v) = Json.object [k .= v]
190+#endif
154 toJSON (PPCRange k minVal maxVal) =
191 toJSON (PPCRange k minVal maxVal) =
155 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
192 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
156
193
157 toEncoding (PPCStartsWith k v) = Json.foldable ["starts-with", k, v]
194 toEncoding (PPCStartsWith k v) = Json.foldable ["starts-with", k, v]
195+#if MIN_VERSION_aeson(2,0,0)
196+ toEncoding (PPCEquals k v) = Json.pairs ((A.fromText k) .= v)
197+#else
158 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
198 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
199+#endif
159 toEncoding (PPCRange k minVal maxVal) =
200 toEncoding (PPCRange k minVal maxVal) =
160 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
201 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202+{- ORMOLU_ENABLE -}
161
203
162-- | A PostPolicy is required to perform uploads via browser forms.
204-- | A PostPolicy is required to perform uploads via browser forms.
163−data PostPolicy = PostPolicy {
205+data PostPolicy = PostPolicy
164− expiration :: UTCTime
206+ { expiration :: UTCTime,
165− , conditions :: [PostPolicyCondition]
207+ conditions :: [PostPolicyCondition]
208+ }
166− } deriving (Show, Eq)
209+ deriving stock (Show, Eq)
167
210
168instance Json.ToJSON PostPolicy where
211instance Json.ToJSON PostPolicy where
169 toJSON (PostPolicy e c) =
212 toJSON (PostPolicy e c) =
213+ Json.object
170− Json.object $ [ "expiration" .= iso8601TimeFormat e
214+ [ "expiration" .= iso8601TimeFormat e,
171− , "conditions" .= c
215+ "conditions" .= c
172− ]
216+ ]
173 toEncoding (PostPolicy e c) =
217 toEncoding (PostPolicy e c) =
174 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
218 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
175
219
176-- | Possible validation errors when creating a PostPolicy.
220-- | Possible validation errors when creating a PostPolicy.
177−data PostPolicyError = PPEKeyNotSpecified
221+data PostPolicyError
222+ = PPEKeyNotSpecified
178− | PPEBucketNotSpecified
223+ | PPEBucketNotSpecified
179− | PPEConditionKeyEmpty
224+ | PPEConditionKeyEmpty
180− | PPERangeInvalid
225+ | PPERangeInvalid
181− deriving (Eq, Show)
226+ deriving stock (Show, Eq)
182
227
183-- | Set the bucket name that the upload should use.
228-- | Set the bucket name that the upload should use.
184ppCondBucket :: Bucket -> PostPolicyCondition
229ppCondBucket :: Bucket -> PostPolicyCondition
@@ -186,8 +231,10 @@ ppCondBucket = PPCEquals "bucket"
186
231
187-- | Set the content length range constraint with minimum and maximum
232-- | Set the content length range constraint with minimum and maximum
188-- byte count values.
233-- byte count values.
189−ppCondContentLengthRange :: Int64 -> Int64
234+ppCondContentLengthRange ::
235+ Int64 ->
236+ Int64 ->
190− -> PostPolicyCondition
237+ PostPolicyCondition
191ppCondContentLengthRange = PPCRange "content-length-range"
238ppCondContentLengthRange = PPCRange "content-length-range"
192
239
193-- | Set the content-type header for the upload.
240-- | Set the content-type header for the upload.
@@ -210,83 +257,99 @@ ppCondSuccessActionStatus n =
210
257
211-- | This function creates a PostPolicy after validating its
258-- | This function creates a PostPolicy after validating its
212-- arguments.
259-- arguments.
260+newPostPolicy ::
261+ UTCTime ->
213−newPostPolicy :: UTCTime -> [PostPolicyCondition]
262+ [PostPolicyCondition] ->
214− -> Either PostPolicyError PostPolicy
263+ Either PostPolicyError PostPolicy
215newPostPolicy expirationTime conds
264newPostPolicy expirationTime conds
216 -- object name condition must be present
265 -- object name condition must be present
217 | not $ any (keyEquals "key") conds =
266 | not $ any (keyEquals "key") conds =
218 Left PPEKeyNotSpecified
267 Left PPEKeyNotSpecified
219−
220 -- bucket name condition must be present
268 -- bucket name condition must be present
221 | not $ any (keyEquals "bucket") conds =
269 | not $ any (keyEquals "bucket") conds =
222 Left PPEBucketNotSpecified
270 Left PPEBucketNotSpecified
223−
224 -- a condition with an empty key is invalid
271 -- a condition with an empty key is invalid
225 | any (keyEquals "") conds || any isEmptyRangeKey conds =
272 | any (keyEquals "") conds || any isEmptyRangeKey conds =
226 Left PPEConditionKeyEmpty
273 Left PPEConditionKeyEmpty
227−
228 -- invalid range check
274 -- invalid range check
229 | any isInvalidRange conds =
275 | any isInvalidRange conds =
230 Left PPERangeInvalid
276 Left PPERangeInvalid
231−
232 -- all good!
277 -- all good!
233 | otherwise =
278 | otherwise =
234 return $ PostPolicy expirationTime conds
279 return $ PostPolicy expirationTime conds
235−
236 where
280 where
237 keyEquals k' (PPCStartsWith k _) = k == k'
281 keyEquals k' (PPCStartsWith k _) = k == k'
238− keyEquals k' (PPCEquals k _) = k == k'
282+ keyEquals k' (PPCEquals k _) = k == k'
239− keyEquals _ _ = False
283+ keyEquals _ _ = False
240−
241 isEmptyRangeKey (PPCRange k _ _) = k == ""
284 isEmptyRangeKey (PPCRange k _ _) = k == ""
242− isEmptyRangeKey _ = False
285+ isEmptyRangeKey _ = False
243−
244 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
286 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
245− isInvalidRange _ = False
287+ isInvalidRange _ = False
246
288
247-- | Convert Post Policy to a string (e.g. for printing).
289-- | Convert Post Policy to a string (e.g. for printing).
248showPostPolicy :: PostPolicy -> ByteString
290showPostPolicy :: PostPolicy -> ByteString
249−showPostPolicy = toS . Json.encode
291+showPostPolicy = toStrictBS . Json.encode
250
292
251-- | Generate a presigned URL and POST policy to upload files via a
293-- | Generate a presigned URL and POST policy to upload files via a
252-- browser. On success, this function returns a URL and POST
294-- browser. On success, this function returns a URL and POST
253-- form-data.
295-- form-data.
254−presignedPostPolicy :: PostPolicy
296+presignedPostPolicy ::
297+ PostPolicy ->
255− -> Minio (ByteString, Map.Map Text ByteString)
298+ Minio (ByteString, H.HashMap Text ByteString)
256presignedPostPolicy p = do
299presignedPostPolicy p = do
257 ci <- asks mcConnInfo
300 ci <- asks mcConnInfo
258− signTime <- liftIO $ Time.getCurrentTime
301+ signTime <- liftIO Time.getCurrentTime
259−
302+ mgr <- asks mcConnManager
260− let
303+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
304+
261− extraConditions =
305+ let extraConditions signParams =
262− [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime)
306+ [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
263− , PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256"
307+ PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
308+ PPCEquals
264− , PPCEquals "x-amz-credential"
309+ "x-amz-credential"
265− (T.intercalate "/" [connectAccessKey ci,
310+ ( T.intercalate
266− decodeUtf8 $ mkScope signTime region])
311+ "/"
312+ [ coerce $ cvAccessKey cv,
313+ decodeUtf8 $ credentialScope signParams
314+ ]
315+ )
267− ]
316+ ]
268− ppWithCreds = p {
317+ ppWithCreds signParams =
318+ p
269− conditions = conditions p ++ extraConditions
319+ { conditions = conditions p ++ extraConditions signParams
270− }
320+ }
271− sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
321+ sp =
322+ SignParams
323+ (coerce $ cvAccessKey cv)
324+ (coerce $ cvSecretKey cv)
325+ (coerce $ cvSessionToken cv)
326+ ServiceS3
327+ signTime
272− signTime (Just $ connectRegion ci) Nothing Nothing
328+ (Just $ connectRegion ci)
329+ Nothing
330+ Nothing
273− signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
331+ signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
274−
275−
276− -- compute form-data
332+ -- compute form-data
277− mkPair (PPCStartsWith k v) = Just (k, v)
333+ mkPair (PPCStartsWith k v) = Just (k, v)
278− mkPair (PPCEquals k v) = Just (k, v)
334+ mkPair (PPCEquals k v) = Just (k, v)
279− mkPair _ = Nothing
335+ mkPair _ = Nothing
280− formFromPolicy = Map.map toS $ Map.fromList $ catMaybes $
336+ formFromPolicy =
281− mkPair <$> conditions ppWithCreds
337+ H.map encodeUtf8 $
338+ H.fromList $
339+ mapMaybe
340+ mkPair
341+ (conditions $ ppWithCreds sp)
282− formData = formFromPolicy `Map.union` signData
342+ formData = formFromPolicy `H.union` signData
283−
284− -- compute POST upload URL
343+ -- compute POST upload URL
285− bucket = Map.findWithDefault "" "bucket" formData
344+ bucket = H.lookupDefault "" "bucket" formData
286− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
345+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
287− region = connectRegion ci
346+ url =
288−
347+ toStrictBS $
348+ toLazyByteString $
349+ scheme
289− url = toS $ toLazyByteString $ scheme <> byteString (getHostAddr ci) <>
350+ <> byteString (getHostAddr ci)
351+ <> byteString "/"
290− byteString "/" <> byteString (toS bucket) <> byteString "/"
352+ <> byteString bucket
353+ <> byteString "/"
291
354
292 return (url, formData)
355 return (url, formData)
src/Network/Minio/PutObject.hs+85−69
@@ -15,29 +15,24 @@
15--
15--
16
16
17module Network.Minio.PutObject
17module Network.Minio.PutObject
18− (
19− putObjectInternal
18+ ( putObjectInternal,
20− , ObjectData(..)
19+ ObjectData (..),
21− , selectPartSizes
20+ selectPartSizes,
21+ )
22− ) where
22+where
23−
23+
24−
25−import Conduit (takeC)
24+import Conduit (takeC)
26−import qualified Conduit as C
25+import qualified Conduit as C
27−import qualified Data.ByteString.Lazy as LBS
26+import qualified Data.ByteString.Lazy as LBS
28−import qualified Data.Conduit.Binary as CB
27+import qualified Data.Conduit.Binary as CB
29import qualified Data.Conduit.Combinators as CC
28import qualified Data.Conduit.Combinators as CC
30−import qualified Data.Conduit.List as CL
29+import qualified Data.Conduit.List as CL
31−import qualified Data.List as List
30+import qualified Data.List as List
32−
33−
34−import Lib.Prelude
31+import Lib.Prelude
35−
36−import Network.Minio.Data
32+import Network.Minio.Data
37−import Network.Minio.Errors
33+import Network.Minio.Errors
38−import Network.Minio.S3API
34+import Network.Minio.S3API
39−import Network.Minio.Utils
35+import Network.Minio.Utils
40−
41
36
42-- | A data-type to represent the source data for an object. A
37-- | A data-type to represent the source data for an object. A
43-- file-path or a producer-conduit may be provided.
38-- file-path or a producer-conduit may be provided.
@@ -50,37 +45,45 @@ import Network.Minio.Utils
50-- the input - if it is not provided, upload will continue until the
45-- the input - if it is not provided, upload will continue until the
51-- stream ends or the object reaches `maxObjectSize` size.
46-- stream ends or the object reaches `maxObjectSize` size.
52data ObjectData m
47data ObjectData m
53− = ODFile FilePath (Maybe Int64) -- ^ Takes filepath and optional
48+ = -- | Takes filepath and optional
54− -- size.
49+ -- size.
50+ ODFile FilePath (Maybe Int64)
51+ | -- | Pass
52+ -- size
53+ -- (bytes)
54+ -- if
55+ -- known.
55− | ODStream (C.ConduitM () ByteString m ()) (Maybe Int64) -- ^ Pass
56+ ODStream (C.ConduitM () ByteString m ()) (Maybe Int64)
56− -- size
57− -- (bytes)
58− -- if
59− -- known.
60
57
61-- | Put an object from ObjectData. This high-level API handles
58-- | Put an object from ObjectData. This high-level API handles
62-- objects of all sizes, and even if the object size is unknown.
59-- objects of all sizes, and even if the object size is unknown.
63−putObjectInternal :: Bucket -> Object -> PutObjectOptions
60+putObjectInternal ::
61+ Bucket ->
62+ Object ->
63+ PutObjectOptions ->
64− -> ObjectData Minio -> Minio ETag
64+ ObjectData Minio ->
65+ Minio ETag
65putObjectInternal b o opts (ODStream src sizeMay) = do
66putObjectInternal b o opts (ODStream src sizeMay) = do
66 case sizeMay of
67 case sizeMay of
67 -- unable to get size, so assume non-seekable file
68 -- unable to get size, so assume non-seekable file
68 Nothing -> sequentialMultipartUpload b o opts Nothing src
69 Nothing -> sequentialMultipartUpload b o opts Nothing src
69−
70 -- got file size, so check for single/multipart upload
70 -- got file size, so check for single/multipart upload
71 Just size ->
71 Just size ->
72+ if
72− if | size <= 64 * oneMiB -> do
73+ | size <= 64 * oneMiB -> do
73− bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
74+ bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
74− putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
75+ putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
75− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
76+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
76− | otherwise -> sequentialMultipartUpload b o opts (Just size) src
77+ | otherwise -> sequentialMultipartUpload b o opts (Just size) src
77−
78putObjectInternal b o opts (ODFile fp sizeMay) = do
78putObjectInternal b o opts (ODFile fp sizeMay) = do
79 hResE <- withNewHandle fp $ \h ->
79 hResE <- withNewHandle fp $ \h ->
80− liftM2 (,) (isHandleSeekable h) (getFileSize h)
80+ liftA2 (,) (isHandleSeekable h) (getFileSize h)
81
81
82− (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return
82+ (isSeekable, handleSizeMay) <-
83+ either
84+ (const $ return (False, Nothing))
85+ return
83− hResE
86+ hResE
84
87
85 -- prefer given size to queried size.
88 -- prefer given size to queried size.
86 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
89 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
@@ -88,18 +91,25 @@ putObjectInternal b o opts (ODFile fp sizeMay) = do
88 case finalSizeMay of
91 case finalSizeMay of
89 -- unable to get size, so assume non-seekable file
92 -- unable to get size, so assume non-seekable file
90 Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
93 Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
91−
92 -- got file size, so check for single/multipart upload
94 -- got file size, so check for single/multipart upload
93 Just size ->
95 Just size ->
96+ if
97+ | size <= 64 * oneMiB ->
94− if | size <= 64 * oneMiB -> either throwIO return =<<
98+ either throwIO return
95− withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
99+ =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
96− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
100+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
97− | isSeekable -> parallelMultipartUpload b o opts fp size
101+ | isSeekable -> parallelMultipartUpload b o opts fp size
102+ | otherwise ->
98− | otherwise -> sequentialMultipartUpload b o opts (Just size) $
103+ sequentialMultipartUpload b o opts (Just size) $
99− CB.sourceFile fp
104+ CB.sourceFile fp
100−
105+
101−parallelMultipartUpload :: Bucket -> Object -> PutObjectOptions
106+parallelMultipartUpload ::
107+ Bucket ->
108+ Object ->
109+ PutObjectOptions ->
110+ FilePath ->
111+ Int64 ->
102− -> FilePath -> Int64 -> Minio ETag
112+ Minio ETag
103parallelMultipartUpload b o opts filePath size = do
113parallelMultipartUpload b o opts filePath size = do
104 -- get a new upload id.
114 -- get a new upload id.
105 uploadId <- newMultipartUpload b o (pooToHeaders opts)
115 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -109,15 +119,17 @@ parallelMultipartUpload b o opts filePath size = do
109 let threads = fromMaybe 10 $ pooNumThreads opts
119 let threads = fromMaybe 10 $ pooNumThreads opts
110
120
111 -- perform upload with 'threads' threads
121 -- perform upload with 'threads' threads
122+ uploadedPartsE <-
112− uploadedPartsE <- limitedMapConcurrently (fromIntegral threads)
123+ limitedMapConcurrently
124+ (fromIntegral threads)
113− (uploadPart uploadId) partSizeInfo
125+ (uploadPart uploadId)
126+ partSizeInfo
114
127
115 -- if there were any errors, rethrow exception.
128 -- if there were any errors, rethrow exception.
116 mapM_ throwIO $ lefts uploadedPartsE
129 mapM_ throwIO $ lefts uploadedPartsE
117
130
118 -- if we get here, all parts were successfully uploaded.
131 -- if we get here, all parts were successfully uploaded.
119 completeMultipartUpload b o uploadId $ rights uploadedPartsE
132 completeMultipartUpload b o uploadId $ rights uploadedPartsE
120−
121 where
133 where
122 uploadPart uploadId (partNum, offset, sz) =
134 uploadPart uploadId (partNum, offset, sz) =
123 withNewHandle filePath $ \h -> do
135 withNewHandle filePath $ \h -> do
@@ -125,10 +137,13 @@ parallelMultipartUpload b o opts filePath size = do
125 putObjectPart b o uploadId partNum [] payload
137 putObjectPart b o uploadId partNum [] payload
126
138
127-- | Upload multipart object from conduit source sequentially
139-- | Upload multipart object from conduit source sequentially
128−sequentialMultipartUpload :: Bucket -> Object -> PutObjectOptions
140+sequentialMultipartUpload ::
141+ Bucket ->
142+ Object ->
143+ PutObjectOptions ->
129− -> Maybe Int64
144+ Maybe Int64 ->
130− -> C.ConduitM () ByteString Minio ()
145+ C.ConduitM () ByteString Minio () ->
131− -> Minio ETag
146+ Minio ETag
132sequentialMultipartUpload b o opts sizeMay src = do
147sequentialMultipartUpload b o opts sizeMay src = do
133 -- get a new upload id.
148 -- get a new upload id.
134 uploadId <- newMultipartUpload b o (pooToHeaders opts)
149 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -136,22 +151,23 @@ sequentialMultipartUpload b o opts sizeMay src = do
136 -- upload parts in loop
151 -- upload parts in loop
137 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
152 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
138 (pnums, _, sizes) = List.unzip3 partSizes
153 (pnums, _, sizes) = List.unzip3 partSizes
139− uploadedParts <- C.runConduit
154+ uploadedParts <-
155+ C.runConduit $
140− $ src
156+ src
141− C..| chunkBSConduit (map fromIntegral sizes)
157+ C..| chunkBSConduit (map fromIntegral sizes)
142− C..| CL.map PayloadBS
158+ C..| CL.map PayloadBS
143− C..| uploadPart' uploadId pnums
159+ C..| uploadPart' uploadId pnums
144− C..| CC.sinkList
160+ C..| CC.sinkList
145
161
146 -- complete multipart upload
162 -- complete multipart upload
147 completeMultipartUpload b o uploadId uploadedParts
163 completeMultipartUpload b o uploadId uploadedParts
148−
149 where
164 where
150 uploadPart' _ [] = return ()
165 uploadPart' _ [] = return ()
151− uploadPart' uid (pn:pns) = do
166+ uploadPart' uid (pn : pns) = do
152 payloadMay <- C.await
167 payloadMay <- C.await
153 case payloadMay of
168 case payloadMay of
154 Nothing -> return ()
169 Nothing -> return ()
170+ Just payload -> do
155− Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload
171+ pinfo <- lift $ putObjectPart b o uid pn [] payload
156− C.yield pinfo
172+ C.yield pinfo
157− uploadPart' uid pns
173+ uploadPart' uid pns
src/Network/Minio/S3API.hs+486−314
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,133 +14,185 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17+-- |
18+-- Module: Network.Minio.S3API
19+-- Copyright: (c) 2017-2023 MinIO Dev Team
20+-- License: Apache 2.0
21+-- Maintainer: MinIO Dev Team <dev@min.io>
22+--
23+-- Lower-level API for S3 compatible object stores. Start with @Network.Minio@
24+-- and use this only if needed.
17module Network.Minio.S3API
25module Network.Minio.S3API
18− (
19− Region
26+ ( Region,
20− , getLocation
27+ getLocation,
21−
28+
22− -- * Listing buckets
29+ -- * Listing buckets
30+
23− --------------------
31+ --------------------
24− , getService
32+ getService,
25−
33+
26− -- * Listing objects
34+ -- * Listing objects
35+
27− --------------------
36+ --------------------
28− , ListObjectsResult(..)
37+ ListObjectsResult (..),
29− , ListObjectsV1Result(..)
38+ ListObjectsV1Result (..),
30− , listObjects'
39+ listObjects',
31− , listObjectsV1'
40+ listObjectsV1',
32−
41+
33− -- * Retrieving buckets
42+ -- * Retrieving buckets
34− , headBucket
43+ headBucket,
35−
44+
36− -- * Retrieving objects
45+ -- * Retrieving objects
46+
37− -----------------------
47+ -----------------------
38− , getObject'
48+ getObject',
39− , headObject
49+ headObject,
40−
50+
41− -- * Creating buckets and objects
51+ -- * Creating buckets and objects
52+
42− ---------------------------------
53+ ---------------------------------
43− , putBucket
54+ putBucket,
44− , ETag
55+ ETag,
45− , maxSinglePutObjectSizeBytes
56+ maxSinglePutObjectSizeBytes,
46− , putObjectSingle'
57+ putObjectSingle',
47− , putObjectSingle
58+ putObjectSingle,
48− , copyObjectSingle
59+ copyObjectSingle,
49−
60+
50− -- * Multipart Upload APIs
61+ -- * Multipart Upload APIs
62+
51− --------------------------
63+ --------------------------
52− , UploadId
64+ UploadId,
53− , PartTuple
65+ PartTuple,
54− , Payload(..)
66+ Payload (..),
55− , PartNumber
67+ PartNumber,
56− , newMultipartUpload
68+ newMultipartUpload,
57− , putObjectPart
69+ putObjectPart,
58− , copyObjectPart
70+ copyObjectPart,
59− , completeMultipartUpload
71+ completeMultipartUpload,
60− , abortMultipartUpload
72+ abortMultipartUpload,
61− , ListUploadsResult(..)
73+ ListUploadsResult (..),
62− , listIncompleteUploads'
74+ listIncompleteUploads',
63− , ListPartsResult(..)
75+ ListPartsResult (..),
64− , listIncompleteParts'
76+ listIncompleteParts',
65−
77+
66− -- * Deletion APIs
78+ -- * Deletion APIs
79+
67− --------------------------
80+ --------------------------
68− , deleteBucket
81+ deleteBucket,
69− , deleteObject
82+ deleteObject,
70−
83+
71− -- * Presigned Operations
84+ -- * Presigned Operations
85+
72− -----------------------------
86+ -----------------------------
73− , module Network.Minio.PresignedOperations
87+ module Network.Minio.PresignedOperations,
74−
88+
75− -- ** Bucket Policies
89+ -- ** Bucket Policies
76− , getBucketPolicy
90+ getBucketPolicy,
77− , setBucketPolicy
91+ setBucketPolicy,
78−
92+
79− -- * Bucket Notifications
93+ -- * Bucket Notifications
94+
80− -------------------------
95+ -------------------------
81− , Notification(..)
96+ Notification (..),
82− , NotificationConfig(..)
97+ NotificationConfig (..),
83− , Arn
98+ Arn,
84− , Event(..)
99+ Event (..),
85− , Filter(..)
100+ Filter (..),
86− , FilterKey(..)
101+ FilterKey (..),
87− , FilterRules(..)
102+ FilterRules (..),
88− , FilterRule(..)
103+ FilterRule (..),
89− , getBucketNotification
104+ getBucketNotification,
90− , putBucketNotification
105+ putBucketNotification,
91− , removeAllBucketNotification
106+ removeAllBucketNotification,
107+ )
92− ) where
108+where
93−
109+
94−import qualified Conduit as C
95−import qualified Data.ByteString as BS
110+import qualified Data.ByteString as BS
96−import qualified Data.Text as T
111+import qualified Data.Text as T
112+import Lib.Prelude
97−import qualified Network.HTTP.Conduit as NC
113+import qualified Network.HTTP.Conduit as NC
98−import qualified Network.HTTP.Types as HT
114+import qualified Network.HTTP.Types as HT
99−import Network.HTTP.Types.Status (status404)
115+import Network.HTTP.Types.Status (status404)
100−import UnliftIO (Handler (Handler))
101−
102−import Lib.Prelude
103−
104−import Network.Minio.API
116+import Network.Minio.API
105−import Network.Minio.APICommon
117+import Network.Minio.APICommon
106−import Network.Minio.Data
118+import Network.Minio.Data
107−import Network.Minio.Errors
119+import Network.Minio.Errors
108−import Network.Minio.PresignedOperations
120+import Network.Minio.PresignedOperations
109−import Network.Minio.Utils
121+import Network.Minio.Utils
110−import Network.Minio.XmlGenerator
122+import Network.Minio.XmlGenerator
111−import Network.Minio.XmlParser
123+import Network.Minio.XmlParser
124+import UnliftIO (Handler (Handler))
112
125
113-- | Fetch all buckets from the service.
126-- | Fetch all buckets from the service.
114getService :: Minio [BucketInfo]
127getService :: Minio [BucketInfo]
115getService = do
128getService = do
129+ resp <-
130+ executeRequest $
116− resp <- executeRequest $ defaultS3ReqInfo {
131+ defaultS3ReqInfo
117− riNeedsLocation = False
132+ { riNeedsLocation = False
118− }
133+ }
119 parseListBuckets $ NC.responseBody resp
134 parseListBuckets $ NC.responseBody resp
120
135
136+-- Parse headers from getObject and headObject calls.
137+parseGetObjectHeaders :: Object -> [HT.Header] -> Maybe ObjectInfo
138+parseGetObjectHeaders object headers =
139+ let metadataPairs = getMetadata headers
140+ userMetadata = getUserMetadataMap metadataPairs
141+ metadata = getNonUserMetadataMap metadataPairs
142+ in ObjectInfo
143+ <$> Just object
144+ <*> getLastModifiedHeader headers
145+ <*> getETagHeader headers
146+ <*> getContentLength headers
147+ <*> Just userMetadata
148+ <*> Just metadata
149+
121−-- | GET an object from the service and return the response headers
150+-- | GET an object from the service and return parsed ObjectInfo and a
122−-- and a conduit source for the object content
151+-- conduit source for the object content
123−getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
152+getObject' ::
124− -> Minio ([HT.Header], C.ConduitM () ByteString Minio ())
153+ Bucket ->
154+ Object ->
155+ HT.Query ->
156+ [HT.Header] ->
157+ Minio GetObjectResponse
125getObject' bucket object queryParams headers = do
158getObject' bucket object queryParams headers = do
126 resp <- mkStreamRequest reqInfo
159 resp <- mkStreamRequest reqInfo
160+ let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
161+ objInfo <-
162+ maybe
163+ (throwIO MErrVInvalidObjectInfoResponse)
164+ return
165+ objInfoMaybe
166+ return $
167+ GetObjectResponse
168+ { gorObjectInfo = objInfo,
127− return (NC.responseHeaders resp, NC.responseBody resp)
169+ gorObjectStream = NC.responseBody resp
170+ }
128 where
171 where
172+ reqInfo =
173+ defaultS3ReqInfo
129− reqInfo = defaultS3ReqInfo { riBucket = Just bucket
174+ { riBucket = Just bucket,
130− , riObject = Just object
175+ riObject = Just object,
131− , riQueryParams = queryParams
176+ riQueryParams = queryParams,
132− , riHeaders = headers
177+ riHeaders =
178+ headers
179+ -- This header is required for safety as otherwise http-client,
180+ -- sends Accept-Encoding: gzip, and the server may actually gzip
181+ -- body. In that case Content-Length header will be missing.
182+ <> [("Accept-Encoding", "identity")]
133− }
183+ }
134
184
135-- | Creates a bucket via a PUT bucket call.
185-- | Creates a bucket via a PUT bucket call.
136putBucket :: Bucket -> Region -> Minio ()
186putBucket :: Bucket -> Region -> Minio ()
137putBucket bucket location = do
187putBucket bucket location = do
138 ns <- asks getSvcNamespace
188 ns <- asks getSvcNamespace
189+ void $
139− void $ executeRequest $
190+ executeRequest $
191+ defaultS3ReqInfo
140− defaultS3ReqInfo { riMethod = HT.methodPut
192+ { riMethod = HT.methodPut,
141− , riBucket = Just bucket
193+ riBucket = Just bucket,
142− , riPayload = PayloadBS $ mkCreateBucketConfig ns location
194+ riPayload = PayloadBS $ mkCreateBucketConfig ns location,
143− , riNeedsLocation = False
195+ riNeedsLocation = False
144 }
196 }
145
197
146-- | Single PUT object size.
198-- | Single PUT object size.
@@ -156,321 +208,432 @@ putObjectSingle' bucket object headers bs = do
156 let size = fromIntegral (BS.length bs)
208 let size = fromIntegral (BS.length bs)
157 -- check length is within single PUT object size.
209 -- check length is within single PUT object size.
158 when (size > maxSinglePutObjectSizeBytes) $
210 when (size > maxSinglePutObjectSizeBytes) $
211+ throwIO $
159− throwIO $ MErrVSinglePUTSizeExceeded size
212+ MErrVSinglePUTSizeExceeded size
160
213
161 let payload = mkStreamingPayload $ PayloadBS bs
214 let payload = mkStreamingPayload $ PayloadBS bs
215+ resp <-
162− resp <- executeRequest $
216+ executeRequest $
217+ defaultS3ReqInfo
163− defaultS3ReqInfo { riMethod = HT.methodPut
218+ { riMethod = HT.methodPut,
164− , riBucket = Just bucket
219+ riBucket = Just bucket,
165− , riObject = Just object
220+ riObject = Just object,
166− , riHeaders = headers
221+ riHeaders = headers,
167− , riPayload = payload
222+ riPayload = payload
168− }
223+ }
169
224
170 let rheaders = NC.responseHeaders resp
225 let rheaders = NC.responseHeaders resp
171 etag = getETagHeader rheaders
226 etag = getETagHeader rheaders
172 maybe
227 maybe
173 (throwIO MErrVETagHeaderNotFound)
228 (throwIO MErrVETagHeaderNotFound)
174− return etag
229+ return
230+ etag
175
231
176-- | PUT an object into the service. This function performs a single
232-- | PUT an object into the service. This function performs a single
177-- PUT object call, and so can only transfer objects upto 5GiB.
233-- PUT object call, and so can only transfer objects upto 5GiB.
178−putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
234+putObjectSingle ::
235+ Bucket ->
236+ Object ->
237+ [HT.Header] ->
238+ Handle ->
239+ Int64 ->
240+ Int64 ->
179− -> Int64 -> Minio ETag
241+ Minio ETag
180putObjectSingle bucket object headers h offset size = do
242putObjectSingle bucket object headers h offset size = do
181 -- check length is within single PUT object size.
243 -- check length is within single PUT object size.
182 when (size > maxSinglePutObjectSizeBytes) $
244 when (size > maxSinglePutObjectSizeBytes) $
245+ throwIO $
183− throwIO $ MErrVSinglePUTSizeExceeded size
246+ MErrVSinglePUTSizeExceeded size
184
247
185 -- content-length header is automatically set by library.
248 -- content-length header is automatically set by library.
186 let payload = mkStreamingPayload $ PayloadH h offset size
249 let payload = mkStreamingPayload $ PayloadH h offset size
250+ resp <-
187− resp <- executeRequest $
251+ executeRequest $
252+ defaultS3ReqInfo
188− defaultS3ReqInfo { riMethod = HT.methodPut
253+ { riMethod = HT.methodPut,
189− , riBucket = Just bucket
254+ riBucket = Just bucket,
190− , riObject = Just object
255+ riObject = Just object,
191− , riHeaders = headers
256+ riHeaders = headers,
192− , riPayload = payload
257+ riPayload = payload
193− }
258+ }
194
259
195 let rheaders = NC.responseHeaders resp
260 let rheaders = NC.responseHeaders resp
196 etag = getETagHeader rheaders
261 etag = getETagHeader rheaders
197 maybe
262 maybe
198 (throwIO MErrVETagHeaderNotFound)
263 (throwIO MErrVETagHeaderNotFound)
199− return etag
264+ return
265+ etag
200
266
201-- | List objects in a bucket matching prefix up to delimiter,
267-- | List objects in a bucket matching prefix up to delimiter,
202-- starting from nextMarker.
268-- starting from nextMarker.
203−listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
269+listObjectsV1' ::
270+ Bucket ->
271+ Maybe Text ->
272+ Maybe Text ->
273+ Maybe Text ->
274+ Maybe Int ->
204− -> Minio ListObjectsV1Result
275+ Minio ListObjectsV1Result
205listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
276listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
277+ resp <-
278+ executeRequest $
279+ defaultS3ReqInfo
206− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
280+ { riMethod = HT.methodGet,
207− , riBucket = Just bucket
281+ riBucket = Just bucket,
208− , riQueryParams = mkOptionalParams params
282+ riQueryParams = mkOptionalParams params
209− }
283+ }
210 parseListObjectsV1Response $ NC.responseBody resp
284 parseListObjectsV1Response $ NC.responseBody resp
211 where
285 where
212− params = [
286+ params =
213− ("marker", nextMarker)
287+ [ ("marker", nextMarker),
214− , ("prefix", prefix)
288+ ("prefix", prefix),
215− , ("delimiter", delimiter)
289+ ("delimiter", delimiter),
216− , ("max-keys", show <$> maxKeys)
290+ ("max-keys", show <$> maxKeys)
217 ]
291 ]
218
292
219-- | List objects in a bucket matching prefix up to delimiter,
293-- | List objects in a bucket matching prefix up to delimiter,
220-- starting from nextToken.
294-- starting from nextToken.
221−listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
295+listObjects' ::
296+ Bucket ->
297+ Maybe Text ->
298+ Maybe Text ->
299+ Maybe Text ->
300+ Maybe Int ->
222− -> Minio ListObjectsResult
301+ Minio ListObjectsResult
223listObjects' bucket prefix nextToken delimiter maxKeys = do
302listObjects' bucket prefix nextToken delimiter maxKeys = do
303+ resp <-
304+ executeRequest $
305+ defaultS3ReqInfo
224− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
306+ { riMethod = HT.methodGet,
225− , riBucket = Just bucket
307+ riBucket = Just bucket,
226− , riQueryParams = mkOptionalParams params
308+ riQueryParams = mkOptionalParams params
227− }
309+ }
228 parseListObjectsResponse $ NC.responseBody resp
310 parseListObjectsResponse $ NC.responseBody resp
229 where
311 where
230− params = [
312+ params =
231− ("list-type", Just "2")
313+ [ ("list-type", Just "2"),
232− , ("continuation_token", nextToken)
314+ ("continuation_token", nextToken),
233− , ("prefix", prefix)
315+ ("prefix", prefix),
234− , ("delimiter", delimiter)
316+ ("delimiter", delimiter),
235− , ("max-keys", show <$> maxKeys)
317+ ("max-keys", show <$> maxKeys)
236 ]
318 ]
237
319
238-- | DELETE a bucket from the service.
320-- | DELETE a bucket from the service.
239deleteBucket :: Bucket -> Minio ()
321deleteBucket :: Bucket -> Minio ()
240−deleteBucket bucket = void $
322+deleteBucket bucket =
323+ void $
241− executeRequest $
324+ executeRequest $
325+ defaultS3ReqInfo
242− defaultS3ReqInfo { riMethod = HT.methodDelete
326+ { riMethod = HT.methodDelete,
243− , riBucket = Just bucket
327+ riBucket = Just bucket
244 }
328 }
245
329
246-- | DELETE an object from the service.
330-- | DELETE an object from the service.
247deleteObject :: Bucket -> Object -> Minio ()
331deleteObject :: Bucket -> Object -> Minio ()
248−deleteObject bucket object = void $
332+deleteObject bucket object =
333+ void $
249− executeRequest $
334+ executeRequest $
335+ defaultS3ReqInfo
250− defaultS3ReqInfo { riMethod = HT.methodDelete
336+ { riMethod = HT.methodDelete,
251− , riBucket = Just bucket
337+ riBucket = Just bucket,
252− , riObject = Just object
338+ riObject = Just object
253 }
339 }
254
340
255-- | Create a new multipart upload.
341-- | Create a new multipart upload.
256newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
342newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
257newMultipartUpload bucket object headers = do
343newMultipartUpload bucket object headers = do
344+ resp <-
345+ executeRequest $
346+ defaultS3ReqInfo
258− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPost
347+ { riMethod = HT.methodPost,
259− , riBucket = Just bucket
348+ riBucket = Just bucket,
260− , riObject = Just object
349+ riObject = Just object,
261− , riQueryParams = [("uploads", Nothing)]
350+ riQueryParams = [("uploads", Nothing)],
262− , riHeaders = headers
351+ riHeaders = headers
263− }
352+ }
264 parseNewMultipartUpload $ NC.responseBody resp
353 parseNewMultipartUpload $ NC.responseBody resp
265
354
266-- | PUT a part of an object as part of a multipart upload.
355-- | PUT a part of an object as part of a multipart upload.
267−putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
356+putObjectPart ::
357+ Bucket ->
358+ Object ->
359+ UploadId ->
360+ PartNumber ->
361+ [HT.Header] ->
362+ Payload ->
268− -> Payload -> Minio PartTuple
363+ Minio PartTuple
269putObjectPart bucket object uploadId partNumber headers payload = do
364putObjectPart bucket object uploadId partNumber headers payload = do
270 -- transform payload to conduit to enable streaming signature
365 -- transform payload to conduit to enable streaming signature
271 let payload' = mkStreamingPayload payload
366 let payload' = mkStreamingPayload payload
367+ resp <-
272− resp <- executeRequest $
368+ executeRequest $
369+ defaultS3ReqInfo
273− defaultS3ReqInfo { riMethod = HT.methodPut
370+ { riMethod = HT.methodPut,
274− , riBucket = Just bucket
371+ riBucket = Just bucket,
275− , riObject = Just object
372+ riObject = Just object,
276− , riQueryParams = mkOptionalParams params
373+ riQueryParams = mkOptionalParams params,
277− , riHeaders = headers
374+ riHeaders = headers,
278− , riPayload = payload'
375+ riPayload = payload'
279− }
376+ }
280 let rheaders = NC.responseHeaders resp
377 let rheaders = NC.responseHeaders resp
281 etag = getETagHeader rheaders
378 etag = getETagHeader rheaders
282 maybe
379 maybe
283 (throwIO MErrVETagHeaderNotFound)
380 (throwIO MErrVETagHeaderNotFound)
284− (return . (partNumber, )) etag
381+ (return . (partNumber,))
382+ etag
285 where
383 where
286− params = [
384+ params =
287− ("uploadId", Just uploadId)
385+ [ ("uploadId", Just uploadId),
288− , ("partNumber", Just $ show partNumber)
386+ ("partNumber", Just $ show partNumber)
289 ]
387 ]
290
388
291srcInfoToHeaders :: SourceInfo -> [HT.Header]
389srcInfoToHeaders :: SourceInfo -> [HT.Header]
292−srcInfoToHeaders srcInfo = ("x-amz-copy-source",
390+srcInfoToHeaders srcInfo =
391+ ( "x-amz-copy-source",
392+ encodeUtf8 $
393+ T.concat
394+ [ "/",
293− toS $ T.concat ["/", srcBucket srcInfo,
395+ srcBucket srcInfo,
396+ "/",
294− "/", srcObject srcInfo]
397+ srcObject srcInfo
398+ ]
399+ )
400+ : rangeHdr
295− ) : rangeHdr ++ zip names values
401+ ++ zip names values
296 where
402 where
403+ names =
404+ [ "x-amz-copy-source-if-match",
297− names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",
405+ "x-amz-copy-source-if-none-match",
298− "x-amz-copy-source-if-unmodified-since",
406+ "x-amz-copy-source-if-unmodified-since",
299− "x-amz-copy-source-if-modified-since"]
407+ "x-amz-copy-source-if-modified-since"
408+ ]
409+ values =
410+ mapMaybe
300− values = mapMaybe (fmap encodeUtf8 . (srcInfo &))
411+ (fmap encodeUtf8 . (srcInfo &))
412+ [ srcIfMatch,
301− [srcIfMatch, srcIfNoneMatch,
413+ srcIfNoneMatch,
302− fmap formatRFC1123 . srcIfUnmodifiedSince,
414+ fmap formatRFC1123 . srcIfUnmodifiedSince,
303− fmap formatRFC1123 . srcIfModifiedSince]
415+ fmap formatRFC1123 . srcIfModifiedSince
416+ ]
417+ rangeHdr =
304− rangeHdr = maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])])
418+ maybe [] ((\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) . toByteRange) (srcRange srcInfo)
305− $ toByteRange <$> srcRange srcInfo
306 toByteRange :: (Int64, Int64) -> HT.ByteRange
419 toByteRange :: (Int64, Int64) -> HT.ByteRange
307 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
420 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
308
421
309-- | Performs server-side copy of an object or part of an object as an
422-- | Performs server-side copy of an object or part of an object as an
310-- upload part of an ongoing multi-part upload.
423-- upload part of an ongoing multi-part upload.
424+copyObjectPart ::
311−copyObjectPart :: DestinationInfo -> SourceInfo -> UploadId
425+ DestinationInfo ->
426+ SourceInfo ->
427+ UploadId ->
428+ PartNumber ->
429+ [HT.Header] ->
312− -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
430+ Minio (ETag, UTCTime)
313copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
431copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
432+ resp <-
314− resp <- executeRequest $
433+ executeRequest $
434+ defaultS3ReqInfo
315− defaultS3ReqInfo { riMethod = HT.methodPut
435+ { riMethod = HT.methodPut,
316− , riBucket = Just $ dstBucket dstInfo
436+ riBucket = Just $ dstBucket dstInfo,
317− , riObject = Just $ dstObject dstInfo
437+ riObject = Just $ dstObject dstInfo,
318− , riQueryParams = mkOptionalParams params
438+ riQueryParams = mkOptionalParams params,
319− , riHeaders = headers ++ srcInfoToHeaders srcInfo
439+ riHeaders = headers ++ srcInfoToHeaders srcInfo
320− }
440+ }
321
441
322 parseCopyObjectResponse $ NC.responseBody resp
442 parseCopyObjectResponse $ NC.responseBody resp
323 where
443 where
324− params = [
444+ params =
325− ("uploadId", Just uploadId)
445+ [ ("uploadId", Just uploadId),
326− , ("partNumber", Just $ show partNumber)
446+ ("partNumber", Just $ show partNumber)
327 ]
447 ]
328
448
329-- | Performs server-side copy of an object that is upto 5GiB in
449-- | Performs server-side copy of an object that is upto 5GiB in
330-- size. If the object is greater than 5GiB, this function throws the
450-- size. If the object is greater than 5GiB, this function throws the
331-- error returned by the server.
451-- error returned by the server.
332−copyObjectSingle :: Bucket -> Object -> SourceInfo -> [HT.Header]
452+copyObjectSingle ::
453+ Bucket ->
454+ Object ->
455+ SourceInfo ->
456+ [HT.Header] ->
333− -> Minio (ETag, UTCTime)
457+ Minio (ETag, UTCTime)
334copyObjectSingle bucket object srcInfo headers = do
458copyObjectSingle bucket object srcInfo headers = do
335 -- validate that srcRange is Nothing for this API.
459 -- validate that srcRange is Nothing for this API.
336 when (isJust $ srcRange srcInfo) $
460 when (isJust $ srcRange srcInfo) $
337 throwIO MErrVCopyObjSingleNoRangeAccepted
461 throwIO MErrVCopyObjSingleNoRangeAccepted
462+ resp <-
338− resp <- executeRequest $
463+ executeRequest $
464+ defaultS3ReqInfo
339− defaultS3ReqInfo { riMethod = HT.methodPut
465+ { riMethod = HT.methodPut,
340− , riBucket = Just bucket
466+ riBucket = Just bucket,
341− , riObject = Just object
467+ riObject = Just object,
342− , riHeaders = headers ++ srcInfoToHeaders srcInfo
468+ riHeaders = headers ++ srcInfoToHeaders srcInfo
343− }
469+ }
344 parseCopyObjectResponse $ NC.responseBody resp
470 parseCopyObjectResponse $ NC.responseBody resp
345
471
346-- | Complete a multipart upload.
472-- | Complete a multipart upload.
347−completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
473+completeMultipartUpload ::
474+ Bucket ->
475+ Object ->
476+ UploadId ->
477+ [PartTuple] ->
348− -> Minio ETag
478+ Minio ETag
349completeMultipartUpload bucket object uploadId partTuple = do
479completeMultipartUpload bucket object uploadId partTuple = do
480+ resp <-
350− resp <- executeRequest $
481+ executeRequest $
482+ defaultS3ReqInfo
351− defaultS3ReqInfo { riMethod = HT.methodPost
483+ { riMethod = HT.methodPost,
352− , riBucket = Just bucket
484+ riBucket = Just bucket,
353− , riObject = Just object
485+ riObject = Just object,
354− , riQueryParams = mkOptionalParams params
486+ riQueryParams = mkOptionalParams params,
355− , riPayload = PayloadBS $
487+ riPayload =
488+ PayloadBS $
356− mkCompleteMultipartUploadRequest partTuple
489+ mkCompleteMultipartUploadRequest partTuple
357− }
490+ }
358 parseCompleteMultipartUploadResponse $ NC.responseBody resp
491 parseCompleteMultipartUploadResponse $ NC.responseBody resp
359 where
492 where
360 params = [("uploadId", Just uploadId)]
493 params = [("uploadId", Just uploadId)]
361
494
362-- | Abort a multipart upload.
495-- | Abort a multipart upload.
363abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
496abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
364−abortMultipartUpload bucket object uploadId = void $
497+abortMultipartUpload bucket object uploadId =
498+ void $
499+ executeRequest $
500+ defaultS3ReqInfo
365− executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
501+ { riMethod = HT.methodDelete,
366− , riBucket = Just bucket
502+ riBucket = Just bucket,
367− , riObject = Just object
503+ riObject = Just object,
368− , riQueryParams = mkOptionalParams params
504+ riQueryParams = mkOptionalParams params
369− }
505+ }
370 where
506 where
371 params = [("uploadId", Just uploadId)]
507 params = [("uploadId", Just uploadId)]
372
508
373-- | List incomplete multipart uploads.
509-- | List incomplete multipart uploads.
374−listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
510+listIncompleteUploads' ::
511+ Bucket ->
512+ Maybe Text ->
513+ Maybe Text ->
514+ Maybe Text ->
515+ Maybe Text ->
516+ Maybe Int ->
375− -> Maybe Text -> Maybe Int -> Minio ListUploadsResult
517+ Minio ListUploadsResult
376listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
518listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
519+ resp <-
520+ executeRequest $
521+ defaultS3ReqInfo
377− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
522+ { riMethod = HT.methodGet,
378− , riBucket = Just bucket
523+ riBucket = Just bucket,
379− , riQueryParams = params
524+ riQueryParams = params
380− }
525+ }
381 parseListUploadsResponse $ NC.responseBody resp
526 parseListUploadsResponse $ NC.responseBody resp
382 where
527 where
383 -- build query params
528 -- build query params
529+ params =
530+ ("uploads", Nothing)
384− params = ("uploads", Nothing) : mkOptionalParams
531+ : mkOptionalParams
385− [ ("prefix", prefix)
532+ [ ("prefix", prefix),
386− , ("delimiter", delimiter)
533+ ("delimiter", delimiter),
387− , ("key-marker", keyMarker)
534+ ("key-marker", keyMarker),
388− , ("upload-id-marker", uploadIdMarker)
535+ ("upload-id-marker", uploadIdMarker),
389− , ("max-uploads", show <$> maxKeys)
536+ ("max-uploads", show <$> maxKeys)
390− ]
537+ ]
391−
392
538
393-- | List parts of an ongoing multipart upload.
539-- | List parts of an ongoing multipart upload.
394−listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
540+listIncompleteParts' ::
541+ Bucket ->
542+ Object ->
543+ UploadId ->
544+ Maybe Text ->
545+ Maybe Text ->
395− -> Maybe Text -> Minio ListPartsResult
546+ Minio ListPartsResult
396listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
547listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
548+ resp <-
549+ executeRequest $
550+ defaultS3ReqInfo
397− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
551+ { riMethod = HT.methodGet,
398− , riBucket = Just bucket
552+ riBucket = Just bucket,
399− , riObject = Just object
553+ riObject = Just object,
400− , riQueryParams = mkOptionalParams params
554+ riQueryParams = mkOptionalParams params
401− }
555+ }
402 parseListPartsResponse $ NC.responseBody resp
556 parseListPartsResponse $ NC.responseBody resp
403 where
557 where
404 -- build optional query params
558 -- build optional query params
405− params = [
559+ params =
406− ("uploadId", Just uploadId)
560+ [ ("uploadId", Just uploadId),
407− , ("part-number-marker", partNumMarker)
561+ ("part-number-marker", partNumMarker),
408− , ("max-parts", maxParts)
562+ ("max-parts", maxParts)
409 ]
563 ]
410
564
411-- | Get metadata of an object.
565-- | Get metadata of an object.
412headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
566headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
413headObject bucket object reqHeaders = do
567headObject bucket object reqHeaders = do
568+ resp <-
569+ executeRequest $
570+ defaultS3ReqInfo
414− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
571+ { riMethod = HT.methodHead,
415− , riBucket = Just bucket
572+ riBucket = Just bucket,
416− , riObject = Just object
573+ riObject = Just object,
574+ riHeaders =
417− , riHeaders = reqHeaders
575+ reqHeaders
576+ -- This header is required for safety as otherwise http-client,
577+ -- sends Accept-Encoding: gzip, and the server may actually gzip
578+ -- body. In that case Content-Length header will be missing.
579+ <> [("Accept-Encoding", "identity")]
418− }
580+ }
419−
420− let
421− headers = NC.responseHeaders resp
422− modTime = getLastModifiedHeader headers
423− etag = getETagHeader headers
424− size = getContentLength headers
425− metadata = getMetadataMap headers
426−
427 maybe (throwIO MErrVInvalidObjectInfoResponse) return $
581 maybe (throwIO MErrVInvalidObjectInfoResponse) return $
428− ObjectInfo <$> Just object <*> modTime <*> etag <*> size <*> Just metadata
582+ parseGetObjectHeaders object $
429−
583+ NC.responseHeaders resp
430
584
431-- | Query the object store if a given bucket exists.
585-- | Query the object store if a given bucket exists.
432headBucket :: Bucket -> Minio Bool
586headBucket :: Bucket -> Minio Bool
433−headBucket bucket = headBucketEx `catches`
587+headBucket bucket =
588+ headBucketEx
434− [ Handler handleNoSuchBucket
589+ `catches` [ Handler handleNoSuchBucket,
435− , Handler handleStatus404
590+ Handler handleStatus404
436− ]
591+ ]
437−
438 where
592 where
439 handleNoSuchBucket :: ServiceErr -> Minio Bool
593 handleNoSuchBucket :: ServiceErr -> Minio Bool
594+ handleNoSuchBucket e
440− handleNoSuchBucket e | e == NoSuchBucket = return False
595+ | e == NoSuchBucket = return False
441− | otherwise = throwIO e
596+ | otherwise = throwIO e
442−
443 handleStatus404 :: NC.HttpException -> Minio Bool
597 handleStatus404 :: NC.HttpException -> Minio Bool
444 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
598 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
445 if NC.responseStatus res == status404
599 if NC.responseStatus res == status404
446− then return False
600+ then return False
447− else throwIO e
601+ else throwIO e
448 handleStatus404 e = throwIO e
602 handleStatus404 e = throwIO e
449−
450 headBucketEx = do
603 headBucketEx = do
604+ resp <-
605+ executeRequest $
606+ defaultS3ReqInfo
451− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
607+ { riMethod = HT.methodHead,
452− , riBucket = Just bucket
608+ riBucket = Just bucket
453− }
609+ }
454 return $ NC.responseStatus resp == HT.ok200
610 return $ NC.responseStatus resp == HT.ok200
455
611
456-- | Set the notification configuration on a bucket.
612-- | Set the notification configuration on a bucket.
457putBucketNotification :: Bucket -> Notification -> Minio ()
613putBucketNotification :: Bucket -> Notification -> Minio ()
458putBucketNotification bucket ncfg = do
614putBucketNotification bucket ncfg = do
459 ns <- asks getSvcNamespace
615 ns <- asks getSvcNamespace
616+ void $
617+ executeRequest $
618+ defaultS3ReqInfo
460− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
619+ { riMethod = HT.methodPut,
461− , riBucket = Just bucket
620+ riBucket = Just bucket,
462− , riQueryParams = [("notification", Nothing)]
621+ riQueryParams = [("notification", Nothing)],
463− , riPayload = PayloadBS $
622+ riPayload =
623+ PayloadBS $
464− mkPutNotificationRequest ns ncfg
624+ mkPutNotificationRequest ns ncfg
465− }
625+ }
466
626
467-- | Retrieve the notification configuration on a bucket.
627-- | Retrieve the notification configuration on a bucket.
468getBucketNotification :: Bucket -> Minio Notification
628getBucketNotification :: Bucket -> Minio Notification
469getBucketNotification bucket = do
629getBucketNotification bucket = do
630+ resp <-
631+ executeRequest $
632+ defaultS3ReqInfo
470− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
633+ { riMethod = HT.methodGet,
471− , riBucket = Just bucket
634+ riBucket = Just bucket,
472− , riQueryParams = [("notification", Nothing)]
635+ riQueryParams = [("notification", Nothing)]
473− }
636+ }
474 parseNotification $ NC.responseBody resp
637 parseNotification $ NC.responseBody resp
475
638
476-- | Remove all notifications configured on a bucket.
639-- | Remove all notifications configured on a bucket.
@@ -480,11 +643,14 @@ removeAllBucketNotification = flip putBucketNotification defaultNotification
480-- | Fetch the policy if any on a bucket.
643-- | Fetch the policy if any on a bucket.
481getBucketPolicy :: Bucket -> Minio Text
644getBucketPolicy :: Bucket -> Minio Text
482getBucketPolicy bucket = do
645getBucketPolicy bucket = do
646+ resp <-
647+ executeRequest $
648+ defaultS3ReqInfo
483− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
649+ { riMethod = HT.methodGet,
484− , riBucket = Just bucket
650+ riBucket = Just bucket,
485− , riQueryParams = [("policy", Nothing)]
651+ riQueryParams = [("policy", Nothing)]
486− }
652+ }
487− return $ toS $ NC.responseBody resp
653+ return $ decodeUtf8Lenient $ toStrictBS $ NC.responseBody resp
488
654
489-- | Set a new policy on a bucket.
655-- | Set a new policy on a bucket.
490-- As a special condition if the policy is empty
656-- As a special condition if the policy is empty
@@ -496,18 +662,24 @@ setBucketPolicy bucket policy = do
496 else putBucketPolicy bucket policy
662 else putBucketPolicy bucket policy
497
663
498-- | Save a new policy on a bucket.
664-- | Save a new policy on a bucket.
499−putBucketPolicy :: Bucket -> Text -> Minio()
665+putBucketPolicy :: Bucket -> Text -> Minio ()
500putBucketPolicy bucket policy = do
666putBucketPolicy bucket policy = do
667+ void $
668+ executeRequest $
669+ defaultS3ReqInfo
501− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
670+ { riMethod = HT.methodPut,
502− , riBucket = Just bucket
671+ riBucket = Just bucket,
503− , riQueryParams = [("policy", Nothing)]
672+ riQueryParams = [("policy", Nothing)],
504− , riPayload = PayloadBS $ encodeUtf8 policy
673+ riPayload = PayloadBS $ encodeUtf8 policy
505− }
674+ }
506
675
507-- | Delete any policy set on a bucket.
676-- | Delete any policy set on a bucket.
508−deleteBucketPolicy :: Bucket -> Minio()
677+deleteBucketPolicy :: Bucket -> Minio ()
509deleteBucketPolicy bucket = do
678deleteBucketPolicy bucket = do
679+ void $
680+ executeRequest $
681+ defaultS3ReqInfo
510− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
682+ { riMethod = HT.methodDelete,
511− , riBucket = Just bucket
683+ riBucket = Just bucket,
512− , riQueryParams = [("policy", Nothing)]
684+ riQueryParams = [("policy", Nothing)]
513− }
685+ }
src/Network/Minio/SelectAPI.hs+233−241
@@ -15,116 +15,103 @@
15--
15--
16
16
17module Network.Minio.SelectAPI
17module Network.Minio.SelectAPI
18− (
19−
20− -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
18+ ( -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
21− -- format objects in AWS S3 and in MinIO using SQL Select
19+ -- format objects in AWS S3 and in MinIO using SQL Select
22− -- statements. This allows significant reduction of data transfer
20+ -- statements. This allows significant reduction of data transfer
23− -- from object storage for computation-intensive tasks, as relevant
21+ -- from object storage for computation-intensive tasks, as relevant
24− -- data is filtered close to the storage.
22+ -- data is filtered close to the storage.
25−
26− selectObjectContent
23+ selectObjectContent,
27−
28− , SelectRequest
24+ SelectRequest,
29− , selectRequest
25+ selectRequest,
30−
26+
31− -- *** Input Serialization
27+ -- *** Input Serialization
32− -------------------------
33−
34− , InputSerialization
28+ InputSerialization,
35− , defaultCsvInput
29+ defaultCsvInput,
36− , linesJsonInput
30+ linesJsonInput,
37− , documentJsonInput
31+ documentJsonInput,
38− , defaultParquetInput
32+ defaultParquetInput,
39− , setInputCSVProps
33+ setInputCSVProps,
40−
41− , CompressionType(..)
34+ CompressionType (..),
42− , setInputCompressionType
35+ setInputCompressionType,
43−
36+
44− -- *** CSV Format details
37+ -- *** CSV Format details
45− ------------------------
38+
46− -- | CSV format options such as delimiters and quote characters are
39+ -- | CSV format options such as delimiters and quote characters are
47− -- specified using using the functions below. Options are combined
40+ -- specified using using the functions below. Options are combined
48− -- monoidally.
41+ -- monoidally.
49−
50− , CSVProp
42+ CSVProp,
51− , recordDelimiter
43+ recordDelimiter,
52− , fieldDelimiter
44+ fieldDelimiter,
53− , quoteCharacter
45+ quoteCharacter,
54− , quoteEscapeCharacter
46+ quoteEscapeCharacter,
55− , commentCharacter
47+ commentCharacter,
56− , allowQuotedRecordDelimiter
48+ allowQuotedRecordDelimiter,
57− , FileHeaderInfo(..)
49+ FileHeaderInfo (..),
58− , fileHeaderInfo
50+ fileHeaderInfo,
59− , QuoteFields(..)
51+ QuoteFields (..),
60− , quoteFields
52+ quoteFields,
61−
53+
62− -- *** Output Serialization
54+ -- *** Output Serialization
63− -------------------------
64−
65− , OutputSerialization
55+ OutputSerialization,
66− , defaultCsvOutput
56+ defaultCsvOutput,
67− , defaultJsonOutput
57+ defaultJsonOutput,
68− , outputCSVFromProps
58+ outputCSVFromProps,
69− , outputJSONFromRecordDelimiter
59+ outputJSONFromRecordDelimiter,
70−
60+
71− -- *** Progress messages
61+ -- *** Progress messages
72− ------------------------
62+ setRequestProgressEnabled,
73−
63+
74− , setRequestProgressEnabled
75−
76− -- *** Interpreting Select output
64+ -- *** Interpreting Select output
77− --------------------------------------------
65+
78− -- | The conduit returned by `selectObjectContent` returns values of
66+ -- | The conduit returned by `selectObjectContent` returns values of
79− -- the `EventMessage` data type. This returns the query output
67+ -- the `EventMessage` data type. This returns the query output
80− -- messages formatted according to the chosen output serialization,
68+ -- messages formatted according to the chosen output serialization,
81− -- interleaved with progress messages (if enabled by
69+ -- interleaved with progress messages (if enabled by
82− -- `setRequestProgressEnabled`), and at the end a statistics
70+ -- `setRequestProgressEnabled`), and at the end a statistics
83− -- message.
71+ -- message.
84− --
72+ --
85− -- If the application is interested in only the payload, then
73+ -- If the application is interested in only the payload, then
86− -- `getPayloadBytes` can be used. For example to simply print the
74+ -- `getPayloadBytes` can be used. For example to simply print the
87− -- payload to stdout:
75+ -- payload to stdout:
88− --
76+ --
89− -- > resultConduit <- selectObjectContent bucket object mySelectRequest
77+ -- > resultConduit <- selectObjectContent bucket object mySelectRequest
90− -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
78+ -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
91− --
79+ --
92− -- Note that runConduit, the connect operator (.|) and stdoutC are
80+ -- Note that runConduit, the connect operator (.|) and stdoutC are
93− -- all from the "conduit" package.
81+ -- all from the "conduit" package.
94−
95− , getPayloadBytes
82+ getPayloadBytes,
96− , EventMessage(..)
83+ EventMessage (..),
97− , Progress(..)
84+ Progress (..),
98− , Stats
85+ Stats,
86+ )
99− ) where
87+where
100−
88+
101−import Conduit ((.|))
89+import Conduit ((.|))
102−import qualified Conduit as C
90+import qualified Conduit as C
103−import qualified Data.Binary as Bin
91+import qualified Data.Binary as Bin
104−import qualified Data.ByteString as B
92+import qualified Data.ByteString as B
105−import qualified Data.ByteString.Lazy as LB
93+import qualified Data.ByteString.Lazy as LB
106−import Data.Digest.CRC32 (crc32, crc32Update)
94+import Data.Digest.CRC32 (crc32, crc32Update)
95+import Lib.Prelude
107−import qualified Network.HTTP.Conduit as NC
96+import qualified Network.HTTP.Conduit as NC
108−import qualified Network.HTTP.Types as HT
97+import qualified Network.HTTP.Types as HT
109−import UnliftIO (MonadUnliftIO)
110−
111−import Lib.Prelude
112−
113−import Network.Minio.API
98+import Network.Minio.API
114−import Network.Minio.Data
99+import Network.Minio.Data
115−import Network.Minio.Errors
100+import Network.Minio.Errors
116−import Network.Minio.Utils
101+import Network.Minio.Utils
117−import Network.Minio.XmlGenerator
102+import Network.Minio.XmlGenerator
118−import Network.Minio.XmlParser
103+import Network.Minio.XmlParser
119−
104+import UnliftIO (MonadUnliftIO)
105+
120−data EventStreamException = ESEPreludeCRCFailed
106+data EventStreamException
107+ = ESEPreludeCRCFailed
121− | ESEMessageCRCFailed
108+ | ESEMessageCRCFailed
122− | ESEUnexpectedEndOfStream
109+ | ESEUnexpectedEndOfStream
123− | ESEDecodeFail [Char]
110+ | ESEDecodeFail [Char]
124− | ESEInvalidHeaderType
111+ | ESEInvalidHeaderType
125− | ESEInvalidHeaderValueType
112+ | ESEInvalidHeaderValueType
126− | ESEInvalidMessageType
113+ | ESEInvalidMessageType
127− deriving (Eq, Show)
114+ deriving stock (Eq, Show)
128
115
129instance Exception EventStreamException
116instance Exception EventStreamException
130
117
@@ -132,171 +119,176 @@ instance Exception EventStreamException
132chunkSize :: Int
119chunkSize :: Int
133chunkSize = 32 * 1024
120chunkSize = 32 * 1024
134
121
135−parseBinary :: Bin.Binary a => ByteString -> IO a
122+parseBinary :: (Bin.Binary a) => ByteString -> IO a
136parseBinary b = do
123parseBinary b = do
137− case Bin.decodeOrFail $ LB.fromStrict b of
124+ case Bin.decodeOrFail $ LB.fromStrict b of
138− Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
125+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
139− Right (_, _, r) -> return r
126+ Right (_, _, r) -> return r
140
127
141bytesToHeaderName :: Text -> IO MsgHeaderName
128bytesToHeaderName :: Text -> IO MsgHeaderName
142bytesToHeaderName t = case t of
129bytesToHeaderName t = case t of
143− ":message-type" -> return MessageType
130+ ":message-type" -> return MessageType
144− ":event-type" -> return EventType
131+ ":event-type" -> return EventType
145− ":content-type" -> return ContentType
132+ ":content-type" -> return ContentType
146− ":error-code" -> return ErrorCode
133+ ":error-code" -> return ErrorCode
147 ":error-message" -> return ErrorMessage
134 ":error-message" -> return ErrorMessage
148− _ -> throwIO ESEInvalidHeaderType
135+ _ -> throwIO ESEInvalidHeaderType
149
136
137+parseHeaders ::
150−parseHeaders :: MonadUnliftIO m
138+ (MonadUnliftIO m) =>
139+ Word32 ->
151− => Word32 -> C.ConduitM ByteString a m [MessageHeader]
140+ C.ConduitM ByteString a m [MessageHeader]
152parseHeaders 0 = return []
141parseHeaders 0 = return []
153parseHeaders hdrLen = do
142parseHeaders hdrLen = do
154− bs1 <- readNBytes 1
143+ bs1 <- readNBytes 1
155− n :: Word8 <- liftIO $ parseBinary bs1
144+ n :: Word8 <- liftIO $ parseBinary bs1
156
145
157− headerKeyBytes <- readNBytes $ fromIntegral n
146+ headerKeyBytes <- readNBytes $ fromIntegral n
158− let headerKey = decodeUtf8Lenient headerKeyBytes
147+ let headerKey = decodeUtf8Lenient headerKeyBytes
159− headerName <- liftIO $ bytesToHeaderName headerKey
148+ headerName <- liftIO $ bytesToHeaderName headerKey
160
149
161− bs2 <- readNBytes 1
150+ bs2 <- readNBytes 1
162− headerValueType :: Word8 <- liftIO $ parseBinary bs2
151+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
163− when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
152+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
164
153
165− bs3 <- readNBytes 2
154+ bs3 <- readNBytes 2
166− vLen :: Word16 <- liftIO $ parseBinary bs3
155+ vLen :: Word16 <- liftIO $ parseBinary bs3
167− headerValueBytes <- readNBytes $ fromIntegral vLen
156+ headerValueBytes <- readNBytes $ fromIntegral vLen
168− let headerValue = decodeUtf8Lenient headerValueBytes
157+ let headerValue = decodeUtf8Lenient headerValueBytes
169− m = (headerName, headerValue)
158+ m = (headerName, headerValue)
170− k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
159+ k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
171
160
172− ms <- parseHeaders (hdrLen - k)
161+ ms <- parseHeaders (hdrLen - k)
173− return (m:ms)
162+ return (m : ms)
174
163
175-- readNBytes returns N bytes read from the string and throws an
164-- readNBytes returns N bytes read from the string and throws an
176-- exception if N bytes are not present on the stream.
165-- exception if N bytes are not present on the stream.
177−readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
166+readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
178readNBytes n = do
167readNBytes n = do
179− b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
168+ b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
180− if B.length b /= n
169+ if B.length b /= n
181− then throwIO ESEUnexpectedEndOfStream
170+ then throwIO ESEUnexpectedEndOfStream
182− else return b
171+ else return b
183−
172+
173+crcCheck ::
184−crcCheck :: MonadUnliftIO m
174+ (MonadUnliftIO m) =>
185− => C.ConduitM ByteString ByteString m ()
175+ C.ConduitM ByteString ByteString m ()
186crcCheck = do
176crcCheck = do
187− b <- readNBytes 12
177+ b <- readNBytes 12
188− n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
178+ n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
189− preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
179+ preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
190− when (crc32 (B.take 8 b) /= preludeCRC) $
180+ when (crc32 (B.take 8 b) /= preludeCRC) $
191− throwIO ESEPreludeCRCFailed
181+ throwIO ESEPreludeCRCFailed
192
182
193− -- we do not yield the checksum
183+ -- we do not yield the checksum
194− C.yield $ B.take 8 b
184+ C.yield $ B.take 8 b
195
185
196− -- 12 bytes have been read off the current message. Now read the
186+ -- 12 bytes have been read off the current message. Now read the
197− -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
187+ -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
198− let startCrc = crc32 b
188+ let startCrc = crc32 b
199− finalCrc <- accumulateYield (fromIntegral n-16) startCrc
189+ finalCrc <- accumulateYield (fromIntegral n - 16) startCrc
200
190
201− bs <- readNBytes 4
191+ bs <- readNBytes 4
202− expectedCrc :: Word32 <- liftIO $ parseBinary bs
192+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
203
193
204− when (finalCrc /= expectedCrc) $
194+ when (finalCrc /= expectedCrc) $
205− throwIO ESEMessageCRCFailed
195+ throwIO ESEMessageCRCFailed
206
196
207− -- we unconditionally recurse - downstream figures out when to
197+ -- we unconditionally recurse - downstream figures out when to
208− -- quit reading the stream
198+ -- quit reading the stream
209− crcCheck
199+ crcCheck
210 where
200 where
211 accumulateYield n checkSum = do
201 accumulateYield n checkSum = do
212− let toRead = min n chunkSize
202+ let toRead = min n chunkSize
213− b <- readNBytes toRead
203+ b <- readNBytes toRead
214− let c' = crc32Update checkSum b
204+ let c' = crc32Update checkSum b
215− n' = n - B.length b
205+ n' = n - B.length b
216− C.yield b
206+ C.yield b
217− if n' > 0
207+ if n' > 0
218− then accumulateYield n' c'
208+ then accumulateYield n' c'
219− else return c'
209+ else return c'
220−
210+
221−handleMessage :: MonadUnliftIO m => C.ConduitT ByteString EventMessage m ()
211+handleMessage :: (MonadUnliftIO m) => C.ConduitT ByteString EventMessage m ()
222handleMessage = do
212handleMessage = do
223− b1 <- readNBytes 4
213+ b1 <- readNBytes 4
224− msgLen :: Word32 <- liftIO $ parseBinary b1
214+ msgLen :: Word32 <- liftIO $ parseBinary b1
225−
215+
226− b2 <- readNBytes 4
216+ b2 <- readNBytes 4
227− hdrLen :: Word32 <- liftIO $ parseBinary b2
217+ hdrLen :: Word32 <- liftIO $ parseBinary b2
228−
218+
229− hs <- parseHeaders hdrLen
219+ hs <- parseHeaders hdrLen
230−
220+
231− let payloadLen = msgLen - hdrLen - 16
221+ let payloadLen = msgLen - hdrLen - 16
232− getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)
222+ getHdrVal h = fmap snd . find ((h ==) . fst)
233− eventHdrValue = getHdrVal EventType hs
223+ eventHdrValue = getHdrVal EventType hs
234− msgHdrValue = getHdrVal MessageType hs
224+ msgHdrValue = getHdrVal MessageType hs
235− errCode = getHdrVal ErrorCode hs
225+ errCode = getHdrVal ErrorCode hs
236− errMsg = getHdrVal ErrorMessage hs
226+ errMsg = getHdrVal ErrorMessage hs
237−
227+
238− case msgHdrValue of
228+ case msgHdrValue of
239− Just "event" -> do
229+ Just "event" -> do
240− case eventHdrValue of
230+ case eventHdrValue of
241− Just "Records" -> passThrough $ fromIntegral payloadLen
231+ Just "Records" -> passThrough $ fromIntegral payloadLen
242− Just "Cont" -> return ()
232+ Just "Cont" -> return ()
243− Just "Progress" -> do
233+ Just "Progress" -> do
244− bs <- readNBytes $ fromIntegral payloadLen
234+ bs <- readNBytes $ fromIntegral payloadLen
245− progress <- parseSelectProgress bs
235+ progress <- parseSelectProgress bs
246− C.yield $ ProgressEventMessage progress
236+ C.yield $ ProgressEventMessage progress
247− Just "Stats" -> do
237+ Just "Stats" -> do
248− bs <- readNBytes $ fromIntegral payloadLen
238+ bs <- readNBytes $ fromIntegral payloadLen
249− stats <- parseSelectProgress bs
239+ stats <- parseSelectProgress bs
250− C.yield $ StatsEventMessage stats
240+ C.yield $ StatsEventMessage stats
251− Just "End" -> return ()
241+ Just "End" -> return ()
252− _ -> throwIO ESEInvalidMessageType
242+ _ -> throwIO ESEInvalidMessageType
253− when (eventHdrValue /= Just "End") handleMessage
243+ when (eventHdrValue /= Just "End") handleMessage
254−
255− Just "error" -> do
244+ Just "error" -> do
256− let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
245+ let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
257− maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
246+ maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
258−
259− _ -> throwIO ESEInvalidMessageType
247+ _ -> throwIO ESEInvalidMessageType
260−
261 where
248 where
262 passThrough 0 = return ()
249 passThrough 0 = return ()
263 passThrough n = do
250 passThrough n = do
264− let c = min n chunkSize
251+ let c = min n chunkSize
265− b <- readNBytes c
252+ b <- readNBytes c
266− C.yield $ RecordPayloadEventMessage b
253+ C.yield $ RecordPayloadEventMessage b
267− passThrough $ n - B.length b
254+ passThrough $ n - B.length b
268−
255+
269−
270−selectProtoConduit :: MonadUnliftIO m
256+selectProtoConduit ::
257+ (MonadUnliftIO m) =>
271− => C.ConduitT ByteString EventMessage m ()
258+ C.ConduitT ByteString EventMessage m ()
272selectProtoConduit = crcCheck .| handleMessage
259selectProtoConduit = crcCheck .| handleMessage
273
260
274-- | selectObjectContent calls the SelectRequest on the given
261-- | selectObjectContent calls the SelectRequest on the given
275-- object. It returns a Conduit of event messages that can be consumed
262-- object. It returns a Conduit of event messages that can be consumed
276-- by the client.
263-- by the client.
277−selectObjectContent :: Bucket -> Object -> SelectRequest
264+selectObjectContent ::
265+ Bucket ->
266+ Object ->
267+ SelectRequest ->
278− -> Minio (C.ConduitT () EventMessage Minio ())
268+ Minio (C.ConduitT () EventMessage Minio ())
279selectObjectContent b o r = do
269selectObjectContent b o r = do
270+ let reqInfo =
271+ defaultS3ReqInfo
280− let reqInfo = defaultS3ReqInfo { riMethod = HT.methodPost
272+ { riMethod = HT.methodPost,
281− , riBucket = Just b
273+ riBucket = Just b,
282− , riObject = Just o
274+ riObject = Just o,
283− , riPayload = PayloadBS $ mkSelectRequest r
275+ riPayload = PayloadBS $ mkSelectRequest r,
284− , riNeedsLocation = False
276+ riNeedsLocation = False,
285− , riQueryParams = [("select", Nothing), ("select-type", Just "2")]
277+ riQueryParams = [("select", Nothing), ("select-type", Just "2")]
286− }
278+ }
287− --print $ mkSelectRequest r
279+ -- print $ mkSelectRequest r
288− resp <- mkStreamRequest reqInfo
280+ resp <- mkStreamRequest reqInfo
289− return $ NC.responseBody resp .| selectProtoConduit
281+ return $ NC.responseBody resp .| selectProtoConduit
290
282
291-- | A helper conduit that returns only the record payload bytes.
283-- | A helper conduit that returns only the record payload bytes.
292−getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
284+getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
293getPayloadBytes = do
285getPayloadBytes = do
294− evM <- C.await
286+ evM <- C.await
295− case evM of
287+ case evM of
296− Just v -> do
288+ Just v -> do
297− case v of
289+ case v of
298− RecordPayloadEventMessage b -> C.yield b
290+ RecordPayloadEventMessage b -> C.yield b
299− RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
291+ RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
300− _ -> return ()
292+ _ -> return ()
301− getPayloadBytes
293+ getPayloadBytes
302− Nothing -> return ()
294+ Nothing -> return ()
src/Network/Minio/Sign/V4.hs+432−329
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,88 +13,94 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
16+{-# LANGUAGE BangPatterns #-}
17+
17−module Network.Minio.Sign.V4 where
18+module Network.Minio.Sign.V4
18−
19+ ( SignParams (..),
20+ signV4QueryParams,
21+ signV4,
22+ signV4PostPolicy,
23+ signV4Stream,
24+ Service (..),
25+ credentialScope,
26+ )
27+where
28+
19−import qualified Conduit as C
29+import qualified Conduit as C
20−import qualified Data.ByteString as B
30+import qualified Data.ByteArray as BA
31+import qualified Data.ByteString as B
21−import qualified Data.ByteString.Base64 as Base64
32+import qualified Data.ByteString.Base64 as Base64
22−import qualified Data.ByteString.Char8 as B8
33+import qualified Data.ByteString.Char8 as B8
34+import qualified Data.ByteString.Lazy as LB
23−import Data.CaseInsensitive (mk)
35+import Data.CaseInsensitive (mk)
24−import qualified Data.CaseInsensitive as CI
36+import qualified Data.CaseInsensitive as CI
25−import qualified Data.Map.Strict as Map
37+import qualified Data.HashMap.Strict as Map
26−import qualified Data.Set as Set
38+import qualified Data.HashSet as Set
39+import Data.List (partition)
40+import qualified Data.List.NonEmpty as NE
27−import qualified Data.Time as Time
41+import qualified Data.Time as Time
42+import Lib.Prelude
28−import qualified Network.HTTP.Conduit as NC
43+import qualified Network.HTTP.Conduit as NC
29−import Network.HTTP.Types (Header, parseQuery)
44+import Network.HTTP.Types (Header, SimpleQuery, hContentEncoding, parseQuery)
30−import qualified Network.HTTP.Types as H
45+import qualified Network.HTTP.Types as H
46+import Network.HTTP.Types.Header (RequestHeaders)
47+import Network.Minio.Data.ByteString
48+import Network.Minio.Data.Crypto
49+import Network.Minio.Data.Time
50+import Network.Minio.Errors
31−import Text.Printf (printf)
51+import Text.Printf (printf)
32−
33−import Lib.Prelude
34−
35−import Network.Minio.Data.ByteString
36−import Network.Minio.Data.Crypto
37−import Network.Minio.Data.Time
38−import Network.Minio.Errors
39
52
40-- these headers are not included in the string to sign when signing a
53-- these headers are not included in the string to sign when signing a
41-- request
54-- request
42−ignoredHeaders :: Set ByteString
55+ignoredHeaders :: Set.HashSet ByteString
43−ignoredHeaders = Set.fromList $ map CI.foldedCase
56+ignoredHeaders =
57+ Set.fromList $
58+ map
59+ CI.foldedCase
44− [ H.hAuthorization
60+ [ H.hAuthorization,
45− , H.hContentType
61+ H.hContentType,
46− , H.hUserAgent
62+ H.hUserAgent
47− ]
63+ ]
48−
64+
49−data SignV4Data = SignV4Data {
65+data Service = ServiceS3 | ServiceSTS
50− sv4SignTime :: UTCTime
66+ deriving stock (Eq, Show)
51− , sv4Scope :: ByteString
67+
52− , sv4CanonicalRequest :: ByteString
68+toByteString :: Service -> ByteString
53− , sv4HeadersToSign :: [(ByteString, ByteString)]
69+toByteString ServiceS3 = "s3"
54− , sv4Output :: [(ByteString, ByteString)]
70+toByteString ServiceSTS = "sts"
55− , sv4StringToSign :: ByteString
71+
56− , sv4SigningKey :: ByteString
57− } deriving (Show)
58−
59−data SignParams = SignParams {
72+data SignParams = SignParams
60− spAccessKey :: Text
73+ { spAccessKey :: Text,
61− , spSecretKey :: Text
74+ spSecretKey :: BA.ScrubbedBytes,
75+ spSessionToken :: Maybe BA.ScrubbedBytes,
76+ spService :: Service,
62− , spTimeStamp :: UTCTime
77+ spTimeStamp :: UTCTime,
63− , spRegion :: Maybe Text
78+ spRegion :: Maybe Text,
64− , spExpirySecs :: Maybe Int
79+ spExpirySecs :: Maybe UrlExpiry,
65− , spPayloadHash :: Maybe ByteString
80+ spPayloadHash :: Maybe ByteString
66− } deriving (Show)
81+ }
67−
82+ deriving stock (Show)
68−debugPrintSignV4Data :: SignV4Data -> IO ()
69−debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
70− B8.putStrLn "SignV4Data:"
71− B8.putStr "Timestamp: " >> print t
72− B8.putStr "Scope: " >> B8.putStrLn s
73− B8.putStrLn "Canonical Request:"
74− B8.putStrLn cr
75− B8.putStr "Headers to Sign: " >> print h2s
76− B8.putStr "Output: " >> print o
77− B8.putStr "StringToSign: " >> B8.putStrLn sts
78− B8.putStr "SigningKey: " >> printBytes sk
79− B8.putStrLn "END of SignV4Data ========="
80− where
81− printBytes b = do
82− mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b
83− B8.putStrLn ""
84
83
85mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
84mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
86mkAuthHeader accessKey scope signedHeaderKeys sign =
85mkAuthHeader accessKey scope signedHeaderKeys sign =
87− let authValue = B.concat
86+ let authValue =
87+ B.concat
88− [ "AWS4-HMAC-SHA256 Credential="
88+ [ "AWS4-HMAC-SHA256 Credential=",
89− , toS accessKey
89+ encodeUtf8 accessKey,
90− , "/"
90+ "/",
91− , scope
91+ scope,
92− , ", SignedHeaders="
92+ ", SignedHeaders=",
93− , signedHeaderKeys
93+ signedHeaderKeys,
94− , ", Signature="
94+ ", Signature=",
95− , sign
95+ sign
96− ]
96+ ]
97− in (H.hAuthorization, authValue)
97+ in (H.hAuthorization, authValue)
98+
99+data IsStreaming = IsStreamingLength Int64 | NotStreaming
100+ deriving stock (Eq, Show)
101+
102+amzSecurityToken :: ByteString
103+amzSecurityToken = "X-Amz-Security-Token"
98
104
99-- | Given SignParams and request details, including request method,
105-- | Given SignParams and request details, including request method,
100-- request path, headers, query params and payload hash, generates an
106-- request path, headers, query params and payload hash, generates an
@@ -108,124 +114,212 @@ mkAuthHeader accessKey scope signedHeaderKeys sign =
108-- is being created. The expiry is interpreted as an integer number of
114-- is being created. The expiry is interpreted as an integer number of
109-- seconds. The output will be the list of query-parameters to add to
115-- seconds. The output will be the list of query-parameters to add to
110-- the request.
116-- the request.
117+signV4QueryParams :: SignParams -> NC.Request -> SimpleQuery
118+signV4QueryParams !sp !req =
119+ let scope = credentialScope sp
120+ expiry = spExpirySecs sp
121+
122+ headersToSign = getHeadersToSign $ NC.requestHeaders req
123+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
124+ -- query-parameters to be added before signing for presigned URLs
125+ -- (i.e. when `isJust expiry`)
126+ authQP =
127+ [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
128+ ("X-Amz-Credential", B.concat [encodeUtf8 $ spAccessKey sp, "/", scope]),
129+ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
130+ ("X-Amz-Expires", maybe "" showBS expiry),
131+ ("X-Amz-SignedHeaders", signedHeaderKeys)
132+ ]
133+ ++ maybeToList ((amzSecurityToken,) . BA.convert <$> spSessionToken sp)
134+ finalQP =
135+ parseQuery (NC.queryString req)
136+ ++ if isJust expiry
137+ then (fmap . fmap) Just authQP
138+ else []
139+ -- 1. compute canonical request
140+ canonicalRequest =
141+ mkCanonicalRequest
142+ False
143+ sp
144+ (NC.setQueryString finalQP req)
145+ headersToSign
146+
147+ -- 2. compute string to sign
148+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
149+ -- 3.1 compute signing key
150+ signingKey = getSigningKey sp
151+ -- 3.2 compute signature
152+ signature = computeSignature stringToSign signingKey
153+ in ("X-Amz-Signature", signature) : authQP
154+
155+-- | Given SignParams and request details, including request method, request
156+-- path, headers, query params and payload hash, generates an updated set of
157+-- headers, including the x-amz-date header and the Authorization header, which
158+-- includes the signature.
159+--
160+-- The output is the list of headers to be added to authenticate the request.
111−signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
161+signV4 :: SignParams -> NC.Request -> [Header]
112signV4 !sp !req =
162signV4 !sp !req =
113− let
163+ let scope = credentialScope sp
114− region = fromMaybe "" $ spRegion sp
164+
115− ts = spTimeStamp sp
116− scope = mkScope ts region
117− accessKey = toS $ spAccessKey sp
118− secretKey = toS $ spSecretKey sp
119− expiry = spExpirySecs sp
120−
121− -- headers to be added to the request
165+ -- extra headers to be added for signing purposes.
166+ extraHeaders =
122− datePair = ("X-Amz-Date", awsTimeFormatBS ts)
167+ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp)
123− computedHeaders = NC.requestHeaders req ++
168+ : ( -- payload hash is only used for S3 (not STS)
124− if isJust $ expiry
169+ [ ( "x-amz-content-sha256",
125− then []
170+ fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
126− else [(\(x, y) -> (mk x, y)) datePair]
171+ )
127− headersToSign = getHeadersToSign computedHeaders
172+ | spService sp == ServiceS3
128− signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
173+ ]
129−
174+ )
130− -- query-parameters to be added before signing for presigned URLs
175+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
131− -- (i.e. when `isJust expiry`)
176+
132− authQP = [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
133− , ("X-Amz-Credential", B.concat [accessKey, "/", scope])
134− , datePair
135− , ("X-Amz-Expires", maybe "" show expiry)
136− , ("X-Amz-SignedHeaders", signedHeaderKeys)
137− ]
138− finalQP = parseQuery (NC.queryString req) ++
139− if isJust expiry
140− then (fmap . fmap) Just authQP
141− else []
142−
143− -- 1. compute canonical request
177+ -- 1. compute canonical request
144− canonicalRequest = mkCanonicalRequest False sp
178+ reqHeaders = NC.requestHeaders req ++ extraHeaders
145− (NC.setQueryString finalQP req)
179+ (canonicalRequest, signedHeaderKeys) =
146− headersToSign
180+ getCanonicalRequestAndSignedHeaders
147−
181+ NotStreaming
182+ sp
183+ req
184+ reqHeaders
185+
148− -- 2. compute string to sign
186+ -- 2. compute string to sign
149− stringToSign = mkStringToSign ts scope canonicalRequest
187+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
150−
151− -- 3.1 compute signing key
188+ -- 3.1 compute signing key
152− signingKey = mkSigningKey ts region secretKey
189+ signingKey = getSigningKey sp
153−
154− -- 3.2 compute signature
190+ -- 3.2 compute signature
155− signature = computeSignature stringToSign signingKey
191+ signature = computeSignature stringToSign signingKey
156−
157− -- 4. compute auth header
192+ -- 4. compute auth header
158− authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
193+ authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
159−
194+ in authHeader : extraHeaders
160− -- finally compute output pairs
195+
161− sha256Hdr = ("x-amz-content-sha256",
196+credentialScope :: SignParams -> ByteString
162− fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp)
197+credentialScope sp =
163− output = if isJust expiry
198+ let region = fromMaybe "" $ spRegion sp
164− then ("X-Amz-Signature", signature) : authQP
165− else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
166− datePair, sha256Hdr]
167−
168− in output
169−
170−
171−mkScope :: UTCTime -> Text -> ByteString
172−mkScope ts region = B.intercalate "/"
199+ in B.intercalate
200+ "/"
173− [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts
201+ [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" $ spTimeStamp sp,
174− , toS region
202+ encodeUtf8 region,
175− , "s3"
203+ toByteString $ spService sp,
176− , "aws4_request"
204+ "aws4_request"
177− ]
205+ ]
178
206
207+-- Folds header name, trims whitespace in header values, skips ignored headers
208+-- and sorts headers.
179getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
209getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
180getHeadersToSign !h =
210getHeadersToSign !h =
181− filter (flip Set.notMember ignoredHeaders . fst) $
211+ filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
182− map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
212+ map (bimap CI.foldedCase stripBS) h
183−
213+
214+-- | Given the list of headers in the request, computes the canonical headers
215+-- and the signed headers strings.
216+getCanonicalHeaders :: NonEmpty Header -> (ByteString, ByteString)
217+getCanonicalHeaders h =
218+ let -- Folds header name, trims spaces in header values, skips ignored
219+ -- headers and sorts headers by name (we must not re-order multi-valued
220+ -- headers).
221+ headersToSign =
222+ NE.toList $
223+ NE.sortBy (\a b -> compare (fst a) (fst b)) $
224+ NE.fromList $
225+ NE.filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
226+ NE.map (bimap CI.foldedCase stripBS) h
227+
228+ canonicalHeaders = mconcat $ map (\(a, b) -> a <> ":" <> b <> "\n") headersToSign
229+ signedHeaderKeys = B.intercalate ";" $ map fst headersToSign
230+ in (canonicalHeaders, signedHeaderKeys)
231+
232+getCanonicalRequestAndSignedHeaders ::
233+ IsStreaming ->
234+ SignParams ->
235+ NC.Request ->
236+ [Header] ->
237+ (ByteString, ByteString)
238+getCanonicalRequestAndSignedHeaders isStreaming sp req requestHeaders =
239+ let httpMethod = NC.method req
240+
241+ canonicalUri = uriEncode False $ NC.path req
242+
243+ canonicalQueryString =
244+ B.intercalate "&" $
245+ map (\(x, y) -> B.concat [x, "=", y]) $
246+ sort $
247+ map
248+ ( bimap (uriEncode True) (maybe "" (uriEncode True))
249+ )
250+ (parseQuery $ NC.queryString req)
251+
252+ (canonicalHeaders, signedHeaderKeys) = getCanonicalHeaders $ NE.fromList requestHeaders
253+ payloadHashStr =
254+ case isStreaming of
255+ IsStreamingLength _ -> "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
256+ NotStreaming -> fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
257+
258+ canonicalRequest =
259+ B.intercalate
260+ "\n"
261+ [ httpMethod,
262+ canonicalUri,
263+ canonicalQueryString,
264+ canonicalHeaders,
265+ signedHeaderKeys,
266+ payloadHashStr
267+ ]
268+ in (canonicalRequest, signedHeaderKeys)
269+
270+mkCanonicalRequest ::
271+ Bool ->
272+ SignParams ->
273+ NC.Request ->
184−mkCanonicalRequest :: Bool -> SignParams -> NC.Request -> [(ByteString, ByteString)]
274+ [(ByteString, ByteString)] ->
185− -> ByteString
275+ ByteString
186mkCanonicalRequest !isStreaming !sp !req !headersForSign =
276mkCanonicalRequest !isStreaming !sp !req !headersForSign =
187− let
277+ let httpMethod = NC.method req
278+ canonicalUri = uriEncode False $ NC.path req
188− canonicalQueryString = B.intercalate "&" $
279+ canonicalQueryString =
280+ B.intercalate "&" $
189− map (\(x, y) -> B.concat [x, "=", y]) $
281+ map (\(x, y) -> B.concat [x, "=", y]) $
190− sort $ map (\(x, y) ->
282+ sortBy (\a b -> compare (fst a) (fst b)) $
283+ map
191− (uriEncode True x, maybe "" (uriEncode True) y)) $
284+ ( bimap (uriEncode True) (maybe "" (uriEncode True))
285+ )
192− (parseQuery $ NC.queryString req)
286+ (parseQuery $ NC.queryString req)
193−
194− sortedHeaders = sort headersForSign
287+ sortedHeaders = sort headersForSign
195−
196− canonicalHeaders = B.concat $
288+ canonicalHeaders =
289+ B.concat $
197− map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
290+ map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
198−
199− signedHeaders = B.intercalate ";" $ map fst sortedHeaders
291+ signedHeaders = B.intercalate ";" $ map fst sortedHeaders
200−
201− payloadHashStr =
292+ payloadHashStr =
202 if isStreaming
293 if isStreaming
203− then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
294+ then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
204− else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
295+ else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
205− in
206− B.intercalate "\n"
296+ in B.intercalate
207− [ NC.method req
297+ "\n"
208− , uriEncode False $ NC.path req
298+ [ httpMethod,
299+ canonicalUri,
209− , canonicalQueryString
300+ canonicalQueryString,
210− , canonicalHeaders
301+ canonicalHeaders,
211− , signedHeaders
302+ signedHeaders,
212− , payloadHashStr
303+ payloadHashStr
213− ]
304+ ]
214
305
215mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
306mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
216−mkStringToSign ts !scope !canonicalRequest = B.intercalate "\n"
307+mkStringToSign ts !scope !canonicalRequest =
308+ B.intercalate
309+ "\n"
217− [ "AWS4-HMAC-SHA256"
310+ [ "AWS4-HMAC-SHA256",
218− , awsTimeFormatBS ts
311+ awsTimeFormatBS ts,
219− , scope
312+ scope,
220− , hashSHA256 canonicalRequest
313+ hashSHA256 canonicalRequest
221− ]
314+ ]
222−
315+
223−mkSigningKey :: UTCTime -> Text -> ByteString -> ByteString
316+getSigningKey :: SignParams -> ByteString
317+getSigningKey sp =
224−mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"
318+ hmacSHA256RawBS "aws4_request"
225− . hmacSHA256RawBS "s3"
319+ . hmacSHA256RawBS (toByteString $ spService sp)
226− . hmacSHA256RawBS (toS region)
320+ . hmacSHA256RawBS (encodeUtf8 $ fromMaybe "" $ spRegion sp)
227− . hmacSHA256RawBS (awsDateFormatBS ts)
321+ . hmacSHA256RawBS (awsDateFormatBS $ spTimeStamp sp)
228− $ B.concat ["AWS4", secretKey]
322+ $ B.concat ["AWS4", BA.convert $ spSecretKey sp]
229
323
230computeSignature :: ByteString -> ByteString -> ByteString
324computeSignature :: ByteString -> ByteString -> ByteString
231computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
325computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -233,159 +327,168 @@ computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
233-- | Takes a validated Post Policy JSON bytestring, the signing time,
327-- | Takes a validated Post Policy JSON bytestring, the signing time,
234-- and ConnInfo and returns form-data for the POST upload containing
328-- and ConnInfo and returns form-data for the POST upload containing
235-- just the signature and the encoded post-policy.
329-- just the signature and the encoded post-policy.
236−signV4PostPolicy :: ByteString -> SignParams
330+signV4PostPolicy ::
331+ ByteString ->
332+ SignParams ->
237− -> Map.Map Text ByteString
333+ Map.HashMap Text ByteString
238signV4PostPolicy !postPolicyJSON !sp =
334signV4PostPolicy !postPolicyJSON !sp =
239− let
240− stringToSign = Base64.encode postPolicyJSON
335+ let stringToSign = Base64.encode postPolicyJSON
241− region = fromMaybe "" $ spRegion sp
336+ signingKey = getSigningKey sp
242− signingKey = mkSigningKey (spTimeStamp sp) region $ toS $ spSecretKey sp
243− signature = computeSignature stringToSign signingKey
337+ signature = computeSignature stringToSign signingKey
244− in
338+ in Map.fromList $
245− Map.fromList [ ("x-amz-signature", signature)
339+ [ ("x-amz-signature", signature),
246− , ("policy", stringToSign)
340+ ("policy", stringToSign)
247− ]
341+ ]
342+ ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
248
343
249chunkSizeConstant :: Int
344chunkSizeConstant :: Int
250chunkSizeConstant = 64 * 1024
345chunkSizeConstant = 64 * 1024
251
346
252-- base16Len computes the number of bytes required to represent @n (> 0)@ in
347-- base16Len computes the number of bytes required to represent @n (> 0)@ in
253-- hexadecimal.
348-- hexadecimal.
254−base16Len :: Integral a => a -> Int
349+base16Len :: (Integral a) => a -> Int
255−base16Len n | n == 0 = 0
350+base16Len n
351+ | n == 0 = 0
256− | otherwise = 1 + base16Len (n `div` 16)
352+ | otherwise = 1 + base16Len (n `div` 16)
257
353
258signedStreamLength :: Int64 -> Int64
354signedStreamLength :: Int64 -> Int64
259signedStreamLength dataLen =
355signedStreamLength dataLen =
260− let
261− chunkSzInt = fromIntegral chunkSizeConstant
356+ let chunkSzInt = fromIntegral chunkSizeConstant
262− (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
357+ (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
263−
264−
265− -- Structure of a chunk:
358+ -- Structure of a chunk:
266− -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
359+ -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
267− encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
360+ encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
268− fullChunkSize = encodedChunkLen chunkSzInt
361+ fullChunkSize = encodedChunkLen chunkSzInt
269− lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
362+ lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
270− finalChunkSize = 1 + 17 + 64 + 2 + 2
363+ finalChunkSize = 1 + 17 + 64 + 2 + 2
271− in
272− numChunks * fullChunkSize + lastChunkSize + finalChunkSize
364+ in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
273−
365+
366+-- For streaming S3, we need to update the content-encoding header.
367+addContentEncoding :: [Header] -> [Header]
368+addContentEncoding hs =
369+ -- assume there is at most one content-encoding header.
370+ let (ceHdrs, others) = partition ((== hContentEncoding) . fst) hs
371+ in maybe
372+ (hContentEncoding, "aws-chunked")
373+ (\(k, v) -> (k, v <> ",aws-chunked"))
374+ (listToMaybe ceHdrs)
375+ : others
376+
274−signV4Stream :: Int64 -> SignParams -> NC.Request
377+signV4Stream ::
275− -> (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
378+ Int64 ->
379+ SignParams ->
380+ NC.Request ->
276− -- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
381+ (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
277signV4Stream !payloadLength !sp !req =
382signV4Stream !payloadLength !sp !req =
278− let
279− ts = spTimeStamp sp
383+ let ts = spTimeStamp sp
280−
384+
281− addContentEncoding hs =
385+ -- compute the updated list of headers to be added for signing purposes.
282− let ceMay = headMay $ filter (\(x, _) -> x == "content-encoding") hs
386+ signedContentLength = signedStreamLength payloadLength
283− in case ceMay of
387+ extraHeaders =
284− Nothing -> ("content-encoding", "aws-chunked") : hs
285− Just (_, ce) -> ("content-encoding", ce <> ",aws-chunked") :
286− filter (\(x, _) -> x /= "content-encoding") hs
287−
288− -- headers to be added to the request
289− datePair = ("X-Amz-Date", awsTimeFormatBS ts)
388+ [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
290− computedHeaders = addContentEncoding $
291− datePair : NC.requestHeaders req
292−
293− -- headers specific to streaming signature
294− signedContentLength = signedStreamLength payloadLength
295− streamingHeaders :: [Header]
296− streamingHeaders =
297− [ ("x-amz-decoded-content-length", show payloadLength)
389+ ("x-amz-decoded-content-length", showBS payloadLength),
298− , ("content-length", show signedContentLength )
390+ ("content-length", showBS signedContentLength),
299− , ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
391+ ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
300− ]
301− headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders
302− signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
303− finalQP = parseQuery (NC.queryString req)
304−
305− -- 1. Compute Seed Signature
306− -- 1.1 Canonical Request
307− canonicalReq = mkCanonicalRequest True sp
308− (NC.setQueryString finalQP req)
309− headersToSign
310−
311− region = fromMaybe "" $ spRegion sp
312− scope = mkScope ts region
313− accessKey = spAccessKey sp
314− secretKey = spSecretKey sp
315−
316− -- 1.2 String toSign
317− stringToSign = mkStringToSign ts scope canonicalReq
318−
319− -- 1.3 Compute signature
320− -- 1.3.1 compute signing key
321− signingKey = mkSigningKey ts region $ toS secretKey
322−
323− -- 1.3.2 Compute signature
324− seedSignature = computeSignature stringToSign signingKey
325−
326− -- 1.3.3 Compute Auth Header
327− authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
328−
329− -- 1.4 Updated headers for the request
330− finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)
331− -- headersToAdd = authHeader : datePair : streamingHeaders
332−
333− toHexStr n = B8.pack $ printf "%x" n
334−
335− (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
336−
337− -- Function to compute string to sign for each chunk.
338− chunkStrToSign prevSign currChunkHash =
339− B.intercalate "\n"
340− [ "AWS4-HMAC-SHA256-PAYLOAD"
341− , awsTimeFormatBS ts
342− , scope
343− , prevSign
344− , hashSHA256 ""
345− , currChunkHash
346 ]
392 ]
347−
393+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
394+ requestHeaders =
395+ addContentEncoding $
396+ foldr setHeader (NC.requestHeaders req) extraHeaders
397+
398+ -- 1. Compute Seed Signature
399+ -- 1.1 Canonical Request
400+ (canonicalReq, signedHeaderKeys) =
401+ getCanonicalRequestAndSignedHeaders
402+ (IsStreamingLength payloadLength)
403+ sp
404+ req
405+ requestHeaders
406+
407+ scope = credentialScope sp
408+ accessKey = spAccessKey sp
409+ -- 1.2 String toSign
410+ stringToSign = mkStringToSign ts scope canonicalReq
411+ -- 1.3 Compute signature
412+ -- 1.3.1 compute signing key
413+ signingKey = getSigningKey sp
414+ -- 1.3.2 Compute signature
415+ seedSignature = computeSignature stringToSign signingKey
416+ -- 1.3.3 Compute Auth Header
417+ authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
418+ -- 1.4 Updated headers for the request
419+ finalReqHeaders = authHeader : requestHeaders
420+ -- headersToAdd = authHeader : datePair : streamingHeaders
421+
422+ toHexStr n = B8.pack $ printf "%x" n
423+ (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
424+ -- Function to compute string to sign for each chunk.
425+ chunkStrToSign prevSign currChunkHash =
426+ B.intercalate
427+ "\n"
428+ [ "AWS4-HMAC-SHA256-PAYLOAD",
429+ awsTimeFormatBS ts,
430+ scope,
431+ prevSign,
432+ hashSHA256 "",
433+ currChunkHash
434+ ]
348− -- Read n byte from upstream and return a strict bytestring.
435+ -- Read n byte from upstream and return a strict bytestring.
349− mustTakeN n = do
436+ mustTakeN n = do
350− bs <- toS <$> (C.takeCE n C..| C.sinkLazy)
437+ bs <- LB.toStrict <$> (C.takeCE n C..| C.sinkLazy)
351 when (B.length bs /= n) $
438 when (B.length bs /= n) $
352− throwIO MErrVStreamingBodyUnexpectedEOF
439+ throwIO MErrVStreamingBodyUnexpectedEOF
353 return bs
440 return bs
354−
355− signerConduit n lps prevSign =
441+ signerConduit n lps prevSign =
356− -- First case encodes a full chunk of length
442+ -- First case encodes a full chunk of length
357− -- 'chunkSizeConstant'.
443+ -- 'chunkSizeConstant'.
444+ if
358− if | n > 0 -> do
445+ | n > 0 -> do
359− bs <- mustTakeN chunkSizeConstant
446+ bs <- mustTakeN chunkSizeConstant
360− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
447+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
361− nextSign = computeSignature strToSign signingKey
448+ nextSign = computeSignature strToSign signingKey
449+ chunkBS =
362− chunkBS = toHexStr chunkSizeConstant
450+ toHexStr chunkSizeConstant
363− <> ";chunk-signature="
451+ <> ";chunk-signature="
364− <> nextSign <> "\r\n" <> bs <> "\r\n"
452+ <> nextSign
453+ <> "\r\n"
454+ <> bs
455+ <> "\r\n"
365− C.yield chunkBS
456+ C.yield chunkBS
366− signerConduit (n-1) lps nextSign
457+ signerConduit (n - 1) lps nextSign
367−
458+
368− -- Second case encodes the last chunk which is smaller than
459+ -- Second case encodes the last chunk which is smaller than
369− -- 'chunkSizeConstant'
460+ -- 'chunkSizeConstant'
370− | lps > 0 -> do
461+ | lps > 0 -> do
371− bs <- mustTakeN $ fromIntegral lps
462+ bs <- mustTakeN $ fromIntegral lps
372− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
463+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
373− nextSign = computeSignature strToSign signingKey
464+ nextSign = computeSignature strToSign signingKey
465+ chunkBS =
466+ toHexStr lps
374− chunkBS = toHexStr lps <> ";chunk-signature="
467+ <> ";chunk-signature="
375− <> nextSign <> "\r\n" <> bs <> "\r\n"
468+ <> nextSign
469+ <> "\r\n"
470+ <> bs
471+ <> "\r\n"
376− C.yield chunkBS
472+ C.yield chunkBS
377− signerConduit 0 0 nextSign
473+ signerConduit 0 0 nextSign
378−
474+
379− -- Last case encodes the final signature chunk that has no
475+ -- Last case encodes the final signature chunk that has no
380− -- data.
476+ -- data.
381− | otherwise -> do
477+ | otherwise -> do
382− let strToSign = chunkStrToSign prevSign (hashSHA256 "")
478+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")
383− nextSign = computeSignature strToSign signingKey
479+ nextSign = computeSignature strToSign signingKey
384− lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
480+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
385− C.yield lastChunkBS
481+ C.yield lastChunkBS
386− in
482+ in \src ->
483+ req
387− \src -> req { NC.requestHeaders = finalReqHeaders
484+ { NC.requestHeaders = finalReqHeaders,
388− , NC.requestBody =
485+ NC.requestBody =
389− NC.requestBodySource signedContentLength $
486+ NC.requestBodySource signedContentLength $
390− src C..| signerConduit numParts lastPSize seedSignature
487+ src C..| signerConduit numParts lastPSize seedSignature
391− }
488+ }
489+
490+-- "setHeader r hdr" adds the hdr to r, replacing it in r if it already exists.
491+setHeader :: Header -> RequestHeaders -> RequestHeaders
492+setHeader hdr r =
493+ let r' = filter (\(name, _) -> name /= fst hdr) r
494+ in hdr : r'
src/Network/Minio/Utils.hs+150−123
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -16,71 +16,77 @@
16
16
17module Network.Minio.Utils where
17module Network.Minio.Utils where
18
18
19−import qualified Conduit as C
19+import qualified Conduit as C
20−import Control.Monad.IO.Unlift (MonadUnliftIO)
20+import Control.Monad.IO.Unlift (MonadUnliftIO)
21−import qualified Control.Monad.Trans.Resource as R
21+import qualified Control.Monad.Trans.Resource as R
22−import qualified Data.ByteString as B
22+import qualified Data.ByteString as B
23−import qualified Data.ByteString.Lazy as LB
23+import qualified Data.ByteString.Lazy as LB
24−import Data.CaseInsensitive (mk, original)
24+import Data.CaseInsensitive (mk, original)
25−import qualified Data.Conduit.Binary as CB
25+import qualified Data.Conduit.Binary as CB
26−import qualified Data.List as List
26+import qualified Data.HashMap.Strict as H
27−import qualified Data.Map as Map
28−import qualified Data.Text as T
27+import qualified Data.Text as T
29−import Data.Text.Encoding.Error (lenientDecode)
30−import Data.Text.Read (decimal)
28+import Data.Text.Read (decimal)
29+import Data.Time
31−import Data.Time (defaultTimeLocale, parseTimeM,
30+ ( defaultTimeLocale,
31+ parseTimeM,
32− rfc822DateFormat)
32+ rfc822DateFormat,
33+ )
34+import Lib.Prelude
33−import Network.HTTP.Conduit (Response)
35+import Network.HTTP.Conduit (Response)
34−import qualified Network.HTTP.Conduit as NC
36+import qualified Network.HTTP.Conduit as NC
35−import qualified Network.HTTP.Types as HT
37+import qualified Network.HTTP.Types as HT
36−import qualified Network.HTTP.Types.Header as Hdr
38+import qualified Network.HTTP.Types.Header as Hdr
39+import Network.Minio.Data.ByteString
40+import Network.Minio.JsonParser (parseErrResponseJSON)
41+import Network.Minio.XmlCommon (parseErrResponse)
37−import qualified System.IO as IO
42+import qualified System.IO as IO
38−import qualified UnliftIO as U
43+import qualified UnliftIO as U
39−import qualified UnliftIO.Async as A
44+import qualified UnliftIO.Async as A
40−import qualified UnliftIO.MVar as UM
45+
41−
46+allocateReadFile ::
42−import Lib.Prelude
43−
44−import Network.Minio.Data
45−import Network.Minio.Data.ByteString
46−import Network.Minio.JsonParser (parseErrResponseJSON)
47−import Network.Minio.XmlParser (parseErrResponse)
48−
49−allocateReadFile :: (MonadUnliftIO m, R.MonadResource m)
47+ (MonadUnliftIO m, R.MonadResource m) =>
48+ FilePath ->
50− => FilePath -> m (R.ReleaseKey, Handle)
49+ m (R.ReleaseKey, Handle)
51allocateReadFile fp = do
50allocateReadFile fp = do
52 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
51 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
53− either (\(e :: IOException) -> throwIO e) (return . (rk,)) hdlE
52+ either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
54 where
53 where
55 openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
54 openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
56 cleanup = either (const $ return ()) IO.hClose
55 cleanup = either (const $ return ()) IO.hClose
57
56
58-- | Queries the file size from the handle. Catches any file operation
57-- | Queries the file size from the handle. Catches any file operation
59-- exceptions and returns Nothing instead.
58-- exceptions and returns Nothing instead.
59+getFileSize ::
60−getFileSize :: (MonadUnliftIO m, R.MonadResource m)
60+ (MonadUnliftIO m) =>
61+ Handle ->
61− => Handle -> m (Maybe Int64)
62+ m (Maybe Int64)
62getFileSize h = do
63getFileSize h = do
63 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
64 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
64 case resE of
65 case resE of
65− Left (_ :: IOException) -> return Nothing
66+ Left (_ :: U.IOException) -> return Nothing
66− Right s -> return $ Just s
67+ Right s -> return $ Just s
67
68
68-- | Queries if handle is seekable. Catches any file operation
69-- | Queries if handle is seekable. Catches any file operation
69-- exceptions and return False instead.
70-- exceptions and return False instead.
70−isHandleSeekable :: (R.MonadResource m, MonadUnliftIO m)
71+isHandleSeekable ::
72+ (R.MonadResource m) =>
71− => Handle -> m Bool
73+ Handle ->
74+ m Bool
72isHandleSeekable h = do
75isHandleSeekable h = do
73 resE <- liftIO $ try $ IO.hIsSeekable h
76 resE <- liftIO $ try $ IO.hIsSeekable h
74 case resE of
77 case resE of
75− Left (_ :: IOException) -> return False
78+ Left (_ :: U.IOException) -> return False
76− Right v -> return v
79+ Right v -> return v
77
80
78-- | Helper function that opens a handle to the filepath and performs
81-- | Helper function that opens a handle to the filepath and performs
79-- the given action on it. Exceptions of type MError are caught and
82-- the given action on it. Exceptions of type MError are caught and
80-- returned - both during file handle allocation and when the action
83-- returned - both during file handle allocation and when the action
81-- is run.
84-- is run.
85+withNewHandle ::
82−withNewHandle :: (MonadUnliftIO m, R.MonadResource m)
86+ (MonadUnliftIO m, R.MonadResource m) =>
87+ FilePath ->
88+ (Handle -> m a) ->
83− => FilePath -> (Handle -> m a) -> m (Either IOException a)
89+ m (Either U.IOException a)
84withNewHandle fp fileAction = do
90withNewHandle fp fileAction = do
85 -- opening a handle can throw MError exception.
91 -- opening a handle can throw MError exception.
86 handleE <- try $ allocateReadFile fp
92 handleE <- try $ allocateReadFile fp
@@ -94,19 +100,61 @@ withNewHandle fp fileAction = do
94 return resE
100 return resE
95
101
96mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
102mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
97−mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
103+mkHeaderFromPairs = map (first mk)
98
104
99lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
105lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
100−lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
106+lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
101
107
102getETagHeader :: [HT.Header] -> Maybe Text
108getETagHeader :: [HT.Header] -> Maybe Text
103getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
109getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
104
110
105getMetadata :: [HT.Header] -> [(Text, Text)]
111getMetadata :: [HT.Header] -> [(Text, Text)]
112+getMetadata =
106−getMetadata = map ((\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y)))
113+ map (\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y))
107−
114+
108−getMetadataMap :: [HT.Header] -> Map Text Text
115+-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
109−getMetadataMap hs = Map.fromList (getMetadata hs)
116+-- stripped and a Just is returned.
117+userMetadataHeaderNameMaybe :: Text -> Maybe Text
118+userMetadataHeaderNameMaybe k =
119+ let prefix = T.toCaseFold "X-Amz-Meta-"
120+ n = T.length prefix
121+ in if T.toCaseFold (T.take n k) == prefix
122+ then Just (T.drop n k)
123+ else Nothing
124+
125+toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
126+toMaybeMetadataHeader (k, v) =
127+ (,v) <$> userMetadataHeaderNameMaybe k
128+
129+getNonUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
130+getNonUserMetadataMap =
131+ H.fromList
132+ . filter
133+ ( isNothing
134+ . userMetadataHeaderNameMaybe
135+ . fst
136+ )
137+
138+addXAmzMetaPrefix :: Text -> Text
139+addXAmzMetaPrefix s
140+ | isJust (userMetadataHeaderNameMaybe s) = s
141+ | otherwise = "X-Amz-Meta-" <> s
142+
143+mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
144+mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix x, encodeUtf8 y))
145+
146+-- | This function collects all headers starting with `x-amz-meta-`
147+-- and strips off this prefix, and returns a map.
148+getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
149+getUserMetadataMap =
150+ H.fromList
151+ . mapMaybe toMaybeMetadataHeader
152+
153+getHostHeader :: (ByteString, Int) -> ByteString
154+getHostHeader (host_, port_) =
155+ if port_ == 80 || port_ == 443
156+ then host_
157+ else host_ <> ":" <> show port_
110
158
111getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
159getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
112getLastModifiedHeader hs = do
160getLastModifiedHeader hs = do
@@ -116,19 +164,21 @@ getLastModifiedHeader hs = do
116getContentLength :: [HT.Header] -> Maybe Int64
164getContentLength :: [HT.Header] -> Maybe Int64
117getContentLength hs = do
165getContentLength hs = do
118 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
166 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
119− fst <$> hush (decimal nbs)
167+ fst <$> either (const Nothing) Just (decimal nbs)
120−
121
168
122decodeUtf8Lenient :: ByteString -> Text
169decodeUtf8Lenient :: ByteString -> Text
123decodeUtf8Lenient = decodeUtf8With lenientDecode
170decodeUtf8Lenient = decodeUtf8With lenientDecode
124
171
125isSuccessStatus :: HT.Status -> Bool
172isSuccessStatus :: HT.Status -> Bool
173+isSuccessStatus sts =
126−isSuccessStatus sts = let s = HT.statusCode sts
174+ let s = HT.statusCode sts
127− in (s >= 200 && s < 300)
175+ in (s >= 200 && s < 300)
128−
176+
177+httpLbs ::
129−httpLbs :: MonadIO m
178+ (MonadIO m) =>
130− => NC.Request -> NC.Manager
179+ NC.Request ->
180+ NC.Manager ->
131− -> m (NC.Response LByteString)
181+ m (NC.Response LByteString)
132httpLbs req mgr = do
182httpLbs req mgr = do
133 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
183 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
134 resp <- either throwIO return respE
184 resp <- either throwIO return respE
@@ -140,21 +190,26 @@ httpLbs req mgr = do
140 Just "application/json" -> do
190 Just "application/json" -> do
141 sErr <- parseErrResponseJSON $ NC.responseBody resp
191 sErr <- parseErrResponseJSON $ NC.responseBody resp
142 throwIO sErr
192 throwIO sErr
143−
193+ _ ->
194+ throwIO $
144− _ -> throwIO $ NC.HttpExceptionRequest req $
195+ NC.HttpExceptionRequest req $
145− NC.StatusCodeException (void resp) (show resp)
196+ NC.StatusCodeException (void resp) (showBS resp)
146
197
147 return resp
198 return resp
148 where
199 where
200+ tryHttpEx ::
149− tryHttpEx :: IO (NC.Response LByteString)
201+ IO (NC.Response LByteString) ->
150− -> IO (Either NC.HttpException (NC.Response LByteString))
202+ IO (Either NC.HttpException (NC.Response LByteString))
151 tryHttpEx = try
203 tryHttpEx = try
204+ contentTypeMay resp =
152− contentTypeMay resp = lookupHeader Hdr.hContentType $
205+ lookupHeader Hdr.hContentType $
153− NC.responseHeaders resp
206+ NC.responseHeaders resp
154−
207+
208+http ::
155−http :: (MonadUnliftIO m, R.MonadResource m)
209+ (MonadUnliftIO m, R.MonadResource m) =>
156− => NC.Request -> NC.Manager
210+ NC.Request ->
211+ NC.Manager ->
157− -> m (Response (C.ConduitT () ByteString m ()))
212+ m (Response (C.ConduitT () ByteString m ()))
158http req mgr = do
213http req mgr = do
159 respE <- tryHttpEx $ NC.http req mgr
214 respE <- tryHttpEx $ NC.http req mgr
160 resp <- either throwIO return respE
215 resp <- either throwIO return respE
@@ -164,25 +219,31 @@ http req mgr = do
164 respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
219 respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
165 sErr <- parseErrResponse respBody
220 sErr <- parseErrResponse respBody
166 throwIO sErr
221 throwIO sErr
167−
168 _ -> do
222 _ -> do
169 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
223 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
224+ throwIO $
170− throwIO $ NC.HttpExceptionRequest req $
225+ NC.HttpExceptionRequest req $
171− NC.StatusCodeException (void resp) content
226+ NC.StatusCodeException (void resp) content
172−
173
227
174 return resp
228 return resp
175 where
229 where
230+ tryHttpEx ::
176− tryHttpEx :: (MonadUnliftIO m) => m a
231+ (MonadUnliftIO m) =>
232+ m a ->
177− -> m (Either NC.HttpException a)
233+ m (Either NC.HttpException a)
178 tryHttpEx = try
234 tryHttpEx = try
235+ contentTypeMay resp =
179− contentTypeMay resp = lookupHeader Hdr.hContentType $
236+ lookupHeader Hdr.hContentType $
180− NC.responseHeaders resp
237+ NC.responseHeaders resp
181
238
182-- Similar to mapConcurrently but limits the number of threads that
239-- Similar to mapConcurrently but limits the number of threads that
183-- can run using a quantity semaphore.
240-- can run using a quantity semaphore.
184−limitedMapConcurrently :: MonadUnliftIO m
241+limitedMapConcurrently ::
242+ (MonadUnliftIO m) =>
185− => Int -> (t -> m a) -> [t] -> m [a]
243+ Int ->
244+ (t -> m a) ->
245+ [t] ->
246+ m [a]
186limitedMapConcurrently 0 _ _ = return []
247limitedMapConcurrently 0 _ _ = return []
187limitedMapConcurrently count act args = do
248limitedMapConcurrently count act args = do
188 t' <- U.newTVarIO count
249 t' <- U.newTVarIO count
@@ -191,17 +252,15 @@ limitedMapConcurrently count act args = do
191 where
252 where
192 wThread t arg =
253 wThread t arg =
193 U.bracket_ (waitSem t) (signalSem t) $ act arg
254 U.bracket_ (waitSem t) (signalSem t) $ act arg
194−
195 -- quantity semaphore implementation using TVar
255 -- quantity semaphore implementation using TVar
196 waitSem t = U.atomically $ do
256 waitSem t = U.atomically $ do
197 v <- U.readTVar t
257 v <- U.readTVar t
198 if v > 0
258 if v > 0
199− then U.writeTVar t (v-1)
259+ then U.writeTVar t (v - 1)
200− else U.retrySTM
260+ else U.retrySTM
201−
202 signalSem t = U.atomically $ do
261 signalSem t = U.atomically $ do
203 v <- U.readTVar t
262 v <- U.readTVar t
204− U.writeTVar t (v+1)
263+ U.writeTVar t (v + 1)
205
264
206-- helper function to 'drop' empty optional parameter.
265-- helper function to 'drop' empty optional parameter.
207mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
266mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -210,7 +269,7 @@ mkQuery k mv = (k,) <$> mv
210-- helper function to build query parameters that are optional.
269-- helper function to build query parameters that are optional.
211-- don't use it with mandatory query params with empty value.
270-- don't use it with mandatory query params with empty value.
212mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
271mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
213−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
272+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
214
273
215-- | Conduit that rechunks bytestrings into the given chunk
274-- | Conduit that rechunks bytestrings into the given chunk
216-- lengths. Stops after given chunk lengths are yielded. Stops if
275-- lengths. Stops after given chunk lengths are yielded. Stops if
@@ -218,41 +277,9 @@ mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
218-- received. Does not throw any errors.
277-- received. Does not throw any errors.
219chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
278chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
220chunkBSConduit [] = return ()
279chunkBSConduit [] = return ()
221−chunkBSConduit (s:ss) = do
280+chunkBSConduit (s : ss) = do
222− bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
281+ bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
282+ if
223− if | B.length bs == s -> C.yield bs >> chunkBSConduit ss
283+ | B.length bs == s -> C.yield bs >> chunkBSConduit ss
224− | B.length bs > 0 -> C.yield bs
284+ | B.length bs > 0 -> C.yield bs
225− | otherwise -> return ()
226−
227−-- | Select part sizes - the logic is that the minimum part-size will
228−-- be 64MiB.
229−selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
230−selectPartSizes size = uncurry (List.zip3 [1..]) $
231− List.unzip $ loop 0 size
232− where
233− ceil :: Double -> Int64
234− ceil = ceiling
235− partSize = max minPartSize (ceil $ fromIntegral size /
236− fromIntegral maxMultipartParts)
237−
238− m = fromIntegral partSize
239− loop st sz
240− | st > sz = []
241− | st + m >= sz = [(st, sz - st)]
242− | otherwise = (st, m) : loop (st + m) sz
285+ | otherwise -> return ()
243−
244−lookupRegionCache :: Bucket -> Minio (Maybe Region)
245−lookupRegionCache b = do
246− rMVar <- asks mcRegionMap
247− rMap <- UM.readMVar rMVar
248− return $ Map.lookup b rMap
249−
250−addToRegionCache :: Bucket -> Region -> Minio ()
251−addToRegionCache b region = do
252− rMVar <- asks mcRegionMap
253− UM.modifyMVar_ rMVar $ return . Map.insert b region
254−
255−deleteFromRegionCache :: Bucket -> Minio ()
256−deleteFromRegionCache b = do
257− rMVar <- asks mcRegionMap
258− UM.modifyMVar_ rMVar $ return . Map.delete b
src/Network/Minio/XmlCommon.hsnew file+65
@@ -0,0 +1,65 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.XmlCommon where
18+
19+import qualified Data.Text as T
20+import Data.Text.Read (decimal)
21+import Data.Time (UTCTime)
22+import Data.Time.Format.ISO8601 (iso8601ParseM)
23+import Lib.Prelude (throwIO)
24+import Network.Minio.Errors
25+import Text.XML (Name (Name), def, parseLBS)
26+import Text.XML.Cursor (Axis, Cursor, content, element, fromDocument, laxElement, ($/), (&/))
27+
28+s3Name :: Text -> Text -> Name
29+s3Name ns s = Name s (Just ns) Nothing
30+
31+uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
32+uncurry4 f (a, b, c, d) = f a b c d
33+
34+uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
35+uncurry6 f (a, b, c, d, e, g) = f a b c d e g
36+
37+-- | Parse time strings from XML
38+parseS3XMLTime :: (MonadIO m) => Text -> m UTCTime
39+parseS3XMLTime t =
40+ maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
41+ iso8601ParseM $
42+ toString t
43+
44+parseDecimal :: (MonadIO m, Integral a) => Text -> m a
45+parseDecimal numStr =
46+ either (throwIO . MErrVXmlParse . show) return $
47+ fst <$> decimal numStr
48+
49+parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
50+parseDecimals numStr = forM numStr parseDecimal
51+
52+s3Elem :: Text -> Text -> Axis
53+s3Elem ns = element . s3Name ns
54+
55+parseRoot :: (MonadIO m) => LByteString -> m Cursor
56+parseRoot =
57+ either (throwIO . MErrVXmlParse . show) (return . fromDocument)
58+ . parseLBS def
59+
60+parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
61+parseErrResponse xmldata = do
62+ r <- parseRoot xmldata
63+ let code = T.concat $ r $/ laxElement "Code" &/ content
64+ message = T.concat $ r $/ laxElement "Message" &/ content
65+ return $ toServiceErr code message
src/Network/Minio/XmlGenerator.hs+165−99
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,89 +15,112 @@
15--
15--
16
16
17module Network.Minio.XmlGenerator
17module Network.Minio.XmlGenerator
18− ( mkCreateBucketConfig
18+ ( mkCreateBucketConfig,
19− , mkCompleteMultipartUploadRequest
19+ mkCompleteMultipartUploadRequest,
20− , mkPutNotificationRequest
20+ mkPutNotificationRequest,
21− , mkSelectRequest
21+ mkSelectRequest,
22+ )
22− ) where
23+where
23−
24
24
25import qualified Data.ByteString.Lazy as LBS
25import qualified Data.ByteString.Lazy as LBS
26−import qualified Data.HashMap.Strict as H
26+import qualified Data.Text as T
27−import qualified Data.Text as T
27+import Network.Minio.Data
28+import Network.Minio.XmlCommon
28−import Text.XML
29+import Text.XML
29−
30−import Lib.Prelude
31−
32−import Network.Minio.Data
33−
34
30
35-- | Create a bucketConfig request body XML
31-- | Create a bucketConfig request body XML
36mkCreateBucketConfig :: Text -> Region -> ByteString
32mkCreateBucketConfig :: Text -> Region -> ByteString
37mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
33mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
38 where
34 where
39− s3Element n = Element (s3Name ns n) mempty
35+ s3Element n = Element (s3Name ns n) mempty
36+ root =
37+ s3Element
40− root = s3Element "CreateBucketConfiguration"
38+ "CreateBucketConfiguration"
39+ [ NodeElement $
40+ s3Element
41− [ NodeElement $ s3Element "LocationConstraint"
41+ "LocationConstraint"
42− [ NodeContent location]
42+ [NodeContent location]
43 ]
43 ]
44− bucketConfig = Document (Prologue [] Nothing []) root []
44+ bucketConfig = Document (Prologue [] Nothing []) root []
45
45
46-- | Create a completeMultipartUpload request body XML
46-- | Create a completeMultipartUpload request body XML
47mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
47mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
48mkCompleteMultipartUploadRequest partInfo =
48mkCompleteMultipartUploadRequest partInfo =
49 LBS.toStrict $ renderLBS def cmur
49 LBS.toStrict $ renderLBS def cmur
50 where
50 where
51+ root =
51− root = Element "CompleteMultipartUpload" mempty $
52+ Element "CompleteMultipartUpload" mempty $
52− map (NodeElement . mkPart) partInfo
53+ map (NodeElement . mkPart) partInfo
53− mkPart (n, etag) = Element "Part" mempty
54+ mkPart (n, etag) =
55+ Element
56+ "Part"
57+ mempty
54− [ NodeElement $ Element "PartNumber" mempty
58+ [ NodeElement $
59+ Element
60+ "PartNumber"
61+ mempty
55− [NodeContent $ T.pack $ show n]
62+ [NodeContent $ T.pack $ show n],
56− , NodeElement $ Element "ETag" mempty
63+ NodeElement $
64+ Element
65+ "ETag"
66+ mempty
57− [NodeContent etag]
67+ [NodeContent etag]
58− ]
68+ ]
59 cmur = Document (Prologue [] Nothing []) root []
69 cmur = Document (Prologue [] Nothing []) root []
60
70
61-- Simplified XML representation without element attributes.
71-- Simplified XML representation without element attributes.
72+data XNode
62−data XNode = XNode Text [XNode]
73+ = XNode Text [XNode]
63− | XLeaf Text Text
74+ | XLeaf Text Text
64− deriving (Eq, Show)
75+ deriving stock (Eq, Show)
65
76
66toXML :: Text -> XNode -> ByteString
77toXML :: Text -> XNode -> ByteString
78+toXML ns node =
79+ LBS.toStrict $
67−toXML ns node = LBS.toStrict $ renderLBS def $
80+ renderLBS def $
68− Document (Prologue [] Nothing []) (xmlNode node) []
81+ Document (Prologue [] Nothing []) (xmlNode node) []
69 where
82 where
70 xmlNode :: XNode -> Element
83 xmlNode :: XNode -> Element
84+ xmlNode (XNode name nodes) =
71− xmlNode (XNode name nodes) = Element (s3Name ns name) mempty $
85+ Element (s3Name ns name) mempty $
72− map (NodeElement . xmlNode) nodes
86+ map (NodeElement . xmlNode) nodes
73− xmlNode (XLeaf name content) = Element (s3Name ns name) mempty
87+ xmlNode (XLeaf name content) =
88+ Element
89+ (s3Name ns name)
90+ mempty
74− [NodeContent content]
91+ [NodeContent content]
75
92
76class ToXNode a where
93class ToXNode a where
77 toXNode :: a -> XNode
94 toXNode :: a -> XNode
78
95
79instance ToXNode Event where
96instance ToXNode Event where
80− toXNode = XLeaf "Event" . show
97+ toXNode = XLeaf "Event" . toText
81
98
82instance ToXNode Notification where
99instance ToXNode Notification where
100+ toXNode (Notification qc tc lc) =
83− toXNode (Notification qc tc lc) = XNode "NotificationConfiguration" $
101+ XNode "NotificationConfiguration" $
84− map (toXNodesWithArnName "QueueConfiguration" "Queue") qc ++
102+ map (toXNodesWithArnName "QueueConfiguration" "Queue") qc
85− map (toXNodesWithArnName "TopicConfiguration" "Topic") tc ++
103+ ++ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc
86− map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
104+ ++ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
87
105
88toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
106toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
89−toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
107+toXNodesWithArnName eltName arnName (NotificationConfig itemId arn events fRule) =
108+ XNode eltName $
90− XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events ++
109+ [XLeaf "Id" itemId, XLeaf arnName arn]
110+ ++ map toXNode events
91− [toXNode fRule]
111+ ++ [toXNode fRule]
92
112
93instance ToXNode Filter where
113instance ToXNode Filter where
94 toXNode (Filter (FilterKey (FilterRules rules))) =
114 toXNode (Filter (FilterKey (FilterRules rules))) =
95 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
115 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
96
116
97getFRXNode :: FilterRule -> XNode
117getFRXNode :: FilterRule -> XNode
98−getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
118+getFRXNode (FilterRule n v) =
119+ XNode
120+ "FilterRule"
121+ [ XLeaf "Name" n,
99− , XLeaf "Value" v
122+ XLeaf "Value" v
100− ]
123+ ]
101
124
102mkPutNotificationRequest :: Text -> Notification -> ByteString
125mkPutNotificationRequest :: Text -> Notification -> ByteString
103mkPutNotificationRequest ns = toXML ns . toXNode
126mkPutNotificationRequest ns = toXML ns . toXNode
@@ -106,60 +129,103 @@ mkSelectRequest :: SelectRequest -> ByteString
106mkSelectRequest r = LBS.toStrict $ renderLBS def sr
129mkSelectRequest r = LBS.toStrict $ renderLBS def sr
107 where
130 where
108 sr = Document (Prologue [] Nothing []) root []
131 sr = Document (Prologue [] Nothing []) root []
132+ root =
109− root = Element "SelectRequest" mempty $
133+ Element "SelectRequest" mempty $
110− [ NodeElement (Element "Expression" mempty
134+ [ NodeElement
135+ ( Element
136+ "Expression"
137+ mempty
111− [NodeContent $ srExpression r])
138+ [NodeContent $ srExpression r]
139+ ),
140+ NodeElement
141+ ( Element
112− , NodeElement (Element "ExpressionType" mempty
142+ "ExpressionType"
143+ mempty
113− [NodeContent $ show $ srExpressionType r])
144+ [NodeContent $ show $ srExpressionType r]
145+ ),
146+ NodeElement
114− , NodeElement (Element "InputSerialization" mempty $
147+ ( Element "InputSerialization" mempty $
115− inputSerializationNodes $ srInputSerialization r)
148+ inputSerializationNodes $
149+ srInputSerialization r
150+ ),
151+ NodeElement
116− , NodeElement (Element "OutputSerialization" mempty $
152+ ( Element "OutputSerialization" mempty $
117− outputSerializationNodes $ srOutputSerialization r)
153+ outputSerializationNodes $
154+ srOutputSerialization r
155+ )
156+ ]
118− ] ++ maybe [] reqProgElem (srRequestProgressEnabled r)
157+ ++ maybe [] reqProgElem (srRequestProgressEnabled r)
119− reqProgElem enabled = [NodeElement
158+ reqProgElem enabled =
159+ [ NodeElement
160+ ( Element
120− (Element "RequestProgress" mempty
161+ "RequestProgress"
162+ mempty
121− [NodeElement
163+ [ NodeElement
122− (Element "Enabled" mempty
164+ ( Element
165+ "Enabled"
166+ mempty
123− [NodeContent
167+ [ NodeContent
124− (if enabled then "TRUE" else "FALSE")]
168+ (if enabled then "TRUE" else "FALSE")
169+ ]
125− )
170+ )
126− ]
171+ ]
127− )
172+ )
128− ]
173+ ]
174+ inputSerializationNodes is =
129− inputSerializationNodes is = comprTypeNode (isCompressionType is) ++
175+ comprTypeNode (isCompressionType is)
130− [NodeElement $ formatNode (isFormatInfo is)]
176+ ++ [NodeElement $ formatNode (isFormatInfo is)]
131− comprTypeNode (Just c) = [NodeElement $ Element "CompressionType" mempty
177+ comprTypeNode (Just c) =
178+ [ NodeElement $
179+ Element
180+ "CompressionType"
181+ mempty
132− [NodeContent $ case c of
182+ [ NodeContent $ case c of
133− CompressionTypeNone -> "NONE"
183+ CompressionTypeNone -> "NONE"
134− CompressionTypeGzip -> "GZIP"
184+ CompressionTypeGzip -> "GZIP"
135− CompressionTypeBzip2 -> "BZIP2"
185+ CompressionTypeBzip2 -> "BZIP2"
136− ]
186+ ]
137− ]
187+ ]
138 comprTypeNode Nothing = []
188 comprTypeNode Nothing = []
139−
140 kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
189 kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
141− formatNode (InputFormatCSV (CSVProp h)) =
190+ formatNode (InputFormatCSV c) =
142− Element "CSV" mempty
191+ Element
192+ "CSV"
193+ mempty
143− (map NodeElement $ map kvElement $ H.toList h)
194+ (map (NodeElement . kvElement) (csvPropsList c))
144 formatNode (InputFormatJSON p) =
195 formatNode (InputFormatJSON p) =
145− Element "JSON" mempty
196+ Element
197+ "JSON"
198+ mempty
146− [NodeElement
199+ [ NodeElement
147− (Element "Type" mempty
200+ ( Element
201+ "Type"
202+ mempty
148− [NodeContent $ case jsonipType p of
203+ [ NodeContent $ case jsonipType p of
149− JSONTypeDocument -> "DOCUMENT"
204+ JSONTypeDocument -> "DOCUMENT"
150− JSONTypeLines -> "LINES"
205+ JSONTypeLines -> "LINES"
151− ]
206+ ]
152− )
207+ )
153− ]
208+ ]
154 formatNode InputFormatParquet = Element "Parquet" mempty []
209 formatNode InputFormatParquet = Element "Parquet" mempty []
155−
156 outputSerializationNodes (OutputSerializationJSON j) =
210 outputSerializationNodes (OutputSerializationJSON j) =
211+ [ NodeElement
157− [NodeElement (Element "JSON" mempty $
212+ ( Element "JSON" mempty $
213+ rdElem $
158− rdElem $ jsonopRecordDelimiter j)]
214+ jsonopRecordDelimiter j
215+ )
216+ ]
159− outputSerializationNodes (OutputSerializationCSV (CSVProp h)) =
217+ outputSerializationNodes (OutputSerializationCSV c) =
160− [NodeElement $ Element "CSV" mempty
218+ [ NodeElement $
219+ Element
220+ "CSV"
221+ mempty
161− (map NodeElement $ map kvElement $ H.toList h)]
222+ (map (NodeElement . kvElement) (csvPropsList c))
162−
223+ ]
163 rdElem Nothing = []
224 rdElem Nothing = []
225+ rdElem (Just t) =
226+ [ NodeElement $
227+ Element
164− rdElem (Just t) = [NodeElement $ Element "RecordDelimiter" mempty
228+ "RecordDelimiter"
229+ mempty
165− [NodeContent t]]
230+ [NodeContent t]
231+ ]
src/Network/Minio/XmlParser.hs+107−152
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,75 +15,38 @@
15--
15--
16
16
17module Network.Minio.XmlParser
17module Network.Minio.XmlParser
18− ( parseListBuckets
18+ ( parseListBuckets,
19− , parseLocation
19+ parseLocation,
20− , parseNewMultipartUpload
20+ parseNewMultipartUpload,
21− , parseCompleteMultipartUploadResponse
21+ parseCompleteMultipartUploadResponse,
22− , parseCopyObjectResponse
22+ parseCopyObjectResponse,
23− , parseListObjectsResponse
23+ parseListObjectsResponse,
24− , parseListObjectsV1Response
24+ parseListObjectsV1Response,
25− , parseListUploadsResponse
25+ parseListUploadsResponse,
26− , parseListPartsResponse
26+ parseListPartsResponse,
27− , parseErrResponse
27+ parseErrResponse,
28− , parseNotification
28+ parseNotification,
29− , parseSelectProgress
29+ parseSelectProgress,
30+ )
30− ) where
31+where
31
32
32import qualified Data.ByteString.Lazy as LB
33import qualified Data.ByteString.Lazy as LB
34+import qualified Data.HashMap.Strict as H
33−import Data.List (zip3, zip4, zip5)
35+import Data.List (zip4, zip6)
34−import qualified Data.Map as Map
36+import qualified Data.Text as T
35−import qualified Data.Text as T
36−import Data.Text.Read (decimal)
37−import Data.Time
37+import Data.Time
38−import Text.XML
38+import Network.Minio.Data
39+import Network.Minio.XmlCommon
39−import Text.XML.Cursor hiding (bool)
40+import Text.XML.Cursor hiding (bool)
40−
41−import Lib.Prelude
42−
43−import Network.Minio.Data
44−import Network.Minio.Errors
45−
46−
47−-- | Represent the time format string returned by S3 API calls.
48−s3TimeFormat :: [Char]
49−s3TimeFormat = iso8601DateFormat $ Just "%T%QZ"
50−
51−-- | Helper functions.
52−uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
53−uncurry4 f (a, b, c, d) = f a b c d
54−
55−uncurry5 :: (a -> b -> c -> d -> e -> f) -> (a, b, c, d, e) -> f
56−uncurry5 f (a, b, c, d, e) = f a b c d e
57−
58−-- | Parse time strings from XML
59−parseS3XMLTime :: (MonadIO m) => Text -> m UTCTime
60−parseS3XMLTime = either (throwIO . MErrVXmlParse) return
61− . parseTimeM True defaultTimeLocale s3TimeFormat
62− . T.unpack
63−
64−parseDecimal :: (MonadIO m, Integral a) => Text -> m a
65−parseDecimal numStr = either (throwIO . MErrVXmlParse . show) return $
66− fst <$> decimal numStr
67−
68−parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
69−parseDecimals numStr = forM numStr parseDecimal
70−
71−s3Elem :: Text -> Text -> Axis
72−s3Elem ns = element . s3Name ns
73−
74−parseRoot :: (MonadIO m) => LByteString -> m Cursor
75−parseRoot = either (throwIO . MErrVXmlParse . show) (return . fromDocument)
76− . parseLBS def
77
41
78-- | Parse the response XML of a list buckets call.
42-- | Parse the response XML of a list buckets call.
79parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
43parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
80parseListBuckets xmldata = do
44parseListBuckets xmldata = do
81 r <- parseRoot xmldata
45 r <- parseRoot xmldata
82 ns <- asks getSvcNamespace
46 ns <- asks getSvcNamespace
83− let
84− s3Elem' = s3Elem ns
47+ let s3Elem' = s3Elem ns
85− names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
48+ names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
86− timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
49+ timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
87
50
88 times <- mapM parseS3XMLTime timeStrings
51 times <- mapM parseS3XMLTime timeStrings
89 return $ zipWith BucketInfo names times
52 return $ zipWith BucketInfo names times
@@ -116,40 +79,38 @@ parseCopyObjectResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =
116parseCopyObjectResponse xmldata = do
79parseCopyObjectResponse xmldata = do
117 r <- parseRoot xmldata
80 r <- parseRoot xmldata
118 ns <- asks getSvcNamespace
81 ns <- asks getSvcNamespace
119− let
120− s3Elem' = s3Elem ns
82+ let s3Elem' = s3Elem ns
121− mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
83+ mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
122
84
123 mtime <- parseS3XMLTime mtimeStr
85 mtime <- parseS3XMLTime mtimeStr
124 return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
86 return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
125
87
126-- | Parse the response XML of a list objects v1 call.
88-- | Parse the response XML of a list objects v1 call.
89+parseListObjectsV1Response ::
127−parseListObjectsV1Response :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
90+ (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
91+ LByteString ->
128− => LByteString -> m ListObjectsV1Result
92+ m ListObjectsV1Result
129parseListObjectsV1Response xmldata = do
93parseListObjectsV1Response xmldata = do
130 r <- parseRoot xmldata
94 r <- parseRoot xmldata
131 ns <- asks getSvcNamespace
95 ns <- asks getSvcNamespace
132− let
133− s3Elem' = s3Elem ns
96+ let s3Elem' = s3Elem ns
134− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
97+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
135−
136− nextMarker = headMay $ r $/ s3Elem' "NextMarker" &/ content
98+ nextMarker = listToMaybe $ r $/ s3Elem' "NextMarker" &/ content
137−
138− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
99+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
139−
140− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
100+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
141− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
101+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
142− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
102+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
143− -- if response xml contains empty etag response fill them with as
103+ -- if response xml contains empty etag response fill them with as
144− -- many empty Text for the zip4 below to work as intended.
104+ -- many empty Text for the zip4 below to work as intended.
145− etags = etagsList ++ repeat ""
105+ etags = etagsList ++ repeat ""
146− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
106+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
147
107
148 modTimes <- mapM parseS3XMLTime modTimeStr
108 modTimes <- mapM parseS3XMLTime modTimeStr
149 sizes <- parseDecimals sizeStr
109 sizes <- parseDecimals sizeStr
150
110
151− let
111+ let objects =
112+ map (uncurry6 ObjectInfo) $
152− objects = map (uncurry5 ObjectInfo) $ zip5 keys modTimes etags sizes (repeat Map.empty)
113+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
153
114
154 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
115 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
155
116
@@ -158,27 +119,24 @@ parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
158parseListObjectsResponse xmldata = do
119parseListObjectsResponse xmldata = do
159 r <- parseRoot xmldata
120 r <- parseRoot xmldata
160 ns <- asks getSvcNamespace
121 ns <- asks getSvcNamespace
161− let
162− s3Elem' = s3Elem ns
122+ let s3Elem' = s3Elem ns
163− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
123+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
164−
165− nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ content
124+ nextToken = listToMaybe $ r $/ s3Elem' "NextContinuationToken" &/ content
166−
167− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
125+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
168−
169− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
126+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
170− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
127+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
171− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
128+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
172− -- if response xml contains empty etag response fill them with as
129+ -- if response xml contains empty etag response fill them with as
173− -- many empty Text for the zip4 below to work as intended.
130+ -- many empty Text for the zip4 below to work as intended.
174− etags = etagsList ++ repeat ""
131+ etags = etagsList ++ repeat ""
175− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
132+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
176
133
177 modTimes <- mapM parseS3XMLTime modTimeStr
134 modTimes <- mapM parseS3XMLTime modTimeStr
178 sizes <- parseDecimals sizeStr
135 sizes <- parseDecimals sizeStr
179
136
180− let
137+ let objects =
138+ map (uncurry6 ObjectInfo) $
181− objects = map (uncurry5 ObjectInfo) $ zip5 keys modTimes etags sizes (repeat Map.empty)
139+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
182
140
183 return $ ListObjectsResult hasMore nextToken objects prefixes
141 return $ ListObjectsResult hasMore nextToken objects prefixes
184
142
@@ -187,20 +145,18 @@ parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
187parseListUploadsResponse xmldata = do
145parseListUploadsResponse xmldata = do
188 r <- parseRoot xmldata
146 r <- parseRoot xmldata
189 ns <- asks getSvcNamespace
147 ns <- asks getSvcNamespace
190− let
191− s3Elem' = s3Elem ns
148+ let s3Elem' = s3Elem ns
192− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
149+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
193− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
150+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
194− nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content
151+ nextKey = listToMaybe $ r $/ s3Elem' "NextKeyMarker" &/ content
195− nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content
152+ nextUpload = listToMaybe $ r $/ s3Elem' "NextUploadIdMarker" &/ content
196− uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
153+ uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
197− uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
154+ uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
198− uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
155+ uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
199
156
200 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
157 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
201
158
202− let
203− uploads = zip3 uploadKeys uploadIds uploadInitTimes
159+ let uploads = zip3 uploadKeys uploadIds uploadInitTimes
204
160
205 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
161 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
206
162
@@ -208,34 +164,25 @@ parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
208parseListPartsResponse xmldata = do
164parseListPartsResponse xmldata = do
209 r <- parseRoot xmldata
165 r <- parseRoot xmldata
210 ns <- asks getSvcNamespace
166 ns <- asks getSvcNamespace
211− let
212− s3Elem' = s3Elem ns
167+ let s3Elem' = s3Elem ns
213− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
168+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
214− nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content
169+ nextPartNumStr = listToMaybe $ r $/ s3Elem' "NextPartNumberMarker" &/ content
215− partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
170+ partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
216− partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
171+ partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
217− partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
172+ partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
218− partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
173+ partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
219
174
220 partModTimes <- mapM parseS3XMLTime partModTimeStr
175 partModTimes <- mapM parseS3XMLTime partModTimeStr
221 partSizes <- parseDecimals partSizeStr
176 partSizes <- parseDecimals partSizeStr
222 partNumbers <- parseDecimals partNumberStr
177 partNumbers <- parseDecimals partNumberStr
223 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
178 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
224
179
225− let
180+ let partInfos =
226− partInfos = map (uncurry4 ObjectPartInfo) $
181+ map (uncurry4 ObjectPartInfo) $
227− zip4 partNumbers partETags partSizes partModTimes
182+ zip4 partNumbers partETags partSizes partModTimes
228
183
229 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
184 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
230
185
231−
232−parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
233−parseErrResponse xmldata = do
234− r <- parseRoot xmldata
235− let code = T.concat $ r $/ element "Code" &/ content
236− message = T.concat $ r $/ element "Message" &/ content
237− return $ toServiceErr code message
238−
239parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
186parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
240parseNotification xmldata = do
187parseNotification xmldata = do
241 r <- parseRoot xmldata
188 r <- parseRoot xmldata
@@ -244,32 +191,40 @@ parseNotification xmldata = do
244 qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
191 qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
245 tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
192 tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
246 lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
193 lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
194+ Notification
247− Notification <$> (mapM (parseNode ns "Queue") qcfg)
195+ <$> mapM (parseNode ns "Queue") qcfg
248− <*> (mapM (parseNode ns "Topic") tcfg)
196+ <*> mapM (parseNode ns "Topic") tcfg
249− <*> (mapM (parseNode ns "CloudFunction") lcfg)
197+ <*> mapM (parseNode ns "CloudFunction") lcfg
250 where
198 where
251−
252 getFilterRule ns c =
199 getFilterRule ns c =
253 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
200 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
254 value = T.concat $ c $/ s3Elem ns "Value" &/ content
201 value = T.concat $ c $/ s3Elem ns "Value" &/ content
255− in FilterRule name value
202+ in FilterRule name value
256−
257 parseNode ns arnName nodeData = do
203 parseNode ns arnName nodeData = do
258 let c = fromNode nodeData
204 let c = fromNode nodeData
259− id = T.concat $ c $/ s3Elem ns "Id" &/ content
205+ itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
260 arn = T.concat $ c $/ s3Elem ns arnName &/ content
206 arn = T.concat $ c $/ s3Elem ns arnName &/ content
261− events = catMaybes $ map textToEvent $ c $/ s3Elem ns "Event" &/ content
207+ events = mapMaybe textToEvent (c $/ s3Elem ns "Event" &/ content)
208+ rules =
209+ c
262− rules = c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key" &/
210+ $/ s3Elem ns "Filter"
211+ &/ s3Elem ns "S3Key"
263− s3Elem ns "FilterRule" &| getFilterRule ns
212+ &/ s3Elem ns "FilterRule"
213+ &| getFilterRule ns
214+ return $
264− return $ NotificationConfig id arn events
215+ NotificationConfig
216+ itemId
217+ arn
218+ events
265− (Filter $ FilterKey $ FilterRules rules)
219+ (Filter $ FilterKey $ FilterRules rules)
266−
220+
267−parseSelectProgress :: MonadIO m => ByteString -> m Progress
221+parseSelectProgress :: (MonadIO m) => ByteString -> m Progress
268parseSelectProgress xmldata = do
222parseSelectProgress xmldata = do
269− r <- parseRoot $ LB.fromStrict xmldata
223+ r <- parseRoot $ LB.fromStrict xmldata
270− let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
224+ let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
271− bProcessed = T.concat $ r $/element "BytesProcessed" &/ content
225+ bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content
272− bReturned = T.concat $ r $/element "BytesReturned" &/ content
226+ bReturned = T.concat $ r $/ element "BytesReturned" &/ content
227+ Progress
273− Progress <$> parseDecimal bScanned
228+ <$> parseDecimal bScanned
274− <*> parseDecimal bProcessed
229+ <*> parseDecimal bProcessed
275− <*> parseDecimal bReturned
230+ <*> parseDecimal bReturned
stack.yaml+4−4
@@ -15,7 +15,7 @@
15# resolver:
15# resolver:
16# name: custom-snapshot
16# name: custom-snapshot
17# location: "./custom-snapshot.yaml"
17# location: "./custom-snapshot.yaml"
18−resolver: lts-13.1
18+resolver: lts-22.19
19
19
20# User packages to be built.
20# User packages to be built.
21# Various formats can be used as shown in the example below.
21# Various formats can be used as shown in the example below.
@@ -36,17 +36,17 @@ resolver: lts-13.1
36# non-dependency (i.e. a user package), and its test suites and benchmarks
36# non-dependency (i.e. a user package), and its test suites and benchmarks
37# will not be run. This is useful for tweaking upstream packages.
37# will not be run. This is useful for tweaking upstream packages.
38packages:
38packages:
39−- '.'
39+ - "."
40# Dependency packages to be pulled from upstream that are not in the resolver
40# Dependency packages to be pulled from upstream that are not in the resolver
41# (e.g., acme-missiles-0.3)
41# (e.g., acme-missiles-0.3)
42−extra-deps: []
42+extra-deps:
43+ - crypton-connection-0.3.2
43
44
44# Override default flag values for local packages and extra-deps
45# Override default flag values for local packages and extra-deps
45flags: {}
46flags: {}
46
47
47# Extra package databases containing global packages
48# Extra package databases containing global packages
48extra-package-dbs: []
49extra-package-dbs: []
49−
50# Control whether we use the GHC we find on the path
50# Control whether we use the GHC we find on the path
51# system-ghc: true
51# system-ghc: true
52#
52#
stack.yaml.locknew file+19
@@ -0,0 +1,19 @@
1+# This file was autogenerated by Stack.
2+# You should not edit this file by hand.
3+# For more information, please see the documentation at:
4+# https://docs.haskellstack.org/en/stable/lock_files
5+
6+packages:
7+- completed:
8+ hackage: crypton-connection-0.3.2@sha256:c7937edc25ab022bcf167703f2ec5ab73b62908e545bb587d2aa42b33cd6f6cc,1581
9+ pantry-tree:
10+ sha256: f986ad29b008cbe5732606e9cde1897191c486a2f1f169a4cb75fd915bce397c
11+ size: 394
12+ original:
13+ hackage: crypton-connection-0.3.2
14+snapshots:
15+- completed:
16+ sha256: e5cac927cf7ccbd52aa41476baa68b88c564ee6ddc3bc573dbf4210069287fe7
17+ size: 713340
18+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/19.yaml
19+ original: lts-22.19
test/LiveServer.hs+807−461
@@ -1,6 +1,5 @@
1−{-# LANGUAGE OverloadedStrings #-}
2--
1--
3−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
4--
3--
5-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
6-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,36 +14,34 @@
15-- limitations under the License.
14-- limitations under the License.
16--
15--
17
16
18−import qualified Test.QuickCheck as Q
19−import Test.Tasty
20−import Test.Tasty.HUnit
21−import Test.Tasty.QuickCheck as QC
22−
23−import Conduit (replicateC)
17+import Conduit (replicateC)
18+import qualified Conduit as C
24−import qualified Control.Monad.Trans.Resource as R
19+import qualified Control.Monad.Trans.Resource as R
25−import qualified Data.ByteString as BS
20+import qualified Data.ByteString as BS
26−import Data.Conduit (yield)
21+import Data.Conduit (yield)
27−import qualified Data.Conduit as C
22+import qualified Data.Conduit.Binary as CB
28−import qualified Data.Conduit.Binary as CB
29−import Data.Conduit.Combinators (sinkList)
23+import Data.Conduit.Combinators (sinkList)
30−import qualified Data.Map.Strict as Map
24+import qualified Data.HashMap.Strict as H
31−import qualified Data.Text as T
25+import qualified Data.Text as T
32−import Data.Time (fromGregorian)
26+import Data.Time (fromGregorian)
33−import qualified Data.Time as Time
27+import qualified Data.Time as Time
28+import Lib.Prelude
34import qualified Network.HTTP.Client.MultipartFormData as Form
29import qualified Network.HTTP.Client.MultipartFormData as Form
35−import qualified Network.HTTP.Conduit as NC
30+import qualified Network.HTTP.Conduit as NC
36−import qualified Network.HTTP.Types as HT
31+import qualified Network.HTTP.Types as HT
32+import Network.Minio
33+import Network.Minio.Credentials (Creds (CredsStatic))
34+import Network.Minio.Data
35+import Network.Minio.Data.Crypto
36+import Network.Minio.S3API
37+import Network.Minio.Utils
37−import System.Directory (getTemporaryDirectory)
38+import System.Directory (getTemporaryDirectory)
38−import System.Environment (lookupEnv)
39+import qualified System.Environment as Env
40+import qualified Test.QuickCheck as Q
41+import Test.Tasty
42+import Test.Tasty.HUnit
43+import Test.Tasty.QuickCheck as QC
39−import qualified System.IO as SIO
44+import qualified UnliftIO.IO as UIO
40−
41−import Lib.Prelude
42−
43−import Network.Minio
44−import Network.Minio.Data
45−import Network.Minio.PutObject
46−import Network.Minio.S3API
47−import Network.Minio.Utils
48
45
49main :: IO ()
46main :: IO ()
50main = defaultMain tests
47main = defaultMain tests
@@ -52,41 +49,107 @@ main = defaultMain tests
52tests :: TestTree
49tests :: TestTree
53tests = testGroup "Tests" [liveServerUnitTests]
50tests = testGroup "Tests" [liveServerUnitTests]
54
51
52+lookupTestEnv :: IO (Maybe ConnectInfo, (AccessKey, SecretKey))
53+lookupTestEnv = do
54+ val <- Env.lookupEnv "MINIO_LOCAL"
55+ secure <- Env.lookupEnv "MINIO_SECURE"
56+ let localServerMaybe = case (val, secure) of
57+ (Just _, Just _) -> Just "https://localhost:9000"
58+ (Just _, Nothing) -> Just "http://localhost:9000"
59+ _ -> Nothing
60+ accessKeyE <- Env.lookupEnv "MINIO_ACCESS_KEY"
61+ let accessKey = maybe "minioadmin" fromString accessKeyE
62+ secretKeyE <- Env.lookupEnv "MINIO_SECRET_KEY"
63+ let secretKey = maybe "minioadmin" fromString secretKeyE
64+ return (localServerMaybe, (accessKey, secretKey))
65+
66+loadTestServerConnInfo :: IO ConnectInfo
67+loadTestServerConnInfo = do
68+ (localServerMaybe, (ak, sk)) <- lookupTestEnv
69+ return $ case localServerMaybe of
70+ Just localServer -> do
71+ setCreds (CredentialValue ak sk mempty) localServer
72+ _ -> minioPlayCI
73+
74+loadTestServerConnInfoSTS :: IO ConnectInfo
75+loadTestServerConnInfoSTS = do
76+ (localServerMaybe, (ak, sk)) <- lookupTestEnv
77+ case localServerMaybe of
78+ Just localServer -> do
79+ let creds = CredentialValue ak sk mempty
80+ assumeRole =
81+ STSAssumeRole
82+ { sarCredentials = creds,
83+ sarOptions = defaultSTSAssumeRoleOptions
84+ }
85+ setSTSCredential assumeRole localServer
86+ Nothing -> do
87+ let creds = case connectCreds minioPlayCI of
88+ CredsStatic c -> c
89+ _ -> error "unexpected play creds"
90+ assumeRole =
91+ STSAssumeRole
92+ { sarCredentials = creds,
93+ sarOptions = defaultSTSAssumeRoleOptions
94+ }
95+ setSTSCredential assumeRole minioPlayCI
96+
55-- conduit that generates random binary stream of given length
97-- conduit that generates random binary stream of given length
56−randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
98+randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
57−randomDataSrc s' = genBS s'
99+randomDataSrc = genBS
58 where
100 where
101+ concatIt bs n =
102+ BS.concat $
59− concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++
103+ replicate (fromIntegral q) bs
60− [BS.take (fromIntegral r) bs]
104+ ++ [BS.take (fromIntegral r) bs]
105+ where
61− where (q, r) = n `divMod` fromIntegral (BS.length bs)
106+ (q, r) = n `divMod` fromIntegral (BS.length bs)
62−
63 genBS s = do
107 genBS s = do
64 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
108 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
65 let byteArr64 = BS.pack w8s
109 let byteArr64 = BS.pack w8s
66 if s < oneMiB
110 if s < oneMiB
67 then yield $ concatIt byteArr64 s
111 then yield $ concatIt byteArr64 s
112+ else do
68− else do yield $ concatIt byteArr64 oneMiB
113+ yield $ concatIt byteArr64 oneMiB
69− genBS (s - oneMiB)
114+ genBS (s - oneMiB)
70
115
71−mkRandFile :: R.MonadResource m => Int64 -> m FilePath
116+mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
72mkRandFile size = do
117mkRandFile size = do
73− dir <- liftIO $ getTemporaryDirectory
118+ dir <- liftIO getTemporaryDirectory
74 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
119 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
75
120
76funTestBucketPrefix :: Text
121funTestBucketPrefix :: Text
77funTestBucketPrefix = "miniohstest-"
122funTestBucketPrefix = "miniohstest-"
78
123
124+funTestWithBucketNoSTS ::
125+ TestName ->
126+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
127+ TestTree
128+funTestWithBucketNoSTS t minioTest = testCaseSteps t $ \step -> do
129+ -- generate a random name for the bucket
130+ bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
131+ let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
132+ liftStep = liftIO . step
133+ connInfo <- loadTestServerConnInfo
134+ ret <- runMinio connInfo $ do
135+ liftStep $ "Creating bucket for test - " ++ t
136+ foundBucket <- bucketExists b
137+ liftIO $ foundBucket @?= False
138+ makeBucket b Nothing
139+ minioTest liftStep b
140+ deleteBucket b
141+ isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
142+
79−funTestWithBucket :: TestName
143+funTestWithBucket ::
144+ TestName ->
80− -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree
145+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
146+ TestTree
81funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
147funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
82 -- generate a random name for the bucket
148 -- generate a random name for the bucket
83 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
149 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
84 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
150 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
85 liftStep = liftIO . step
151 liftStep = liftIO . step
86− connInfo <- ( bool minioPlayCI
152+ connInfo <- loadTestServerConnInfo
87− ( setCreds (Credentials "minio" "minio123") "http://localhost:9000" )
88− . isJust
89− ) <$> lookupEnv "MINIO_LOCAL"
90 ret <- runMinio connInfo $ do
153 ret <- runMinio connInfo $ do
91 liftStep $ "Creating bucket for test - " ++ t
154 liftStep $ "Creating bucket for test - " ++ t
92 foundBucket <- bucketExists b
155 foundBucket <- bucketExists b
@@ -96,24 +159,40 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
96 deleteBucket b
159 deleteBucket b
97 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
160 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
98
161
162+ connInfoSTS <- loadTestServerConnInfoSTS
163+ let t' = t ++ " (with AssumeRole Credentials)"
164+ ret' <- runMinio connInfoSTS $ do
165+ liftStep $ "Creating bucket for test - " ++ t'
166+ foundBucket <- bucketExists b
167+ liftIO $ foundBucket @?= False
168+ makeBucket b Nothing
169+ minioTest liftStep b
170+ deleteBucket b
171+ isRight ret' @? ("Functional test " ++ t' ++ " failed => " ++ show ret')
172+
99liveServerUnitTests :: TestTree
173liveServerUnitTests :: TestTree
174+liveServerUnitTests =
175+ testGroup
100−liveServerUnitTests = testGroup "Unit tests against a live server"
176+ "Unit tests against a live server"
101− [ basicTests
177+ [ basicTests,
102− , listingTest
178+ listingTest,
103− , highLevelListingTest
179+ highLevelListingTest,
104− , lowLevelMultipartTest
180+ lowLevelMultipartTest,
105− , putObjectSizeTest
181+ putObjectSizeTest,
106− , putObjectNoSizeTest
182+ putObjectNoSizeTest,
107− , multipartTest
183+ multipartTest,
108− , putObjectContentTypeTest
184+ putObjectContentTypeTest,
109− , putObjectContentLanguageTest
185+ putObjectContentLanguageTest,
110− , putObjectStorageClassTest
186+ putObjectStorageClassTest,
187+ putObjectUserMetadataTest,
188+ getObjectTest,
111− , copyObjectTests
189+ copyObjectTests,
112− , presignedUrlFunTest
190+ presignedUrlFunTest,
113− , presignedPostPolicyFunTest
191+ presignedPostPolicyFunTest,
114− , bucketPolicyFunTest
192+ bucketPolicyFunTest,
115− , getNPutSSECTest
193+ getNPutSSECTest,
194+ assumeRoleRequestTest
116− ]
195+ ]
117
196
118basicTests :: TestTree
197basicTests :: TestTree
119basicTests = funTestWithBucket "Basic tests" $
198basicTests = funTestWithBucket "Basic tests" $
@@ -122,83 +201,109 @@ basicTests = funTestWithBucket "Basic tests" $
122 buckets <- getService
201 buckets <- getService
123 unless (length (filter (== bucket) $ map biName buckets) == 1) $
202 unless (length (filter (== bucket) $ map biName buckets) == 1) $
124 liftIO $
203 liftIO $
125− assertFailure ("The bucket " ++ show bucket ++
204+ assertFailure
205+ ( "The bucket "
206+ ++ show bucket
126− " was expected to exist.")
207+ ++ " was expected to exist."
208+ )
127
209
128 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
210 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
129 mbE <- try $ makeBucket bucket Nothing
211 mbE <- try $ makeBucket bucket Nothing
130 case mbE of
212 case mbE of
131 Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
213 Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
132− _ -> return ()
214+ _ -> return ()
133
215
134 step "makeBucket with an invalid bucket name and check for appropriate exception."
216 step "makeBucket with an invalid bucket name and check for appropriate exception."
135 invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
217 invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
136 case invalidMBE of
218 case invalidMBE of
137 Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
219 Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
138− _ -> return ()
220+ _ -> return ()
139
221
140 step "getLocation works"
222 step "getLocation works"
141 region <- getLocation bucket
223 region <- getLocation bucket
142 liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
224 liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
143
225
144 step "singlepart putObject works"
226 step "singlepart putObject works"
227+ testFilepath <- mkRandFile 200
145− fPutObject bucket "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
228+ fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
146
229
147 step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
230 step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
148− fpE <- try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
231+ fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
149 case fpE of
232 case fpE of
150 Left exn -> liftIO $ exn @?= NoSuchBucket
233 Left exn -> liftIO $ exn @?= NoSuchBucket
151− _ -> return ()
234+ _ -> return ()
152
235
153 outFile <- mkRandFile 0
236 outFile <- mkRandFile 0
154 step "simple fGetObject works"
237 step "simple fGetObject works"
155− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions
238+ fGetObject bucket "test-file" outFile defaultGetObjectOptions
156
239
157 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
240 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
158 step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
241 step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
242+ resE <-
243+ try $
244+ fGetObject
245+ bucket
246+ "test-file"
247+ outFile
159− resE <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
248+ defaultGetObjectOptions
160− gooIfUnmodifiedSince = (Just unmodifiedTime)
249+ { gooIfUnmodifiedSince = Just unmodifiedTime
161− }
250+ }
162 case resE of
251 case resE of
163 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
252 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
164− _ -> return ()
253+ _ -> return ()
165
254
166 step "fGetObject an object with no matching etag, check for exception"
255 step "fGetObject an object with no matching etag, check for exception"
256+ resE1 <-
257+ try $
258+ fGetObject
259+ bucket
260+ "test-file"
261+ outFile
167− resE1 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
262+ defaultGetObjectOptions
168− gooIfMatch = (Just "invalid-etag")
263+ { gooIfMatch = Just "invalid-etag"
169− }
264+ }
170 case resE1 of
265 case resE1 of
171 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
266 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
172− _ -> return ()
267+ _ -> return ()
173
268
174 step "fGetObject an object with no valid range, check for exception"
269 step "fGetObject an object with no valid range, check for exception"
270+ resE2 <-
271+ try $
272+ fGetObject
273+ bucket
274+ "test-file"
275+ outFile
175− resE2 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
276+ defaultGetObjectOptions
176− gooRange = (Just $ HT.ByteRangeFromTo 100 200)
277+ { gooRange = Just $ HT.ByteRangeFromTo 100 300
177− }
278+ }
178 case resE2 of
279 case resE2 of
179− Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
280+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
180− _ -> return ()
281+ _ -> return ()
181
282
182 step "fGetObject on object with a valid range"
283 step "fGetObject on object with a valid range"
284+ fGetObject
285+ bucket
286+ "test-file"
287+ outFile
183− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
288+ defaultGetObjectOptions
184− gooRange = (Just $ HT.ByteRangeFrom 1)
289+ { gooRange = Just $ HT.ByteRangeFrom 1
185− }
290+ }
186
291
187 step "fGetObject a non-existent object and check for NoSuchKey exception"
292 step "fGetObject a non-existent object and check for NoSuchKey exception"
188 resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
293 resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
189 case resE3 of
294 case resE3 of
190 Left exn -> liftIO $ exn @?= NoSuchKey
295 Left exn -> liftIO $ exn @?= NoSuchKey
191− _ -> return ()
296+ _ -> return ()
192
297
193 step "create new multipart upload works"
298 step "create new multipart upload works"
194 uid <- newMultipartUpload bucket "newmpupload" []
299 uid <- newMultipartUpload bucket "newmpupload" []
195− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
300+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
196
301
197 step "abort a new multipart upload works"
302 step "abort a new multipart upload works"
198 abortMultipartUpload bucket "newmpupload" uid
303 abortMultipartUpload bucket "newmpupload" uid
199
304
200 step "delete object works"
305 step "delete object works"
201− deleteObject bucket "lsb-release"
306+ deleteObject bucket "test-file"
202
307
203 step "statObject test"
308 step "statObject test"
204 let object = "sample"
309 let object = "sample"
@@ -208,257 +313,310 @@ basicTests = funTestWithBucket "Basic tests" $
208
313
209 step "get metadata of the object"
314 step "get metadata of the object"
210 res <- statObject bucket object defaultGetObjectOptions
315 res <- statObject bucket object defaultGetObjectOptions
211− liftIO $ (oiSize res) @?= 0
316+ liftIO $ oiSize res @?= 0
212
317
213 step "delete object"
318 step "delete object"
214 deleteObject bucket object
319 deleteObject bucket object
215
320
216lowLevelMultipartTest :: TestTree
321lowLevelMultipartTest :: TestTree
217lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
322lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
218− \step bucket -> do
323+ \step bucket -> do
219− -- low-level multipart operation tests.
324+ -- low-level multipart operation tests.
220− let object = "newmpupload"
325+ let object = "newmpupload"
221− mb15 = 15 * 1024 * 1024
326+ mb15 = 15 * 1024 * 1024
222
327
223− step "Prepare for low-level multipart tests."
328+ step "Prepare for low-level multipart tests."
224− step "create new multipart upload"
329+ step "create new multipart upload"
225− uid <- newMultipartUpload bucket object []
330+ uid <- newMultipartUpload bucket object []
226− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
331+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
227
332
228− randFile <- mkRandFile mb15
333+ randFile <- mkRandFile mb15
229
334
230− step "put object parts 1 of 1"
335+ step "put object parts 1 of 1"
336+ partInfo <-
231− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
337+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
232− partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
338+ putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
233
339
234− step "complete multipart"
340+ step "complete multipart"
235− void $ completeMultipartUpload bucket object uid [partInfo]
341+ void $ completeMultipartUpload bucket object uid [partInfo]
236
342
237− destFile <- mkRandFile 0
343+ destFile <- mkRandFile 0
238− step "Retrieve the created object and check size"
344+ step "Retrieve the created object and check size"
239− fGetObject bucket object destFile defaultGetObjectOptions
345+ fGetObject bucket object destFile defaultGetObjectOptions
240− gotSize <- withNewHandle destFile getFileSize
346+ gotSize <- withNewHandle destFile getFileSize
347+ liftIO $
348+ gotSize
241− liftIO $ gotSize == Right (Just mb15) @?
349+ == Right (Just mb15)
242− "Wrong file size of put file after getting"
350+ @? "Wrong file size of put file after getting"
243
351
244− step "Cleanup actions"
352+ step "Cleanup actions"
245− removeObject bucket object
353+ removeObject bucket object
246
354
247putObjectSizeTest :: TestTree
355putObjectSizeTest :: TestTree
248putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
356putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
249 \step bucket -> do
357 \step bucket -> do
250− -- putObject test (conduit source, size specified)
358+ -- putObject test (conduit source, size specified)
251− let obj = "msingle"
359+ let obj = "msingle"
252− mb1 = 1 * 1024 * 1024
360+ mb1 = 1 * 1024 * 1024
253
361
254− step "Prepare for putObject with from source with size."
362+ step "Prepare for putObject with from source with size."
255− rFile <- mkRandFile mb1
363+ rFile <- mkRandFile mb1
256
364
257− step "Upload single file."
365+ step "Upload single file."
258− putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
366+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
259
367
260− step "Retrieve and verify file size"
368+ step "Retrieve and verify file size"
261− destFile <- mkRandFile 0
369+ destFile <- mkRandFile 0
262− fGetObject bucket obj destFile defaultGetObjectOptions
370+ fGetObject bucket obj destFile defaultGetObjectOptions
263− gotSize <- withNewHandle destFile getFileSize
371+ gotSize <- withNewHandle destFile getFileSize
372+ liftIO $
373+ gotSize
264− liftIO $ gotSize == Right (Just mb1) @?
374+ == Right (Just mb1)
265− "Wrong file size of put file after getting"
375+ @? "Wrong file size of put file after getting"
266
376
267− step "Cleanup actions"
377+ step "Cleanup actions"
268− deleteObject bucket obj
378+ deleteObject bucket obj
269
379
270putObjectNoSizeTest :: TestTree
380putObjectNoSizeTest :: TestTree
271putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
381putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
272 \step bucket -> do
382 \step bucket -> do
273− -- putObject test (conduit source, no size specified)
383+ -- putObject test (conduit source, no size specified)
274− let obj = "mpart"
384+ let obj = "mpart"
275− mb70 = 70 * 1024 * 1024
385+ mb70 = 70 * 1024 * 1024
276
386
277− step "Prepare for putObject with from source without providing size."
387+ step "Prepare for putObject with from source without providing size."
278− rFile <- mkRandFile mb70
388+ rFile <- mkRandFile mb70
279
389
280− step "Upload multipart file."
390+ step "Upload multipart file."
281− putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
391+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
282
392
283− step "Retrieve and verify file size"
393+ step "Retrieve and verify file size"
284− destFile <- mkRandFile 0
394+ destFile <- mkRandFile 0
285− fGetObject bucket obj destFile defaultGetObjectOptions
395+ fGetObject bucket obj destFile defaultGetObjectOptions
286− gotSize <- withNewHandle destFile getFileSize
396+ gotSize <- withNewHandle destFile getFileSize
397+ liftIO $
398+ gotSize
287− liftIO $ gotSize == Right (Just mb70) @?
399+ == Right (Just mb70)
288− "Wrong file size of put file after getting"
400+ @? "Wrong file size of put file after getting"
289
401
290− step "Cleanup actions"
402+ step "Cleanup actions"
291− deleteObject bucket obj
403+ deleteObject bucket obj
292
404
293highLevelListingTest :: TestTree
405highLevelListingTest :: TestTree
294highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
406highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
295 \step bucket -> do
407 \step bucket -> do
296− step "High-level listObjects Test"
408+ step "High-level listObjects Test"
297− step "put 3 objects"
409+ step "put 3 objects"
298− let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
299− extractObjectsFromList os =
410+ let extractObjectsFromList =
300− mapM (\t -> case t of
411+ mapM
412+ ( \case
301− ListItemObject o -> Just $ oiObject o
413+ ListItemObject o -> Just $ oiObject o
302− _ -> Nothing) os
414+ _ -> Nothing
303− expectedNonRecList = ["o4", "dir/"]
415+ )
304− extractObjectsAndDirsFromList os =
416+ extractObjectsAndDirsFromList =
417+ map
305− map (\t -> case t of
418+ ( \case
306− ListItemObject o -> oiObject o
419+ ListItemObject o -> oiObject o
307− ListItemPrefix d -> d) os
420+ ListItemPrefix d -> d
308−
421+ )
422+ expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
423+ expectedNonRecList = ["o4", "dir/"]
424+
425+ testFilepath <- mkRandFile 200
309− forM_ expectedObjects $
426+ forM_ expectedObjects $
310− \obj -> fPutObject bucket obj "/etc/lsb-release" defaultPutObjectOptions
427+ \obj -> fPutObject bucket obj testFilepath defaultPutObjectOptions
311−
428+
312− step "High-level listing of objects"
429+ step "High-level listing of objects"
313− items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
430+ items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
431+ liftIO $
314− liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
432+ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
315 extractObjectsAndDirsFromList items
433 extractObjectsAndDirsFromList items
316
434
317− step "High-level recursive listing of objects"
435+ step "High-level recursive listing of objects"
318− objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
436+ objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
319
437
438+ liftIO
439+ $ assertEqual
320− liftIO $ assertEqual "Objects match failed!"
440+ "Objects match failed!"
321− (Just $ sort expectedObjects) $
441+ (Just $ sort expectedObjects)
322− extractObjectsFromList objects
442+ $ extractObjectsFromList objects
323
443
324− step "High-level listing of objects (version 1)"
444+ step "High-level listing of objects (version 1)"
325− itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
445+ itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
446+ liftIO $
326− liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
447+ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
327 extractObjectsAndDirsFromList itemsV1
448 extractObjectsAndDirsFromList itemsV1
328
449
329− step "High-level recursive listing of objects (version 1)"
450+ step "High-level recursive listing of objects (version 1)"
451+ objectsV1 <-
452+ C.runConduit $
330− objectsV1 <- C.runConduit $ listObjectsV1 bucket Nothing True C..|
453+ listObjectsV1 bucket Nothing True
331− sinkList
454+ C..| sinkList
332−
455+
333− liftIO $ assertEqual "Objects match failed!"
456+ liftIO
334− (Just $ sort expectedObjects) $
457+ $ assertEqual
335− extractObjectsFromList objectsV1
458+ "Objects match failed!"
336−
459+ (Just $ sort expectedObjects)
337− let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
460+ $ extractObjectsFromList objectsV1
461+
462+ let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
338− expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
463+ expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
339− step "High-level listing with prefix"
464+ step "High-level listing with prefix"
340− prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
465+ prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
466+ liftIO
467+ $ assertEqual
341− liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
468+ "Objects/Dirs under prefix match failed!"
469+ expectedPrefListing
342− expectedPrefListing $ extractObjectsAndDirsFromList prefItems
470+ $ extractObjectsAndDirsFromList prefItems
343−
471+
344− step "High-level listing with prefix recursive"
472+ step "High-level listing with prefix recursive"
345− prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
473+ prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
474+ liftIO
475+ $ assertEqual
346− liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
476+ "Objects/Dirs under prefix match recursive failed!"
477+ expectedPrefListingRec
347− expectedPrefListingRec $ extractObjectsFromList prefItemsRec
478+ $ extractObjectsFromList prefItemsRec
348−
479+
349− step "High-level listing with prefix (version 1)"
480+ step "High-level listing with prefix (version 1)"
350− prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
481+ prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
482+ liftIO
483+ $ assertEqual
351− liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
484+ "Objects/Dirs under prefix match failed!"
485+ expectedPrefListing
352− expectedPrefListing $ extractObjectsAndDirsFromList prefItemsV1
486+ $ extractObjectsAndDirsFromList prefItemsV1
353−
487+
354− step "High-level listing with prefix recursive (version 1)"
488+ step "High-level listing with prefix recursive (version 1)"
355− prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
489+ prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
490+ liftIO
491+ $ assertEqual
356− liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
492+ "Objects/Dirs under prefix match recursive failed!"
493+ expectedPrefListingRec
357− expectedPrefListingRec $ extractObjectsFromList prefItemsRecV1
494+ $ extractObjectsFromList prefItemsRecV1
358−
495+
359− step "Cleanup actions"
496+ step "Cleanup actions"
360− forM_ expectedObjects $
497+ forM_ expectedObjects $
361− \obj -> removeObject bucket obj
498+ \obj -> removeObject bucket obj
362−
499+
363− step "High-level listIncompleteUploads Test"
500+ step "High-level listIncompleteUploads Test"
364− let object = "newmpupload"
501+ let object = "newmpupload"
365− step "create 10 multipart uploads"
502+ step "create 10 multipart uploads"
366− forM_ [1..10::Int] $ \_ -> do
503+ forM_ [1 .. 10 :: Int] $ \_ -> do
367− uid <- newMultipartUpload bucket object []
368− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
369−
370− step "High-level listing of incomplete multipart uploads"
371− uploads <- C.runConduit $
372− listIncompleteUploads bucket (Just "newmpupload") True C..|
373− sinkList
374− liftIO $ length uploads @?= 10
375−
376− step "cleanup"
377− forM_ uploads $ \(UploadInfo _ uid _ _) ->
378− abortMultipartUpload bucket object uid
379−
380− step "High-level listIncompleteParts Test"
381− let mb5 = 5 * 1024 * 1024
382−
383− step "create a multipart upload"
384 uid <- newMultipartUpload bucket object []
504 uid <- newMultipartUpload bucket object []
385 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
505 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
386
506
507+ step "High-level listing of incomplete multipart uploads"
508+ uploads <-
509+ C.runConduit $
510+ listIncompleteUploads bucket (Just "newmpupload") True
511+ C..| sinkList
512+ liftIO $ length uploads @?= 10
513+
514+ step "cleanup"
515+ forM_ uploads $ \(UploadInfo _ uid _ _) ->
516+ abortMultipartUpload bucket object uid
517+
518+ step "High-level listIncompleteParts Test"
519+ let mb5 = 5 * 1024 * 1024
520+
521+ step "create a multipart upload"
522+ uid <- newMultipartUpload bucket object []
523+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
524+
387− step "put object parts 1..10"
525+ step "put object parts 1..10"
388− inputFile <- mkRandFile mb5
526+ inputFile <- mkRandFile mb5
389− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
527+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
390− forM_ [1..10] $ \pnum ->
528+ forM_ [1 .. 10] $ \pnum ->
391 putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
529 putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
392
530
393− step "fetch list parts"
531+ step "fetch list parts"
532+ incompleteParts <-
533+ C.runConduit $
394− incompleteParts <- C.runConduit $ listIncompleteParts bucket object uid
534+ listIncompleteParts bucket object uid
395− C..| sinkList
535+ C..| sinkList
396− liftIO $ length incompleteParts @?= 10
536+ liftIO $ length incompleteParts @?= 10
397
537
398− step "cleanup"
538+ step "cleanup"
399− abortMultipartUpload bucket object uid
539+ abortMultipartUpload bucket object uid
400
540
401listingTest :: TestTree
541listingTest :: TestTree
402listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
542listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
403− step "listObjects' test"
543+ step "listObjects' test"
404− step "put 10 objects"
544+ step "put 10 objects"
405− let objects = (\s ->T.concat ["lsb-release", T.pack (show s)]) <$> [1..10::Int]
545+ let objects = (\s -> T.concat ["test-file-", T.pack (show s)]) <$> [1 .. 10 :: Int]
406−
546+
547+ testFilepath <- mkRandFile 200
407− forM_ [1..10::Int] $ \s ->
548+ forM_ [1 .. 10 :: Int] $ \s ->
408− fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release" defaultPutObjectOptions
549+ fPutObject bucket (T.concat ["test-file-", T.pack (show s)]) testFilepath defaultPutObjectOptions
409−
550+
410− step "Simple list"
551+ step "Simple list"
411− res <- listObjects' bucket Nothing Nothing Nothing Nothing
552+ res <- listObjects' bucket Nothing Nothing Nothing Nothing
412− let expectedObjects = sort objects
553+ let expectedObjects = sort objects
554+ liftIO $
555+ assertEqual
413− liftIO $ assertEqual "Objects match failed!" expectedObjects
556+ "Objects match failed!"
557+ expectedObjects
414− (map oiObject $ lorObjects res)
558+ (map oiObject $ lorObjects res)
415−
559+
416− step "Simple list version 1"
560+ step "Simple list version 1"
417− resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
561+ resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
418− let expected = sort $ map (T.concat .
562+ let expected =
419− ("lsb-release":) .
563+ sort $
564+ map
565+ ( T.concat
566+ . ("test-file-" :)
420− (\x -> [x]) .
567+ . (: [])
421− T.pack .
568+ . T.pack
569+ . show
570+ )
422− show) [1..10::Int]
571+ [1 .. 10 :: Int]
572+ liftIO $
573+ assertEqual
423− liftIO $ assertEqual "Objects match failed!" expected
574+ "Objects match failed!"
575+ expected
424− (map oiObject $ lorObjects' resV1)
576+ (map oiObject $ lorObjects' resV1)
425−
577+
426− step "Cleanup actions"
578+ step "Cleanup actions"
427− forM_ objects $ \obj -> deleteObject bucket obj
579+ forM_ objects $ \obj -> deleteObject bucket obj
428−
580+
429− step "listIncompleteUploads' test"
581+ step "listIncompleteUploads' test"
430− step "create 10 multipart uploads"
582+ step "create 10 multipart uploads"
431− let object = "newmpupload"
583+ let object = "newmpupload"
432− forM_ [1..10::Int] $ \_ -> do
584+ forM_ [1 .. 10 :: Int] $ \_ -> do
433− uid <- newMultipartUpload bucket object []
585+ uid <- newMultipartUpload bucket object []
434− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
435−
436− step "list incomplete multipart uploads"
437− incompleteUploads <- listIncompleteUploads' bucket (Just "newmpupload") Nothing
438− Nothing Nothing Nothing
439− liftIO $ (length $ lurUploads incompleteUploads) @?= 10
440−
441− step "cleanup"
442− forM_ (lurUploads incompleteUploads) $
443− \(_, uid, _) -> abortMultipartUpload bucket object uid
444−
445− step "Basic listIncompleteParts Test"
446− let mb5 = 5 * 1024 * 1024
447−
448− step "create a multipart upload"
449− uid <- newMultipartUpload bucket object []
450− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
586+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
451−
452− step "put object parts 1..10"
453− inputFile <- mkRandFile mb5
454− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
455− forM_ [1..10] $ \pnum ->
456− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
457
587
588+ step "list incomplete multipart uploads"
589+ incompleteUploads <-
590+ listIncompleteUploads'
591+ bucket
592+ (Just "newmpupload")
593+ Nothing
594+ Nothing
595+ Nothing
596+ Nothing
597+ liftIO $ length (lurUploads incompleteUploads) @?= 10
598+
599+ step "cleanup"
600+ forM_ (lurUploads incompleteUploads) $
601+ \(_, uid, _) -> abortMultipartUpload bucket object uid
602+
603+ step "Basic listIncompleteParts Test"
604+ let mb5 = 5 * 1024 * 1024
605+
606+ step "create a multipart upload"
607+ uid <- newMultipartUpload bucket object []
608+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
609+
610+ step "put object parts 1..10"
611+ inputFile <- mkRandFile mb5
612+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
613+ forM_ [1 .. 10] $ \pnum ->
614+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
615+
458− step "fetch list parts"
616+ step "fetch list parts"
459− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
617+ listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
460− liftIO $ (length $ lprParts listPartsResult) @?= 10
618+ liftIO $ length (lprParts listPartsResult) @?= 10
461− abortMultipartUpload bucket object uid
619+ abortMultipartUpload bucket object uid
462
620
463presignedUrlFunTest :: TestTree
621presignedUrlFunTest :: TestTree
464presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
622presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -470,29 +628,47 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
470 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
628 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
471
629
472 step "PUT object presigned URL - makePresignedUrl"
630 step "PUT object presigned URL - makePresignedUrl"
631+ putUrl <-
473− putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
632+ makePresignedUrl
633+ 3600
634+ HT.methodPut
635+ (Just bucket)
636+ (Just obj)
474− (Just obj) (Just "us-east-1") [] []
637+ (Just "us-east-1")
475−
638+ []
639+ []
640+
641+ -- print putUrl
476 let size1 = 1000 :: Int64
642 let size1 = 1000 :: Int64
477 inputFile <- mkRandFile size1
643 inputFile <- mkRandFile size1
478
644
479 -- attempt to upload using the presigned URL
645 -- attempt to upload using the presigned URL
480 putResp <- putR size1 inputFile mgr putUrl
646 putResp <- putR size1 inputFile mgr putUrl
647+ liftIO $
481− liftIO $ (NC.responseStatus putResp == HT.status200) @?
648+ (NC.responseStatus putResp == HT.status200)
482− "presigned PUT failed"
649+ @? "presigned PUT failed"
483
650
484 step "GET object presigned URL - makePresignedUrl"
651 step "GET object presigned URL - makePresignedUrl"
652+ getUrl <-
485− getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
653+ makePresignedUrl
654+ 3600
655+ HT.methodGet
656+ (Just bucket)
657+ (Just obj)
486− (Just obj) (Just "us-east-1") [] []
658+ (Just "us-east-1")
659+ []
660+ []
487
661
488 getResp <- getR mgr getUrl
662 getResp <- getR mgr getUrl
663+ liftIO $
489− liftIO $ (NC.responseStatus getResp == HT.status200) @?
664+ (NC.responseStatus getResp == HT.status200)
490− "presigned GET failed"
665+ @? "presigned GET failed"
491
666
492 -- read content from file to compare with response above
667 -- read content from file to compare with response above
493 bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
668 bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
669+ liftIO $
494− liftIO $ (bs == NC.responseBody getResp) @?
670+ (bs == NC.responseBody getResp)
495− "presigned put and get got mismatched data"
671+ @? "presigned put and get got mismatched data"
496
672
497 step "PUT object presigned - presignedPutObjectURL"
673 step "PUT object presigned - presignedPutObjectURL"
498 putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
674 putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
@@ -501,66 +677,71 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
501 testFile <- mkRandFile size2
677 testFile <- mkRandFile size2
502
678
503 putResp2 <- putR size2 testFile mgr putUrl2
679 putResp2 <- putR size2 testFile mgr putUrl2
680+ liftIO $
504− liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
681+ (NC.responseStatus putResp2 == HT.status200)
505− "presigned PUT failed (presignedPutObjectUrl)"
682+ @? "presigned PUT failed (presignedPutObjectUrl)"
506
683
507 step "HEAD object presigned URL - presignedHeadObjectUrl"
684 step "HEAD object presigned URL - presignedHeadObjectUrl"
508 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
685 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
509
686
687+ headResp <- do
510− headResp <- do let req = NC.parseRequest_ $ toS headUrl
688+ let req = NC.parseRequest_ $ decodeUtf8 headUrl
511− NC.httpLbs (req {NC.method = HT.methodHead}) mgr
689+ NC.httpLbs (req {NC.method = HT.methodHead}) mgr
690+ liftIO $
512− liftIO $ (NC.responseStatus headResp == HT.status200) @?
691+ (NC.responseStatus headResp == HT.status200)
513− "presigned HEAD failed (presignedHeadObjectUrl)"
692+ @? "presigned HEAD failed (presignedHeadObjectUrl)"
514
693
515 -- check that header info is accurate
694 -- check that header info is accurate
516− let h = Map.fromList $ NC.responseHeaders headResp
695+ let h = H.fromList $ NC.responseHeaders headResp
517− cLen = Map.findWithDefault "0" HT.hContentLength h
696+ cLen = H.lookupDefault "0" HT.hContentLength h
518− liftIO $ (cLen == show size2) @? "Head req returned bad content length"
697+ liftIO $ (cLen == showBS size2) @? "Head req returned bad content length"
519
698
520 step "GET object presigned URL - presignedGetObjectUrl"
699 step "GET object presigned URL - presignedGetObjectUrl"
521 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
700 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
522
701
523 getResp2 <- getR mgr getUrl2
702 getResp2 <- getR mgr getUrl2
703+ liftIO $
524− liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
704+ (NC.responseStatus getResp2 == HT.status200)
525− "presigned GET failed (presignedGetObjectUrl)"
705+ @? "presigned GET failed (presignedGetObjectUrl)"
526
706
527 -- read content from file to compare with response above
707 -- read content from file to compare with response above
528 bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
708 bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
709+ liftIO $
529− liftIO $ (bs2 == NC.responseBody getResp2) @?
710+ (bs2 == NC.responseBody getResp2)
530− "presigned put and get got mismatched data (presigned*Url)"
711+ @? "presigned put and get got mismatched data (presigned*Url)"
531−
532
712
533 mapM_ (removeObject bucket) [obj, obj2]
713 mapM_ (removeObject bucket) [obj, obj2]
534 where
714 where
535 putR size filePath mgr url = do
715 putR size filePath mgr url = do
536− let req = NC.parseRequest_ $ toS url
716+ let req = NC.parseRequest_ $ decodeUtf8 url
717+ let req' =
718+ req
537− let req' = req { NC.method = HT.methodPut
719+ { NC.method = HT.methodPut,
720+ NC.requestBody =
538− , NC.requestBody = NC.requestBodySource size $
721+ NC.requestBodySource size $
539− CB.sourceFile filePath}
722+ CB.sourceFile filePath
723+ }
540 NC.httpLbs req' mgr
724 NC.httpLbs req' mgr
541−
542 getR mgr url = do
725 getR mgr url = do
543− let req = NC.parseRequest_ $ toS url
726+ let req = NC.parseRequest_ $ decodeUtf8 url
544 NC.httpLbs req mgr
727 NC.httpLbs req mgr
545
728
546presignedPostPolicyFunTest :: TestTree
729presignedPostPolicyFunTest :: TestTree
547−presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
730+presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
548 \step bucket -> do
731 \step bucket -> do
549−
550 step "presignedPostPolicy basic test"
732 step "presignedPostPolicy basic test"
551− now <- liftIO $ Time.getCurrentTime
733+ now <- liftIO Time.getCurrentTime
552
734
553 let key = "presignedPostPolicyTest/myfile"
735 let key = "presignedPostPolicyTest/myfile"
736+ policyConds =
554− policyConds = [ ppCondBucket bucket
737+ [ ppCondBucket bucket,
555− , ppCondKey key
738+ ppCondKey key,
556− , ppCondContentLengthRange 1 1000
739+ ppCondContentLengthRange 1 1000,
557− , ppCondContentType "application/octet-stream"
740+ ppCondContentType "application/octet-stream",
558− , ppCondSuccessActionStatus 200
741+ ppCondSuccessActionStatus 200
559− ]
742+ ]
560−
561 expirationTime = Time.addUTCTime 3600 now
743 expirationTime = Time.addUTCTime 3600 now
562 postPolicyE = newPostPolicy expirationTime policyConds
744 postPolicyE = newPostPolicy expirationTime policyConds
563−
564 size = 1000 :: Int64
745 size = 1000 :: Int64
565
746
566 inputFile <- mkRandFile size
747 inputFile <- mkRandFile size
@@ -571,43 +752,43 @@ presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
571 (url, formData) <- presignedPostPolicy postPolicy
752 (url, formData) <- presignedPostPolicy postPolicy
572 -- liftIO (print url) >> liftIO (print formData)
753 -- liftIO (print url) >> liftIO (print formData)
573 result <- liftIO $ postForm url formData inputFile
754 result <- liftIO $ postForm url formData inputFile
755+ liftIO $
574− liftIO $ (NC.responseStatus result == HT.status200) @?
756+ (NC.responseStatus result == HT.status200)
575− "presigned POST failed"
757+ @? "presigned POST failed"
576
758
577 mapM_ (removeObject bucket) [key]
759 mapM_ (removeObject bucket) [key]
578− where
760+ where
579−
580− postForm url formData inputFile = do
761+ postForm url formData inputFile = do
581− req <- NC.parseRequest $ toS url
762+ req <- NC.parseRequest $ decodeUtf8 url
763+ let parts =
582− let parts = map (\(x, y) -> Form.partBS x y) $
764+ map (uncurry Form.partBS) $
583− Map.toList formData
765+ H.toList formData
584− parts' = parts ++ [Form.partFile "file" inputFile]
766+ parts' = parts ++ [Form.partFile "file" inputFile]
585− req' <- Form.formDataBody parts' req
767+ req' <- Form.formDataBody parts' req
586− mgr <- NC.newManager NC.tlsManagerSettings
768+ mgr <- NC.newManager NC.tlsManagerSettings
587− NC.httpLbs req' mgr
769+ NC.httpLbs req' mgr
588
770
589bucketPolicyFunTest :: TestTree
771bucketPolicyFunTest :: TestTree
590bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
772bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
591 \step bucket -> do
773 \step bucket -> do
592−
593 step "bucketPolicy basic test - no policy exception"
774 step "bucketPolicy basic test - no policy exception"
594 resE <- try $ getBucketPolicy bucket
775 resE <- try $ getBucketPolicy bucket
595 case resE of
776 case resE of
596 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
777 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
597− _ -> return ()
778+ _ -> return ()
598
779
599 resE' <- try $ setBucketPolicy bucket T.empty
780 resE' <- try $ setBucketPolicy bucket T.empty
600 case resE' of
781 case resE' of
601 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
782 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
602− _ -> return ()
783+ _ -> return ()
603
784
604 let expectedPolicyJSON = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket/*\"]}]}"
785 let expectedPolicyJSON = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket/*\"]}]}"
605
786
606 step "try a malformed policy, expect error"
787 step "try a malformed policy, expect error"
607 resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
788 resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
608 case resE'' of
789 case resE'' of
609− Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "Policy has invalid resource."
790+ Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
610− _ -> return ()
791+ _ -> return ()
611
792
612 let expectedPolicyJSON' = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "/*\"]}]}"
793 let expectedPolicyJSON' = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "/*\"]}]}"
613
794
@@ -622,13 +803,22 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
622 step "verify bucket policy: (2) get `myobject` anonymously"
803 step "verify bucket policy: (2) get `myobject` anonymously"
623 connInfo <- asks mcConnInfo
804 connInfo <- asks mcConnInfo
624 let proto = bool "http://" "https://" $ connectIsSecure connInfo
805 let proto = bool "http://" "https://" $ connectIsSecure connInfo
806+ url =
807+ BS.concat
808+ [ proto,
625− url = BS.concat [proto, getHostAddr connInfo, "/", toS bucket,
809+ getHostAddr connInfo,
810+ "/",
811+ encodeUtf8 bucket,
626− "/", toS obj]
812+ "/",
813+ encodeUtf8 obj
814+ ]
815+ respE <-
816+ liftIO $
627− respE <- liftIO $ (fmap (Right . toS) $ NC.simpleHttp $ toS url) `catch`
817+ fmap (Right . toStrictBS) (NC.simpleHttp $ decodeUtf8 url)
628− (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
818+ `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
629 case respE of
819 case respE of
630− Left err -> liftIO $ assertFailure $ show err
820+ Left err -> liftIO $ assertFailure $ show err
631− Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")
821+ Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
632
822
633 deleteObject bucket obj
823 deleteObject bucket obj
634
824
@@ -638,25 +828,28 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
638multipartTest :: TestTree
828multipartTest :: TestTree
639multipartTest = funTestWithBucket "Multipart Tests" $
829multipartTest = funTestWithBucket "Multipart Tests" $
640 \step bucket -> do
830 \step bucket -> do
831+ -- Commenting out test since it's platform specific.
832+ -- FIXME: Need to find a platform agnostic way to test this.
641− step "Prepare for putObjectInternal with non-seekable file, with size."
833+ -- step "Prepare for putObjectInternal with non-seekable file, with size."
642− step "Upload multipart file."
834+ -- step "Upload multipart file."
643− let mb80 = 80 * 1024 * 1024
835+ -- let mb80 = 80 * 1024 * 1024
644− obj = "mpart"
836+ -- obj = "mpart"
645
837
646− void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
838+ -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
647
839
648− step "Retrieve and verify file size"
840+ -- step "Retrieve and verify file size"
649− destFile <- mkRandFile 0
841+ -- destFile <- mkRandFile 0
650− fGetObject bucket obj destFile defaultGetObjectOptions
842+ -- fGetObject bucket obj destFile defaultGetObjectOptions
651− gotSize <- withNewHandle destFile getFileSize
843+ -- gotSize <- withNewHandle destFile getFileSize
844+ -- liftIO $
652− liftIO $ gotSize == Right (Just mb80) @?
845+ -- gotSize == Right (Just mb80)
653− "Wrong file size of put file after getting"
846+ -- @? "Wrong file size of put file after getting"
654
847
655− step "Cleanup actions"
848+ -- step "Cleanup actions"
656− removeObject bucket obj
849+ -- removeObject bucket obj
657
850
658− step "cleanup"
851+ -- step "cleanup"
659− removeObject bucket "big"
852+ -- removeObject bucket "big"
660
853
661 step "Prepare for removeIncompleteUpload"
854 step "Prepare for removeIncompleteUpload"
662 -- low-level multipart operation tests.
855 -- low-level multipart operation tests.
@@ -670,15 +863,17 @@ multipartTest = funTestWithBucket "Multipart Tests" $
670 randFile <- mkRandFile kb5
863 randFile <- mkRandFile kb5
671
864
672 step "upload 2 parts"
865 step "upload 2 parts"
673− forM_ [1,2] $ \partNum -> do
866+ forM_ [1, 2] $ \partNum -> do
674− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
867+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
675− void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
868+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
676
869
677 step "remove ongoing upload"
870 step "remove ongoing upload"
678 removeIncompleteUpload bucket object
871 removeIncompleteUpload bucket object
872+ uploads <-
873+ C.runConduit $
679− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
874+ listIncompleteUploads bucket (Just object) False
680− C..| sinkList
875+ C..| sinkList
681− liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
876+ liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
682
877
683putObjectContentTypeTest :: TestTree
878putObjectContentTypeTest :: TestTree
684putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
879putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
@@ -689,28 +884,39 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
689
884
690 step "create server object with content-type"
885 step "create server object with content-type"
691 inputFile <- mkRandFile size1
886 inputFile <- mkRandFile size1
887+ fPutObject
888+ bucket
889+ object
890+ inputFile
692− fPutObject bucket object inputFile defaultPutObjectOptions {
891+ defaultPutObjectOptions
693− pooContentType = Just "application/javascript"
892+ { pooContentType = Just "application/javascript"
694− }
893+ }
695
894
696 -- retrieve obj info to check
895 -- retrieve obj info to check
697 oi <- headObject bucket object []
896 oi <- headObject bucket object []
698 let m = oiMetadata oi
897 let m = oiMetadata oi
699
898
700 step "Validate content-type"
899 step "Validate content-type"
701− liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (Map.lookup "Content-Type" m)
900+ liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
702
901
703 step "upload object with content-encoding set to identity"
902 step "upload object with content-encoding set to identity"
903+ fPutObject
904+ bucket
905+ object
906+ inputFile
704− fPutObject bucket object inputFile defaultPutObjectOptions {
907+ defaultPutObjectOptions
705− pooContentEncoding = Just "identity"
908+ { pooContentEncoding = Just "identity"
706− }
909+ }
707
910
708 oiCE <- headObject bucket object []
911 oiCE <- headObject bucket object []
709 let m' = oiMetadata oiCE
912 let m' = oiMetadata oiCE
710
913
711 step "Validate content-encoding"
914 step "Validate content-encoding"
915+ liftIO $
916+ assertEqual
712− liftIO $ assertEqual "Content-Encoding did not match" (Just "identity")
917+ "Content-Encoding did not match"
918+ (Just "identity")
713− (Map.lookup "Content-Encoding" m')
919+ (H.lookup "Content-Encoding" m')
714
920
715 step "Cleanup actions"
921 step "Cleanup actions"
716
922
@@ -725,19 +931,103 @@ putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage test
725
931
726 step "create server object with content-language"
932 step "create server object with content-language"
727 inputFile <- mkRandFile size1
933 inputFile <- mkRandFile size1
934+ fPutObject
935+ bucket
936+ object
937+ inputFile
728− fPutObject bucket object inputFile defaultPutObjectOptions {
938+ defaultPutObjectOptions
729− pooContentLanguage = Just "en-US"
939+ { pooContentLanguage = Just "en-US"
730− }
940+ }
731
941
732 -- retrieve obj info to check
942 -- retrieve obj info to check
733 oi <- headObject bucket object []
943 oi <- headObject bucket object []
734 let m = oiMetadata oi
944 let m = oiMetadata oi
735
945
736 step "Validate content-language"
946 step "Validate content-language"
947+ liftIO $
948+ assertEqual
737− liftIO $ assertEqual "content-language did not match" (Just "en-US")
949+ "content-language did not match"
950+ (Just "en-US")
738− (Map.lookup "Content-Language" m)
951+ (H.lookup "Content-Language" m)
952+ step "Cleanup actions"
953+
954+ removeObject bucket object
955+
956+putObjectUserMetadataTest :: TestTree
957+putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
958+ \step bucket -> do
959+ step "putObject user-metadata test"
960+ let object = "object-with-metadata"
961+ size1 = 100 :: Int64
962+
963+ step "create server object with usermetdata"
964+ inputFile <- mkRandFile size1
965+
966+ fPutObject
967+ bucket
968+ object
969+ inputFile
970+ defaultPutObjectOptions
971+ { pooUserMetadata =
972+ [ ("x-Amz-meta-mykey1", "myval1"),
973+ ("mykey2", "myval2")
974+ ]
975+ }
976+
977+ step "Validate user-metadata"
978+ -- retrieve obj info to check
979+ oi <- headObject bucket object []
980+ let m = oiUserMetadata oi
981+ -- need to do a case-insensitive comparison
982+ sortedMeta =
983+ sort $
984+ map (bimap T.toLower T.toLower) $
985+ H.toList m
986+ ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
987+
988+ liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
989+
739 step "Cleanup actions"
990 step "Cleanup actions"
991+ removeObject bucket object
992+
993+getObjectTest :: TestTree
994+getObjectTest = funTestWithBucket "getObject test" $
995+ \step bucket -> do
996+ step "putObject with some metadata"
997+ let object = "object-with-metadata"
998+ size1 = 100 :: Int64
999+
1000+ inputFile <- mkRandFile size1
1001+ fPutObject
1002+ bucket
1003+ object
1004+ inputFile
1005+ defaultPutObjectOptions
1006+ { pooUserMetadata =
1007+ [ ("x-Amz-meta-mykey1", "myval1"),
1008+ ("mykey2", "myval2")
1009+ ]
1010+ }
1011+
1012+ step "get the object - check the metadata matches"
1013+ -- retrieve obj info to check
1014+ gor <- getObject bucket object defaultGetObjectOptions
1015+ let m = oiUserMetadata $ gorObjectInfo gor
1016+ -- need to do a case-insensitive comparison
1017+ sortedMeta =
1018+ sort $
1019+ map (bimap T.toLower T.toLower) $
1020+ H.toList m
1021+ ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
1022+
1023+ liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
1024+
1025+ step "get the object content"
1026+ getObjectHash <- hashSHA256FromSource $ gorObjectStream gor
1027+ inputHash <- hashSHA256FromSource $ C.sourceFile inputFile
1028+ liftIO $ (getObjectHash == inputHash) @? "Input file and output file mismatched!"
740
1029
1030+ step "Cleanup actions"
741 removeObject bucket object
1031 removeObject bucket object
742
1032
743putObjectStorageClassTest :: TestTree
1033putObjectStorageClassTest :: TestTree
@@ -755,13 +1045,21 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
755 inputFile' <- mkRandFile size1
1045 inputFile' <- mkRandFile size1
756 inputFile'' <- mkRandFile size0
1046 inputFile'' <- mkRandFile size0
757
1047
758− fPutObject bucket object inputFile defaultPutObjectOptions {
1048+ fPutObject
1049+ bucket
1050+ object
1051+ inputFile
1052+ defaultPutObjectOptions
759− pooStorageClass = Just "STANDARD"
1053+ { pooStorageClass = Just "STANDARD"
760− }
1054+ }
761−
1055+
1056+ fPutObject
1057+ bucket
1058+ object'
1059+ inputFile'
762− fPutObject bucket object' inputFile' defaultPutObjectOptions {
1060+ defaultPutObjectOptions
763− pooStorageClass = Just "REDUCED_REDUNDANCY"
1061+ { pooStorageClass = Just "REDUCED_REDUNDANCY"
764− }
1062+ }
765
1063
766 removeObject bucket object
1064 removeObject bucket object
767
1065
@@ -770,15 +1068,24 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
770 let m' = oiMetadata oi'
1068 let m' = oiMetadata oi'
771
1069
772 step "Validate x-amz-storage-class rrs"
1070 step "Validate x-amz-storage-class rrs"
1071+ liftIO $
1072+ assertEqual
773− liftIO $ assertEqual "storageClass did not match" (Just "REDUCED_REDUNDANCY")
1073+ "storageClass did not match"
1074+ (Just "REDUCED_REDUNDANCY")
774− (Map.lookup "X-Amz-Storage-Class" m')
1075+ (H.lookup "X-Amz-Storage-Class" m')
775−
1076+
1077+ fpE <-
1078+ try $
1079+ fPutObject
1080+ bucket
1081+ object''
1082+ inputFile''
776− fpE <- try $ fPutObject bucket object'' inputFile'' defaultPutObjectOptions {
1083+ defaultPutObjectOptions
777− pooStorageClass = Just "INVALID_STORAGE_CLASS"
1084+ { pooStorageClass = Just "INVALID_STORAGE_CLASS"
778− }
1085+ }
779 case fpE of
1086 case fpE of
780 Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
1087 Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
781− _ -> return ()
1088+ _ -> return ()
782
1089
783 step "Cleanup actions"
1090 step "Cleanup actions"
784
1091
@@ -797,7 +1104,7 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
797 fPutObject bucket object inputFile defaultPutObjectOptions
1104 fPutObject bucket object inputFile defaultPutObjectOptions
798
1105
799 step "copy object"
1106 step "copy object"
800− let srcInfo = defaultSourceInfo { srcBucket = bucket, srcObject = object}
1107+ let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
801 (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
1108 (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
802
1109
803 -- retrieve obj info to check
1110 -- retrieve obj info to check
@@ -808,8 +1115,9 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
808
1115
809 let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
1116 let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
810
1117
1118+ liftIO $
811− liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
1119+ (s == size1 && e == etag && isMTimeDiffOk)
812− "Copied object did not match expected."
1120+ @? "Copied object did not match expected."
813
1121
814 step "cleanup actions"
1122 step "cleanup actions"
815 removeObject bucket object
1123 removeObject bucket object
@@ -830,12 +1138,18 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
830 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
1138 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
831
1139
832 step "put object parts 1-3"
1140 step "put object parts 1-3"
833− let srcInfo' = defaultSourceInfo { srcBucket = bucket, srcObject = srcObj }
1141+ let srcInfo' = defaultSourceInfo {srcBucket = bucket, srcObject = srcObj}
834− dstInfo' = defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj }
1142+ dstInfo' = defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
835− parts <- forM [1..3] $ \p -> do
1143+ parts <- forM [1 .. 3] $ \p -> do
1144+ (etag', _) <-
836− (etag', _) <- copyObjectPart dstInfo' srcInfo'{
1145+ copyObjectPart
1146+ dstInfo'
1147+ srcInfo'
837− srcRange = Just $ (,) ((p-1)*mb5) ((p-1)*mb5 + (mb5 - 1))
1148+ { srcRange = Just $ (,) ((p - 1) * mb5) ((p - 1) * mb5 + (mb5 - 1))
1149+ }
1150+ uid
838− } uid (fromIntegral p) []
1151+ (fromIntegral p)
1152+ []
839 return (fromIntegral p, etag')
1153 return (fromIntegral p, etag')
840
1154
841 step "complete multipart"
1155 step "complete multipart"
@@ -881,11 +1195,13 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
881 fPutObject bucket src inputFile' defaultPutObjectOptions
1195 fPutObject bucket src inputFile' defaultPutObjectOptions
882
1196
883 step "copy last 10MiB of object"
1197 step "copy last 10MiB of object"
1198+ copyObject
884− copyObject defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj } defaultSourceInfo {
1199+ defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1200+ defaultSourceInfo
885− srcBucket = bucket
1201+ { srcBucket = bucket,
886− , srcObject = src
1202+ srcObject = src,
887− , srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
1203+ srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
888− }
1204+ }
889
1205
890 step "verify uploaded object"
1206 step "verify uploaded object"
891 cSize <- oiSize <$> headObject bucket copyObj []
1207 cSize <- oiSize <$> headObject bucket copyObj []
@@ -896,42 +1212,72 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
896
1212
897getNPutSSECTest :: TestTree
1213getNPutSSECTest :: TestTree
898getNPutSSECTest =
1214getNPutSSECTest =
899− funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
1215+ funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
900− -- Skip this test if the server is not using TLS as encryption is
1216+ -- Skip this test if the server is not using TLS as encryption is
901− -- disabled anyway.
1217+ -- disabled anyway.
902− isTLSConn <- asks (connectIsSecure . mcConnInfo)
1218+ isTLSConn <- asks (connectIsSecure . mcConnInfo)
903− if isTLSConn
1219+ if isTLSConn
1220+ then do
904− then do step "Make an encryption key"
1221+ step "Make an encryption key"
905− key <- case mkSSECKey $ BS.pack [0..31] of
1222+ key <- case mkSSECKey $ BS.pack [0 .. 31] of
906− Nothing -> liftIO $ assertFailure "This should not happen"
1223+ Nothing -> liftIO $ assertFailure "This should not happen"
907− Just k -> return k
1224+ Just k -> return k
908−
1225+
909− let mb1 = 1024*1024
1226+ let mb1 = 1024 * 1024
910− obj = "1"
1227+ obj = "1"
911− step "Upload an object using the encryption key"
1228+ step "Upload an object using the encryption key"
912− rFile <- mkRandFile mb1
1229+ rFile <- mkRandFile mb1
913− let putOpts = defaultPutObjectOptions { pooSSE = Just $ SSEC key }
1230+ let putOpts = defaultPutObjectOptions {pooSSE = Just $ SSEC key}
914− fPutObject bucket obj rFile putOpts
1231+ fPutObject bucket obj rFile putOpts
915−
1232+
916− step "Stat object without key - should fail"
1233+ step "Stat object without key - should fail"
917− headRes <- try $ statObject bucket obj defaultGetObjectOptions
1234+ headRes <- try $ statObject bucket obj defaultGetObjectOptions
918− case headRes of
1235+ case headRes of
919− Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
1236+ Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
920− Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
1237+ Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
921− | NC.responseStatus rsp == HT.status400 -> return ()
1238+ | NC.responseStatus rsp == HT.status400 -> return ()
922− | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1239+ | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
923− Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1240+ Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
924−
1241+
925− step "Get file and check length"
1242+ step "Get file and check length"
926− dstFile <- mkRandFile 0
1243+ dstFile <- mkRandFile 0
927− let getOpts = defaultGetObjectOptions { gooSSECKey = Just key }
1244+ let getOpts = defaultGetObjectOptions {gooSSECKey = Just key}
928− fGetObject bucket obj dstFile getOpts
1245+ fGetObject bucket obj dstFile getOpts
929−
1246+
930− gotSize <- withNewHandle dstFile getFileSize
1247+ gotSize <- withNewHandle dstFile getFileSize
1248+ liftIO $
1249+ gotSize
931− liftIO $ gotSize == Right (Just mb1) @?
1250+ == Right (Just mb1)
932− "Wrong file size of object when getting"
1251+ @? "Wrong file size of object when getting"
933−
1252+
934− step "Cleanup"
1253+ step "Cleanup"
935− deleteObject bucket obj
1254+ deleteObject bucket obj
936−
937− else step "Skipping encryption test as server is not using TLS"
1255+ else step "Skipping encryption test as server is not using TLS"
1256+
1257+assumeRoleRequestTest :: TestTree
1258+assumeRoleRequestTest = testCaseSteps "Assume Role STS API" $ \step -> do
1259+ step "Load credentials"
1260+ (_, (ak, sk)) <- lookupTestEnv
1261+ val <- Env.lookupEnv "MINIO_LOCAL"
1262+ isSecure <- Env.lookupEnv "MINIO_SECURE"
1263+ let localMinioCred = Just $ CredentialValue ak sk mempty
1264+ playCreds =
1265+ case connectCreds minioPlayCI of
1266+ CredsStatic c -> Just c
1267+ _ -> Nothing
1268+ (cvMay, loc) =
1269+ case (val, isSecure) of
1270+ (Just _, Just _) -> (localMinioCred, "https://localhost:9000")
1271+ (Just _, Nothing) -> (localMinioCred, "http://localhost:9000")
1272+ (Nothing, _) -> (playCreds, "https://play.min.io:9000")
1273+ cv <- maybe (assertFailure "bad creds") return cvMay
1274+ let assumeRole =
1275+ STSAssumeRole cv $
1276+ defaultSTSAssumeRoleOptions
1277+ { saroLocation = Just "us-east-1",
1278+ saroEndpoint = Just loc
1279+ }
1280+ step "AssumeRole request"
1281+ res <- requestSTSCredential assumeRole
1282+ let v = credentialValueText $ fst res
1283+ print (v, snd res)
test/Network/Minio/API/Test.hs+75−61
@@ -15,88 +15,102 @@
15--
15--
16
16
17module Network.Minio.API.Test
17module Network.Minio.API.Test
18− ( bucketNameValidityTests
18+ ( bucketNameValidityTests,
19− , objectNameValidityTests
19+ objectNameValidityTests,
20− , parseServerInfoJSONTest
20+ parseServerInfoJSONTest,
21− , parseHealStatusTest
21+ parseHealStatusTest,
22− , parseHealStartRespTest
22+ parseHealStartRespTest,
23+ )
23− ) where
24+where
24−
25+
25−import Data.Aeson (eitherDecode)
26+import Data.Aeson (eitherDecode)
26−import Test.Tasty
27+import Network.Minio.API
27−import Test.Tasty.HUnit
28−
29−import Lib.Prelude
30−
31−import Network.Minio.AdminAPI
28+import Network.Minio.AdminAPI
32−import Network.Minio.API
29+import Test.Tasty
30+import Test.Tasty.HUnit
33
31
34assertBool' :: Bool -> Assertion
32assertBool' :: Bool -> Assertion
35assertBool' = assertBool "Test failed!"
33assertBool' = assertBool "Test failed!"
36
34
37bucketNameValidityTests :: TestTree
35bucketNameValidityTests :: TestTree
38−bucketNameValidityTests = testGroup "Bucket Name Validity Tests"
36+bucketNameValidityTests =
37+ testGroup
38+ "Bucket Name Validity Tests"
39− [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName ""
39+ [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName "",
40− , testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab"
40+ testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab",
41− , testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
41+ testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
42− , testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD"
42+ testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD",
43− , testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2"
43+ testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2",
44− , testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-"
44+ testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-",
45− , testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg"
45+ testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg",
46− , testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1"
46+ testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1",
47− , testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea"
47+ testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea",
48− , testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d"
48+ testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d",
49− , testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
49+ testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
50− ]
50+ ]
51
51
52objectNameValidityTests :: TestTree
52objectNameValidityTests :: TestTree
53−objectNameValidityTests = testGroup "Object Name Validity Tests"
53+objectNameValidityTests =
54+ testGroup
55+ "Object Name Validity Tests"
54− [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""
56+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName "",
55− , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
57+ testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
56− ]
58+ ]
57
59
58parseServerInfoJSONTest :: TestTree
60parseServerInfoJSONTest :: TestTree
61+parseServerInfoJSONTest =
59−parseServerInfoJSONTest = testGroup "Parse MinIO Admin API ServerInfo JSON test" $
62+ testGroup "Parse MinIO Admin API ServerInfo JSON test" $
63+ map
60− map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
64+ ( \(tName, tDesc, tfn, tVal) ->
65+ testCase tName $
66+ assertBool tDesc $
61− tfn (eitherDecode tVal :: Either [Char] [ServerInfo])) testCases
67+ tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
68+ )
69+ testCases
62 where
70 where
71+ testCases =
63− testCases = [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON)
72+ [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON),
64− , ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON)
73+ ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON),
65− , ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
74+ ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
66− ]
75+ ]
67 fsJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":20530,\"Backend\":{\"Type\":1,\"OnlineDisks\":0,\"OfflineDisks\":0,\"StandardSCData\":0,\"StandardSCParity\":0,\"RRSCData\":0,\"RRSCParity\":0,\"Sets\":null}},\"network\":{\"transferred\":808,\"received\":1160},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":5992503019270,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
76 fsJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":20530,\"Backend\":{\"Type\":1,\"OnlineDisks\":0,\"OfflineDisks\":0,\"StandardSCData\":0,\"StandardSCParity\":0,\"RRSCData\":0,\"RRSCParity\":0,\"Sets\":null}},\"network\":{\"transferred\":808,\"received\":1160},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":5992503019270,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
68−
69 erasureJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":2,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
77 erasureJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":2,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
70−
71 invalidJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":42,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
78 invalidJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":42,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
72
79
73parseHealStatusTest :: TestTree
80parseHealStatusTest :: TestTree
81+parseHealStatusTest =
74−parseHealStatusTest = testGroup "Parse MinIO Admin API HealStatus JSON test" $
82+ testGroup "Parse MinIO Admin API HealStatus JSON test" $
83+ map
75− map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
84+ ( \(tName, tDesc, tfn, tVal) ->
85+ testCase tName $
86+ assertBool tDesc $
76− tfn (eitherDecode tVal :: Either [Char] HealStatus)) testCases
87+ tfn (eitherDecode tVal :: Either [Char] HealStatus)
77−
88+ )
89+ testCases
78 where
90 where
91+ testCases =
79− testCases = [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON')
92+ [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON'),
80− , ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON')
93+ ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON'),
81− , ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
94+ ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
82− ]
95+ ]
83−
84 erasureJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
96 erasureJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
85−
86 invalidJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]"
97 invalidJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]"
87−
88 invalidItemType = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"hello\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
98 invalidItemType = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"hello\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
89
99
90parseHealStartRespTest :: TestTree
100parseHealStartRespTest :: TestTree
101+parseHealStartRespTest =
91−parseHealStartRespTest = testGroup "Parse MinIO Admin API HealStartResp JSON test" $
102+ testGroup "Parse MinIO Admin API HealStartResp JSON test" $
103+ map
92− map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
104+ ( \(tName, tDesc, tfn, tVal) ->
105+ testCase tName $
106+ assertBool tDesc $
93− tfn (eitherDecode tVal :: Either [Char] HealStartResp)) testCases
107+ tfn (eitherDecode tVal :: Either [Char] HealStartResp)
94−
108+ )
109+ testCases
95 where
110 where
111+ testCases =
96− testCases = [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON)
112+ [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON),
97− , ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
113+ ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
98− ]
114+ ]
99−
100 hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
115 hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
101−
102 missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
116 missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
test/Network/Minio/JsonParser/Test.hs+26−27
@@ -15,26 +15,26 @@
15--
15--
16
16
17module Network.Minio.JsonParser.Test
17module Network.Minio.JsonParser.Test
18− (
19− jsonParserTests
18+ ( jsonParserTests,
19+ )
20− ) where
20+where
21
21
22+import Lib.Prelude
23+import Network.Minio.Errors
24+import Network.Minio.JsonParser
22−import Test.Tasty
25+import Test.Tasty
23−import Test.Tasty.HUnit
26+import Test.Tasty.HUnit
24−import UnliftIO (MonadUnliftIO)
27+import UnliftIO (MonadUnliftIO)
25−
26−import Lib.Prelude
27−
28−import Network.Minio.Errors
29−import Network.Minio.JsonParser
30
28
31jsonParserTests :: TestTree
29jsonParserTests :: TestTree
32−jsonParserTests = testGroup "JSON Parser Tests"
30+jsonParserTests =
31+ testGroup
32+ "JSON Parser Tests"
33− [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
33+ [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34− ]
34+ ]
35
35
36tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
36tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
37−tryValidationErr act = try act
37+tryValidationErr = try
38
38
39assertValidationErr :: MErrV -> Assertion
39assertValidationErr :: MErrV -> Assertion
40assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
40assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
@@ -44,21 +44,20 @@ testParseErrResponseJSON = do
44 -- 1. Test parsing of an invalid error json.
44 -- 1. Test parsing of an invalid error json.
45 parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
45 parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
46 when (isRight parseResE) $
46 when (isRight parseResE) $
47+ assertFailure $
47− assertFailure $ "Parsing should have failed => " ++ show parseResE
48+ "Parsing should have failed => " ++ show parseResE
48
49
49 forM_ cases $ \(jsondata, sErr) -> do
50 forM_ cases $ \(jsondata, sErr) -> do
50 parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
51 parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
51 either assertValidationErr (@?= sErr) parseErr
52 either assertValidationErr (@?= sErr) parseErr
52−
53 where
53 where
54− cases = [
54+ cases =
55− -- 2. Test parsing of a valid error json.
55+ [ -- 2. Test parsing of a valid error json.
56− ("{\"Code\":\"InvalidAccessKeyId\",\"Message\":\"The access key ID you provided does not exist in our records.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
56+ ( "{\"Code\":\"InvalidAccessKeyId\",\"Message\":\"The access key ID you provided does not exist in our records.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
57− ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
57+ ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58− )
58+ ),
59− ,
60− -- 3. Test parsing of a valid, empty Resource.
59+ -- 3. Test parsing of a valid, empty Resource.
61− ("{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
60+ ( "{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
62− ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
61+ ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
63− )
62+ )
64 ]
63 ]
test/Network/Minio/TestHelpers.hs+8−8
@@ -15,18 +15,18 @@
15--
15--
16
16
17module Network.Minio.TestHelpers
17module Network.Minio.TestHelpers
18− ( runTestNS
18+ ( runTestNS,
19+ )
19− ) where
20+where
20
21
21−import Network.Minio.Data
22+import Network.Minio.Data
22
23
23−import Lib.Prelude
24−
25−newtype TestNS = TestNS { testNamespace :: Text }
24+newtype TestNS = TestNS {testNamespace :: Text}
26
25
27instance HasSvcNamespace TestNS where
26instance HasSvcNamespace TestNS where
28 getSvcNamespace = testNamespace
27 getSvcNamespace = testNamespace
29
28
30runTestNS :: ReaderT TestNS m a -> m a
29runTestNS :: ReaderT TestNS m a -> m a
30+runTestNS =
31−runTestNS = flip runReaderT $
31+ flip runReaderT $
32− TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
32+ TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs+15−17
@@ -15,33 +15,31 @@
15--
15--
16
16
17module Network.Minio.Utils.Test
17module Network.Minio.Utils.Test
18− (
19− limitedMapConcurrentlyTests
18+ ( limitedMapConcurrentlyTests,
19+ )
20− ) where
20+where
21
21
22−import Test.Tasty
23−import Test.Tasty.HUnit
24−
25−import Lib.Prelude
26−
27−import Network.Minio.Utils
22+import Network.Minio.Utils
23+import Test.Tasty
24+import Test.Tasty.HUnit
28
25
29limitedMapConcurrentlyTests :: TestTree
26limitedMapConcurrentlyTests :: TestTree
30−limitedMapConcurrentlyTests = testGroup "limitedMapConcurrently Tests"
27+limitedMapConcurrentlyTests =
28+ testGroup
29+ "limitedMapConcurrently Tests"
31− [ testCase "Test with various thread counts" testLMC
30+ [ testCase "Test with various thread counts" testLMC
32− ]
31+ ]
33
32
34testLMC :: Assertion
33testLMC :: Assertion
35testLMC = do
34testLMC = do
36 let maxNum = 50
35 let maxNum = 50
37 -- test with thread count of 1 to 2*maxNum
36 -- test with thread count of 1 to 2*maxNum
38− forM_ [1..(2*maxNum)] $ \threads -> do
37+ forM_ [1 .. (2 * maxNum)] $ \threads -> do
39− res <- limitedMapConcurrently threads compute [1..maxNum]
38+ res <- limitedMapConcurrently threads compute [1 .. maxNum]
40 sum res @?= overallResultCheck maxNum
39 sum res @?= overallResultCheck maxNum
41 where
40 where
42 -- simple function to run in each thread
41 -- simple function to run in each thread
43 compute :: Int -> IO Int
42 compute :: Int -> IO Int
44− compute n = return $ sum [1..n]
43+ compute n = return $ sum [1 .. n]
45−
46 -- function to check overall result
44 -- function to check overall result
47− overallResultCheck n = sum $ map (\t -> (t * (t+1)) `div` 2) [1..n]
45+ overallResultCheck n = sum $ map (\t -> (t * (t + 1)) `div` 2) [1 .. n]
test/Network/Minio/XmlGenerator/Test.hs+137−98
@@ -13,30 +13,33 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
17{-# LANGUAGE QuasiQuotes #-}
16{-# LANGUAGE QuasiQuotes #-}
18−module Network.Minio.XmlGenerator.Test
19− ( xmlGeneratorTests
20− ) where
21−
22−import Test.Tasty
23−import Test.Tasty.HUnit
24−import Text.RawString.QQ (r)
25
17
26−import Lib.Prelude
18+module Network.Minio.XmlGenerator.Test
19+ ( xmlGeneratorTests,
20+ )
21+where
27
22
23+import qualified Data.ByteString.Lazy as LBS
24+import Lib.Prelude
28−import Network.Minio.Data
25+import Network.Minio.Data
29−import Network.Minio.TestHelpers
26+import Network.Minio.TestHelpers
30−import Network.Minio.XmlGenerator
27+import Network.Minio.XmlGenerator
31−import Network.Minio.XmlParser (parseNotification)
28+import Network.Minio.XmlParser (parseNotification)
29+import Test.Tasty
30+import Test.Tasty.HUnit
31+import Text.RawString.QQ (r)
32+import Text.XML (def, parseLBS)
32
33
33xmlGeneratorTests :: TestTree
34xmlGeneratorTests :: TestTree
34−xmlGeneratorTests = testGroup "XML Generator Tests"
35+xmlGeneratorTests =
36+ testGroup
37+ "XML Generator Tests"
35− [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig
38+ [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig,
36− , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest
39+ testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest,
37− , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest
40+ testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest,
38− , testCase "Test mkSelectRequest" testMkSelectRequest
41+ testCase "Test mkSelectRequest" testMkSelectRequest
39− ]
42+ ]
40
43
41testMkCreateBucketConfig :: Assertion
44testMkCreateBucketConfig :: Assertion
42testMkCreateBucketConfig = do
45testMkCreateBucketConfig = do
@@ -44,100 +47,136 @@ testMkCreateBucketConfig = do
44 assertEqual "CreateBucketConfiguration xml should match: " expected $
47 assertEqual "CreateBucketConfiguration xml should match: " expected $
45 mkCreateBucketConfig ns "EU"
48 mkCreateBucketConfig ns "EU"
46 where
49 where
50+ expected =
47− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
51+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
48− \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
52+ \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
49− \<LocationConstraint>EU</LocationConstraint>\
53+ \<LocationConstraint>EU</LocationConstraint>\
50− \</CreateBucketConfiguration>"
54+ \</CreateBucketConfiguration>"
51
55
52testMkCompleteMultipartUploadRequest :: Assertion
56testMkCompleteMultipartUploadRequest :: Assertion
53testMkCompleteMultipartUploadRequest =
57testMkCompleteMultipartUploadRequest =
54 assertEqual "completeMultipartUpload xml should match: " expected $
58 assertEqual "completeMultipartUpload xml should match: " expected $
55− mkCompleteMultipartUploadRequest [(1, "abc")]
59+ mkCompleteMultipartUploadRequest [(1, "abc")]
56 where
60 where
61+ expected =
57− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
62+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
58− \<CompleteMultipartUpload>\
63+ \<CompleteMultipartUpload>\
59− \<Part>\
64+ \<Part>\
60− \<PartNumber>1</PartNumber><ETag>abc</ETag>\
65+ \<PartNumber>1</PartNumber><ETag>abc</ETag>\
61− \</Part>\
66+ \</Part>\
62− \</CompleteMultipartUpload>"
67+ \</CompleteMultipartUpload>"
63−
64
68
65testMkPutNotificationRequest :: Assertion
69testMkPutNotificationRequest :: Assertion
66testMkPutNotificationRequest =
70testMkPutNotificationRequest =
67 forM_ cases $ \val -> do
71 forM_ cases $ \val -> do
68 let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
72 let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
69− result = toS $ mkPutNotificationRequest ns val
73+ result = fromStrictBS $ mkPutNotificationRequest ns val
70 ntf <- runExceptT $ runTestNS $ parseNotification result
74 ntf <- runExceptT $ runTestNS $ parseNotification result
75+ either
71− either (\_ -> assertFailure "XML Parse Error!")
76+ (\_ -> assertFailure "XML Parse Error!")
72− (@?= val) ntf
77+ (@?= val)
78+ ntf
73 where
79 where
80+ cases =
74− cases = [ Notification []
81+ [ Notification
82+ []
75− [ NotificationConfig
83+ [ NotificationConfig
76− "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
84+ "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
77− "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
85+ "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
78− [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
86+ [ReducedRedundancyLostObject, ObjectCreated]
87+ defaultFilter
79− ]
88+ ]
80− []
89+ [],
81− , Notification
90+ Notification
82− [ NotificationConfig
91+ [ NotificationConfig
92+ "1"
83− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
93+ "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
84− [ObjectCreatedPut]
94+ [ObjectCreatedPut]
95+ ( Filter $
96+ FilterKey $
85− (Filter $ FilterKey $ FilterRules
97+ FilterRules
86− [ FilterRule "prefix" "images/"
98+ [ FilterRule "prefix" "images/",
87− , FilterRule "suffix" ".jpg"])
99+ FilterRule "suffix" ".jpg"
100+ ]
101+ ),
88− , NotificationConfig
102+ NotificationConfig
103+ ""
89− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
104+ "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
90− [ObjectCreated] defaultFilter
105+ [ObjectCreated]
106+ defaultFilter
91− ]
107+ ]
92− [ NotificationConfig
108+ [ NotificationConfig
109+ ""
93− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
110+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
94− [ReducedRedundancyLostObject] defaultFilter
111+ [ReducedRedundancyLostObject]
112+ defaultFilter
95− ]
113+ ]
96− [ NotificationConfig
114+ [ NotificationConfig
115+ "ObjectCreatedEvents"
97− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
116+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
98− [ObjectCreated] defaultFilter
117+ [ObjectCreated]
118+ defaultFilter
99− ]
119+ ]
100− ]
120+ ]
101
121
102testMkSelectRequest :: Assertion
122testMkSelectRequest :: Assertion
103testMkSelectRequest = mapM_ assertFn cases
123testMkSelectRequest = mapM_ assertFn cases
104 where
124 where
125+ assertFn (a, b) =
126+ let generatedReqDoc = parseLBS def $ LBS.fromStrict $ mkSelectRequest a
127+ expectedReqDoc = parseLBS def $ LBS.fromStrict b
128+ in case (generatedReqDoc, expectedReqDoc) of
105− assertFn (a, b) = assertEqual "selectRequest XML should match: " b $ mkSelectRequest a
129+ (Right genDoc, Right expDoc) -> assertEqual "selectRequest XML should match: " expDoc genDoc
130+ (Left err, _) -> assertFailure $ "Generated selectRequest failed to parse as XML" ++ show err
131+ (_, Left err) -> assertFailure $ "Expected selectRequest failed to parse as XML" ++ show err
132+ cases =
133+ [ ( SelectRequest
106− cases = [ ( SelectRequest "Select * from S3Object" SQL
134+ "Select * from S3Object"
135+ SQL
136+ ( InputSerialization
107− (InputSerialization (Just CompressionTypeGzip)
137+ (Just CompressionTypeGzip)
138+ ( InputFormatCSV $
108− (InputFormatCSV $ fileHeaderInfo FileHeaderIgnore
139+ fileHeaderInfo FileHeaderIgnore
109− <> recordDelimiter "\n"
110− <> fieldDelimiter ","
111− <> quoteCharacter "\""
112− <> quoteEscapeCharacter "\""
113− ))
114− (OutputSerializationCSV $ quoteFields QuoteFieldsAsNeeded
115− <> recordDelimiter "\n"
140+ <> recordDelimiter "\n"
116− <> fieldDelimiter ","
141+ <> fieldDelimiter ","
117− <> quoteCharacter "\""
142+ <> quoteCharacter "\""
118− <> quoteEscapeCharacter "\""
143+ <> quoteEscapeCharacter "\""
119 )
144 )
145+ )
146+ ( OutputSerializationCSV $
147+ quoteFields QuoteFieldsAsNeeded
148+ <> recordDelimiter "\n"
149+ <> fieldDelimiter ","
150+ <> quoteCharacter "\""
151+ <> quoteEscapeCharacter "\""
152+ )
120− (Just False)
153+ (Just False),
121− , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><QuoteCharacter>&#34;</QuoteCharacter><RecordDelimiter>
154+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><FieldDelimiter>,</FieldDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><RecordDelimiter>
122−</RecordDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
155+</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
123−</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
156+</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
124− )
157+ ),
125− , ( setRequestProgressEnabled False $
158+ ( setRequestProgressEnabled False $
126− setInputCompressionType CompressionTypeGzip $
159+ setInputCompressionType CompressionTypeGzip $
160+ selectRequest
127− selectRequest "Select * from S3Object" documentJsonInput
161+ "Select * from S3Object"
162+ documentJsonInput
128− (outputJSONFromRecordDelimiter "\n")
163+ (outputJSONFromRecordDelimiter "\n"),
129− , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter>
164+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter>
130</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
165</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
131− )
166+ ),
132− , ( setRequestProgressEnabled False $
167+ ( setRequestProgressEnabled False $
133− setInputCompressionType CompressionTypeNone $
168+ setInputCompressionType CompressionTypeNone $
169+ selectRequest
170+ "Select * from S3Object"
134− selectRequest "Select * from S3Object" defaultParquetInput
171+ defaultParquetInput
172+ ( outputCSVFromProps $
135− (outputCSVFromProps $ quoteFields QuoteFieldsAsNeeded
173+ quoteFields QuoteFieldsAsNeeded
136− <> recordDelimiter "\n"
174+ <> recordDelimiter "\n"
137− <> fieldDelimiter ","
175+ <> fieldDelimiter ","
138− <> quoteCharacter "\""
176+ <> quoteCharacter "\""
139− <> quoteEscapeCharacter "\"")
177+ <> quoteEscapeCharacter "\""
178+ ),
140− , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
179+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
141−</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
180+</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
142− )
181+ )
143− ]
182+ ]
test/Network/Minio/XmlParser/Test.hs+298−276
@@ -13,48 +13,49 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
17{-# LANGUAGE QuasiQuotes #-}
16{-# LANGUAGE QuasiQuotes #-}
18−module Network.Minio.XmlParser.Test
19− ( xmlParserTests
20− ) where
21−
22−import qualified Data.Map as Map
23−import Data.Time (fromGregorian)
24−import Test.Tasty
25−import Test.Tasty.HUnit
26−import Text.RawString.QQ (r)
27−import UnliftIO (MonadUnliftIO)
28
17
18+module Network.Minio.XmlParser.Test
19+ ( xmlParserTests,
20+ )
21+where
22+
23+import qualified Data.HashMap.Strict as H
24+import Data.Time (fromGregorian)
29−import Lib.Prelude
25+import Lib.Prelude
30−
31−import Network.Minio.Data
26+import Network.Minio.Data
32−import Network.Minio.Errors
27+import Network.Minio.Errors
33−import Network.Minio.TestHelpers
28+import Network.Minio.TestHelpers
34−import Network.Minio.XmlParser
29+import Network.Minio.XmlParser
30+import Test.Tasty
31+import Test.Tasty.HUnit
32+import Text.RawString.QQ (r)
33+import UnliftIO (MonadUnliftIO)
35
34
36xmlParserTests :: TestTree
35xmlParserTests :: TestTree
37−xmlParserTests = testGroup "XML Parser Tests"
36+xmlParserTests =
37+ testGroup
38+ "XML Parser Tests"
38− [ testCase "Test parseLocation" testParseLocation
39+ [ testCase "Test parseLocation" testParseLocation,
39− , testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload
40+ testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload,
40− , testCase "Test parseListObjectsResponse" testParseListObjectsResult
41+ testCase "Test parseListObjectsResponse" testParseListObjectsResult,
41− , testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result
42+ testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result,
42− , testCase "Test parseListUploadsresponse" testParseListIncompleteUploads
43+ testCase "Test parseListUploadsresponse" testParseListIncompleteUploads,
43− , testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse
44+ testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse,
44− , testCase "Test parseListPartsResponse" testParseListPartsResponse
45+ testCase "Test parseListPartsResponse" testParseListPartsResponse,
45− , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse
46+ testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse,
46− , testCase "Test parseNotification" testParseNotification
47+ testCase "Test parseNotification" testParseNotification,
47− , testCase "Test parseSelectProgress" testParseSelectProgress
48+ testCase "Test parseSelectProgress" testParseSelectProgress
48− ]
49+ ]
49
50
50tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
51tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
51−tryValidationErr act = try act
52+tryValidationErr = try
52
53
53assertValidtionErr :: MErrV -> Assertion
54assertValidtionErr :: MErrV -> Assertion
54assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
55assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
55
56
56eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
57eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
57−eitherValidationErr (Left e) _ = assertValidtionErr e
58+eitherValidationErr (Left e) _ = assertValidtionErr e
58eitherValidationErr (Right a) f = f a
59eitherValidationErr (Right a) f = f a
59
60
60testParseLocation :: Assertion
61testParseLocation :: Assertion
@@ -62,224 +63,224 @@ testParseLocation = do
62 -- 1. Test parsing of an invalid location constraint xml.
63 -- 1. Test parsing of an invalid location constraint xml.
63 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
64 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
64 when (isRight parseResE) $
65 when (isRight parseResE) $
66+ assertFailure $
65− assertFailure $ "Parsing should have failed => " ++ show parseResE
67+ "Parsing should have failed => " ++ show parseResE
66
68
67 forM_ cases $ \(xmldata, expectedLocation) -> do
69 forM_ cases $ \(xmldata, expectedLocation) -> do
68 parseLocE <- tryValidationErr $ parseLocation xmldata
70 parseLocE <- tryValidationErr $ parseLocation xmldata
69 either assertValidtionErr (@?= expectedLocation) parseLocE
71 either assertValidtionErr (@?= expectedLocation) parseLocE
70 where
72 where
71− cases = [
73+ cases =
72− -- 2. Test parsing of a valid location xml.
74+ [ -- 2. Test parsing of a valid location xml.
73− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
75+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
74− \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
76+ \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
75− "EU"
77+ "EU"
76− )
78+ ),
77− ,
78− -- 3. Test parsing of a valid, empty location xml.
79+ -- 3. Test parsing of a valid, empty location xml.
79− ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
80+ ( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
80− "us-east-1"
81+ "us-east-1"
81− )
82+ )
82 ]
83 ]
83
84
84−
85testParseNewMultipartUpload :: Assertion
85testParseNewMultipartUpload :: Assertion
86testParseNewMultipartUpload = do
86testParseNewMultipartUpload = do
87 forM_ cases $ \(xmldata, expectedUploadId) -> do
87 forM_ cases $ \(xmldata, expectedUploadId) -> do
88 parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
88 parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
89 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
89 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
90 where
90 where
91− cases = [
91+ cases =
92− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
92+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
93− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
93+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
94− \ <Bucket>example-bucket</Bucket>\
94+ \ <Bucket>example-bucket</Bucket>\
95− \ <Key>example-object</Key>\
95+ \ <Key>example-object</Key>\
96− \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
96+ \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
97− \</InitiateMultipartUploadResult>",
97+ \</InitiateMultipartUploadResult>",
98− "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
98+ "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
99− ),
99+ ),
100− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
100+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
101− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
101+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
102− \ <Bucket>example-bucket</Bucket>\
102+ \ <Bucket>example-bucket</Bucket>\
103− \ <Key>example-object</Key>\
103+ \ <Key>example-object</Key>\
104− \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
104+ \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
105− \</InitiateMultipartUploadResult>",
105+ \</InitiateMultipartUploadResult>",
106− "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
106+ "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
107− )
107+ )
108 ]
108 ]
109
109
110testParseListObjectsResult :: Assertion
110testParseListObjectsResult :: Assertion
111testParseListObjectsResult = do
111testParseListObjectsResult = do
112− let
112+ let xmldata =
113− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
113+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
114− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
114+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
115− \<Name>bucket</Name>\
115+ \<Name>bucket</Name>\
116− \<Prefix/>\
116+ \<Prefix/>\
117− \<NextContinuationToken>opaque</NextContinuationToken>\
117+ \<NextContinuationToken>opaque</NextContinuationToken>\
118− \<KeyCount>1000</KeyCount>\
118+ \<KeyCount>1000</KeyCount>\
119− \<MaxKeys>1000</MaxKeys>\
119+ \<MaxKeys>1000</MaxKeys>\
120− \<IsTruncated>true</IsTruncated>\
120+ \<IsTruncated>true</IsTruncated>\
121− \<Contents>\
121+ \<Contents>\
122− \<Key>my-image.jpg</Key>\
122+ \<Key>my-image.jpg</Key>\
123− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
123+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
124− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
124+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
125− \<Size>434234</Size>\
125+ \<Size>434234</Size>\
126− \<StorageClass>STANDARD</StorageClass>\
126+ \<StorageClass>STANDARD</StorageClass>\
127− \</Contents>\
127+ \</Contents>\
128− \</ListBucketResult>"
128+ \</ListBucketResult>"
129−
130− expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
129+ expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
131− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 Map.empty
130+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
132− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
131+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
133
132
134 parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
133 parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
135 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
134 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
136
135
137testParseListObjectsV1Result :: Assertion
136testParseListObjectsV1Result :: Assertion
138testParseListObjectsV1Result = do
137testParseListObjectsV1Result = do
139− let
138+ let xmldata =
140− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
139+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
141− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
140+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
142− \<Name>bucket</Name>\
141+ \<Name>bucket</Name>\
143− \<Prefix/>\
142+ \<Prefix/>\
144− \<NextMarker>my-image1.jpg</NextMarker>\
143+ \<NextMarker>my-image1.jpg</NextMarker>\
145− \<KeyCount>1000</KeyCount>\
144+ \<KeyCount>1000</KeyCount>\
146− \<MaxKeys>1000</MaxKeys>\
145+ \<MaxKeys>1000</MaxKeys>\
147− \<IsTruncated>true</IsTruncated>\
146+ \<IsTruncated>true</IsTruncated>\
148− \<Contents>\
147+ \<Contents>\
149− \<Key>my-image.jpg</Key>\
148+ \<Key>my-image.jpg</Key>\
150− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
149+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
151− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
150+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
152− \<Size>434234</Size>\
151+ \<Size>434234</Size>\
153− \<StorageClass>STANDARD</StorageClass>\
152+ \<StorageClass>STANDARD</StorageClass>\
154− \</Contents>\
153+ \</Contents>\
155− \</ListBucketResult>"
154+ \</ListBucketResult>"
156−
157− expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
155+ expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
158− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 Map.empty
156+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
159− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
157+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
160
158
161 parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
159 parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
162 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
160 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
163
161
164testParseListIncompleteUploads :: Assertion
162testParseListIncompleteUploads :: Assertion
165testParseListIncompleteUploads = do
163testParseListIncompleteUploads = do
166− let
164+ let xmldata =
167− xmldata = "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
165+ "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
168− \<Bucket>example-bucket</Bucket>\
166+ \<Bucket>example-bucket</Bucket>\
169− \<KeyMarker/>\
167+ \<KeyMarker/>\
170− \<UploadIdMarker/>\
168+ \<UploadIdMarker/>\
171− \<NextKeyMarker>sample.jpg</NextKeyMarker>\
169+ \<NextKeyMarker>sample.jpg</NextKeyMarker>\
172− \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
170+ \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
173− \<Delimiter>/</Delimiter>\
171+ \<Delimiter>/</Delimiter>\
174− \<Prefix/>\
172+ \<Prefix/>\
175− \<MaxUploads>1000</MaxUploads>\
173+ \<MaxUploads>1000</MaxUploads>\
176− \<IsTruncated>false</IsTruncated>\
174+ \<IsTruncated>false</IsTruncated>\
177− \<Upload>\
175+ \<Upload>\
178− \<Key>sample.jpg</Key>\
176+ \<Key>sample.jpg</Key>\
179− \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
177+ \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
180− \<Initiator>\
178+ \<Initiator>\
181− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
179+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
182− \<DisplayName>s3-nickname</DisplayName>\
180+ \<DisplayName>s3-nickname</DisplayName>\
183− \</Initiator>\
181+ \</Initiator>\
184− \<Owner>\
182+ \<Owner>\
185− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
183+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
186− \<DisplayName>s3-nickname</DisplayName>\
184+ \<DisplayName>s3-nickname</DisplayName>\
187− \</Owner>\
185+ \</Owner>\
188− \<StorageClass>STANDARD</StorageClass>\
186+ \<StorageClass>STANDARD</StorageClass>\
189− \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
187+ \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
190− \</Upload>\
188+ \</Upload>\
191− \<CommonPrefixes>\
189+ \<CommonPrefixes>\
192− \<Prefix>photos/</Prefix>\
190+ \<Prefix>photos/</Prefix>\
193− \</CommonPrefixes>\
191+ \</CommonPrefixes>\
194− \<CommonPrefixes>\
192+ \<CommonPrefixes>\
195− \<Prefix>videos/</Prefix>\
193+ \<Prefix>videos/</Prefix>\
196− \</CommonPrefixes>\
194+ \</CommonPrefixes>\
197− \</ListMultipartUploadsResult>"
195+ \</ListMultipartUploadsResult>"
198− expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
196+ expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
199− uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
197+ uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
200− initTime = UTCTime (fromGregorian 2010 11 26) 69857
198+ initTime = UTCTime (fromGregorian 2010 11 26) 69857
201− prefixes = ["photos/", "videos/"]
199+ prefixes = ["photos/", "videos/"]
202
200
203 parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
201 parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
204 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
202 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
205
203
206−
207testParseCompleteMultipartUploadResponse :: Assertion
204testParseCompleteMultipartUploadResponse :: Assertion
208testParseCompleteMultipartUploadResponse = do
205testParseCompleteMultipartUploadResponse = do
209− let
206+ let xmldata =
210− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
207+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
211−\<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
208+ \<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
212− \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
209+ \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
213− \<Bucket>Example-Bucket</Bucket>\
210+ \<Bucket>Example-Bucket</Bucket>\
214− \<Key>Example-Object</Key>\
211+ \<Key>Example-Object</Key>\
215− \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
212+ \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
216−\</CompleteMultipartUploadResult>"
213+ \</CompleteMultipartUploadResult>"
217− expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
214+ expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
218
215
219 parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
216 parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
220 eitherValidationErr parsedETagE (@?= expectedETag)
217 eitherValidationErr parsedETagE (@?= expectedETag)
221
218
222testParseListPartsResponse :: Assertion
219testParseListPartsResponse :: Assertion
223testParseListPartsResponse = do
220testParseListPartsResponse = do
224− let
221+ let xmldata =
225− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
222+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
226−\<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
223+ \<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
227− \<Bucket>example-bucket</Bucket>\
224+ \<Bucket>example-bucket</Bucket>\
228− \<Key>example-object</Key>\
225+ \<Key>example-object</Key>\
229− \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
226+ \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
230− \<Initiator>\
227+ \<Initiator>\
231− \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
228+ \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
232− \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
229+ \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
233− \</Initiator>\
230+ \</Initiator>\
234− \<Owner>\
231+ \<Owner>\
235− \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
232+ \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
236− \<DisplayName>someName</DisplayName>\
233+ \<DisplayName>someName</DisplayName>\
237− \</Owner>\
234+ \</Owner>\
238− \<StorageClass>STANDARD</StorageClass>\
235+ \<StorageClass>STANDARD</StorageClass>\
239− \<PartNumberMarker>1</PartNumberMarker>\
236+ \<PartNumberMarker>1</PartNumberMarker>\
240− \<NextPartNumberMarker>3</NextPartNumberMarker>\
237+ \<NextPartNumberMarker>3</NextPartNumberMarker>\
241− \<MaxParts>2</MaxParts>\
238+ \<MaxParts>2</MaxParts>\
242− \<IsTruncated>true</IsTruncated>\
239+ \<IsTruncated>true</IsTruncated>\
243− \<Part>\
240+ \<Part>\
244− \<PartNumber>2</PartNumber>\
241+ \<PartNumber>2</PartNumber>\
245− \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
242+ \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
246− \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
243+ \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
247− \<Size>10485760</Size>\
244+ \<Size>10485760</Size>\
248− \</Part>\
245+ \</Part>\
249− \<Part>\
246+ \<Part>\
250− \<PartNumber>3</PartNumber>\
247+ \<PartNumber>3</PartNumber>\
251− \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
248+ \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
252− \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
249+ \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
253− \<Size>10485760</Size>\
250+ \<Size>10485760</Size>\
254− \</Part>\
251+ \</Part>\
255−\</ListPartsResult>"
252+ \</ListPartsResult>"
256−
257− expectedListResult = ListPartsResult True (Just 3) [part1, part2]
253+ expectedListResult = ListPartsResult True (Just 3) [part1, part2]
258− part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
254+ part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
259− modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
255+ modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
260− part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
256+ part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
261− modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
257+ modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
262
258
263 parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
259 parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
264 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
260 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
265
261
266testParseCopyObjectResponse :: Assertion
262testParseCopyObjectResponse :: Assertion
267testParseCopyObjectResponse = do
263testParseCopyObjectResponse = do
268− let
264+ let cases =
269− cases = [ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
265+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
270−\<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
266+ \<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
271− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
267+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
272− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
268+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
273−\</CopyObjectResult>",
269+ \</CopyObjectResult>",
274− ("\"9b2cf535f27731c974343645a3985328\"",
270+ ( "\"9b2cf535f27731c974343645a3985328\"",
275− UTCTime (fromGregorian 2009 10 28) 81120))
271+ UTCTime (fromGregorian 2009 10 28) 81120
272+ )
273+ ),
276− , ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
274+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
277−\<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
275+ \<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
278− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
276+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
279− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
277+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
280−\</CopyPartResult>",
278+ \</CopyPartResult>",
281− ("\"9b2cf535f27731c974343645a3985328\"",
279+ ( "\"9b2cf535f27731c974343645a3985328\"",
282− UTCTime (fromGregorian 2009 10 28) 81120))]
280+ UTCTime (fromGregorian 2009 10 28) 81120
281+ )
282+ )
283+ ]
283
284
284 forM_ cases $ \(xmldata, (etag, modTime)) -> do
285 forM_ cases $ \(xmldata, (etag, modTime)) -> do
285 parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
286 parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
@@ -287,73 +288,89 @@ testParseCopyObjectResponse = do
287
288
288testParseNotification :: Assertion
289testParseNotification :: Assertion
289testParseNotification = do
290testParseNotification = do
290− let
291+ let cases =
291− cases = [ ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
292+ [ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
292−\ <TopicConfiguration>\
293+ \ <TopicConfiguration>\
293−\ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
294+ \ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
294−\ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
295+ \ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
295−\ <Event>s3:ReducedRedundancyLostObject</Event>\
296+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
296−\ <Event>s3:ObjectCreated:*</Event>\
297+ \ <Event>s3:ObjectCreated:*</Event>\
297−\ </TopicConfiguration>\
298+ \ </TopicConfiguration>\
298−\</NotificationConfiguration>",
299+ \</NotificationConfiguration>",
299− Notification []
300+ Notification
301+ []
300− [ NotificationConfig
302+ [ NotificationConfig
301 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
303 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
302 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
304 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
303− [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
305+ [ReducedRedundancyLostObject, ObjectCreated]
306+ defaultFilter
304− ]
307+ ]
305− [])
308+ []
309+ ),
306− , ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
310+ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
307−\ <CloudFunctionConfiguration>\
311+ \ <CloudFunctionConfiguration>\
308−\ <Id>ObjectCreatedEvents</Id>\
312+ \ <Id>ObjectCreatedEvents</Id>\
309−\ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
313+ \ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
310−\ <Event>s3:ObjectCreated:*</Event>\
314+ \ <Event>s3:ObjectCreated:*</Event>\
311−\ </CloudFunctionConfiguration>\
315+ \ </CloudFunctionConfiguration>\
312−\ <QueueConfiguration>\
316+ \ <QueueConfiguration>\
313−\ <Id>1</Id>\
317+ \ <Id>1</Id>\
314−\ <Filter>\
318+ \ <Filter>\
315−\ <S3Key>\
319+ \ <S3Key>\
316−\ <FilterRule>\
320+ \ <FilterRule>\
317−\ <Name>prefix</Name>\
321+ \ <Name>prefix</Name>\
318−\ <Value>images/</Value>\
322+ \ <Value>images/</Value>\
319−\ </FilterRule>\
323+ \ </FilterRule>\
320−\ <FilterRule>\
324+ \ <FilterRule>\
321−\ <Name>suffix</Name>\
325+ \ <Name>suffix</Name>\
322−\ <Value>.jpg</Value>\
326+ \ <Value>.jpg</Value>\
323−\ </FilterRule>\
327+ \ </FilterRule>\
324−\ </S3Key>\
328+ \ </S3Key>\
325−\ </Filter>\
329+ \ </Filter>\
326−\ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
330+ \ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
327−\ <Event>s3:ObjectCreated:Put</Event>\
331+ \ <Event>s3:ObjectCreated:Put</Event>\
328−\ </QueueConfiguration>\
332+ \ </QueueConfiguration>\
329−\ <TopicConfiguration>\
333+ \ <TopicConfiguration>\
330−\ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
334+ \ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
331−\ <Event>s3:ReducedRedundancyLostObject</Event>\
335+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
332−\ </TopicConfiguration>\
336+ \ </TopicConfiguration>\
333−\ <QueueConfiguration>\
337+ \ <QueueConfiguration>\
334−\ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
338+ \ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
335−\ <Event>s3:ObjectCreated:*</Event>\
339+ \ <Event>s3:ObjectCreated:*</Event>\
336−\ </QueueConfiguration>)\
340+ \ </QueueConfiguration>)\
337−\</NotificationConfiguration>",
341+ \</NotificationConfiguration>",
342+ Notification
338− Notification [ NotificationConfig
343+ [ NotificationConfig
344+ "1"
339− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
345+ "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
340− [ObjectCreatedPut]
346+ [ObjectCreatedPut]
347+ ( Filter $
348+ FilterKey $
341− (Filter $ FilterKey $ FilterRules
349+ FilterRules
342− [FilterRule "prefix" "images/",
350+ [ FilterRule "prefix" "images/",
343− FilterRule "suffix" ".jpg"])
351+ FilterRule "suffix" ".jpg"
352+ ]
353+ ),
344− , NotificationConfig
354+ NotificationConfig
355+ ""
345− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
356+ "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
346− [ObjectCreated] defaultFilter
357+ [ObjectCreated]
358+ defaultFilter
347− ]
359+ ]
348− [ NotificationConfig
360+ [ NotificationConfig
361+ ""
349− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
362+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
350− [ReducedRedundancyLostObject] defaultFilter
363+ [ReducedRedundancyLostObject]
364+ defaultFilter
351− ]
365+ ]
352− [ NotificationConfig
366+ [ NotificationConfig
367+ "ObjectCreatedEvents"
353− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
368+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
354− [ObjectCreated] defaultFilter
369+ [ObjectCreated]
370+ defaultFilter
355− ])
371+ ]
372+ )
356− ]
373+ ]
357
374
358 forM_ cases $ \(xmldata, val) -> do
375 forM_ cases $ \(xmldata, val) -> do
359 result <- runExceptT $ runTestNS $ parseNotification xmldata
376 result <- runExceptT $ runTestNS $ parseNotification xmldata
@@ -362,20 +379,25 @@ testParseNotification = do
362-- | Tests parsing of both progress and stats
379-- | Tests parsing of both progress and stats
363testParseSelectProgress :: Assertion
380testParseSelectProgress :: Assertion
364testParseSelectProgress = do
381testParseSelectProgress = do
382+ let cases =
365− let cases = [ ([r|<?xml version="1.0" encoding="UTF-8"?>
383+ [ ( [r|<?xml version="1.0" encoding="UTF-8"?>
366<Progress>
384<Progress>
367 <BytesScanned>512</BytesScanned>
385 <BytesScanned>512</BytesScanned>
368 <BytesProcessed>1024</BytesProcessed>
386 <BytesProcessed>1024</BytesProcessed>
369 <BytesReturned>1024</BytesReturned>
387 <BytesReturned>1024</BytesReturned>
388+</Progress>|],
370−</Progress>|] , Progress 512 1024 1024)
389+ Progress 512 1024 1024
390+ ),
371− , ([r|<?xml version="1.0" encoding="UTF-8"?>
391+ ( [r|<?xml version="1.0" encoding="UTF-8"?>
372<Stats>
392<Stats>
373 <BytesScanned>512</BytesScanned>
393 <BytesScanned>512</BytesScanned>
374 <BytesProcessed>1024</BytesProcessed>
394 <BytesProcessed>1024</BytesProcessed>
375 <BytesReturned>1024</BytesReturned>
395 <BytesReturned>1024</BytesReturned>
396+</Stats>|],
376−</Stats>|], Progress 512 1024 1024)
397+ Progress 512 1024 1024
398+ )
377− ]
399+ ]
378−
400+
379− forM_ cases $ \(xmldata, progress) -> do
401+ forM_ cases $ \(xmldata, progress) -> do
380− result <- runExceptT $ parseSelectProgress xmldata
402+ result <- runExceptT $ parseSelectProgress xmldata
381− eitherValidationErr result (@?= progress)
403+ eitherValidationErr result (@?= progress)
test/Spec.hs+83−82
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,21 +14,17 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17−import Test.Tasty
18−import Test.Tasty.QuickCheck as QC
19−
20−import qualified Data.ByteString as B
17+import qualified Data.ByteString as B
21−import qualified Data.List as L
18+import qualified Data.List as L
22−
23−import Lib.Prelude
19+import Lib.Prelude
24−
25−import Network.Minio.API.Test
20+import Network.Minio.API.Test
26−import Network.Minio.CopyObject
21+import Network.Minio.CopyObject
27−import Network.Minio.Data
22+import Network.Minio.Data
28−import Network.Minio.PutObject
29−import Network.Minio.Utils.Test
23+import Network.Minio.Utils.Test
30−import Network.Minio.XmlGenerator.Test
24+import Network.Minio.XmlGenerator.Test
31−import Network.Minio.XmlParser.Test
25+import Network.Minio.XmlParser.Test
26+import Test.Tasty
27+import Test.Tasty.QuickCheck as QC
32
28
33main :: IO ()
29main :: IO ()
34main = defaultMain tests
30main = defaultMain tests
@@ -51,82 +47,87 @@ properties = testGroup "Properties" [qcProps] -- [scProps]
51-- ]
47-- ]
52
48
53qcProps :: TestTree
49qcProps :: TestTree
50+qcProps =
51+ testGroup
54−qcProps = testGroup "(checked by QuickCheck)"
52+ "(checked by QuickCheck)"
55− [ QC.testProperty "selectPartSizes:" $
53+ [ QC.testProperty "selectPartSizes:" $
54+ \n ->
56− \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
55+ let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
57−
58 -- check that pns increments from 1.
56 -- check that pns increments from 1.
59− isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1..]
57+ isPNumsAscendingFrom1 = all (uncurry (==)) $ zip pns [1 ..]
60−
61− consPairs [] = []
58+ consPairs [] = []
62− consPairs [_] = []
59+ consPairs [_] = []
63− consPairs (a:(b:c)) = (a, b):(consPairs (b:c))
60+ consPairs (a : (b : c)) = (a, b) : consPairs (b : c)
64−
65 -- check `offs` is monotonically increasing.
61 -- check `offs` is monotonically increasing.
66− isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs
62+ isOffsetsAsc = all (uncurry (<)) $ consPairs offs
67−
68 -- check sizes sums to n.
63 -- check sizes sums to n.
69 isSumSizeOk = sum sizes == n
64 isSumSizeOk = sum sizes == n
70−
71 -- check sizes are constant except last
65 -- check sizes are constant except last
72 isSizesConstantExceptLast =
66 isSizesConstantExceptLast =
73− all (\(a, b) -> a == b) (consPairs $ L.init sizes)
67+ all (uncurry (==)) (consPairs $ L.init sizes)
74−
75 -- check each part except last is at least minPartSize;
68 -- check each part except last is at least minPartSize;
76 -- last part may be 0 only if it is the only part.
69 -- last part may be 0 only if it is the only part.
77 nparts = length sizes
70 nparts = length sizes
78 isMinPartSizeOk =
71 isMinPartSizeOk =
72+ if
79− if | nparts > 1 -> -- last part can be smaller but > 0
73+ | nparts > 1 -> -- last part can be smaller but > 0
80− all (>= minPartSize) (take (nparts - 1) sizes) &&
74+ all (>= minPartSize) (take (nparts - 1) sizes)
81− all (\s -> s > 0) (drop (nparts - 1) sizes)
75+ && all (> 0) (drop (nparts - 1) sizes)
82− | nparts == 1 -> -- size may be 0 here.
76+ | nparts == 1 -> -- size may be 0 here.
83− maybe True (\x -> x >= 0 && x <= minPartSize) $
77+ maybe True (\x -> x >= 0 && x <= minPartSize) $
84− headMay sizes
78+ listToMaybe sizes
85− | otherwise -> False
79+ | otherwise -> False
86−
87− in n < 0 ||
80+ in n < 0
88− (isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk &&
81+ || ( isPNumsAscendingFrom1
82+ && isOffsetsAsc
83+ && isSumSizeOk
89− isSizesConstantExceptLast && isMinPartSizeOk)
84+ && isSizesConstantExceptLast
90−
85+ && isMinPartSizeOk
86+ ),
91− , QC.testProperty "selectCopyRanges:" $
87+ QC.testProperty "selectCopyRanges:" $
92− \(start, end) ->
88+ \(start, end) ->
93− let (_, pairs) = L.unzip (selectCopyRanges (start, end))
89+ let (_, pairs) = L.unzip (selectCopyRanges (start, end))
94−
95− -- is last part's snd offset end?
90+ -- is last part's snd offset end?
96− isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
91+ isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
97− -- is first part's fst offset start
92+ -- is first part's fst offset start
98− isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
93+ isFirstPartOk = maybe False ((start ==) . fst) $ listToMaybe pairs
99−
100− -- each pair is >=64MiB except last, and all those parts
94+ -- each pair is >=64MiB except last, and all those parts
101− -- have same size.
95+ -- have same size.
102− initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs
96+ initSizes = maybe [] (map (\(a, b) -> b - a + 1) . init) (nonEmpty pairs)
97+ isPartSizesOk =
103− isPartSizesOk = all (>= minPartSize) initSizes &&
98+ all (>= minPartSize) initSizes
99+ && maybe
100+ True
104− maybe True (\k -> all (== k) initSizes)
101+ (\k -> all (== k) initSizes)
105− (headMay initSizes)
102+ (listToMaybe initSizes)
106−
107− -- returned offsets are contiguous.
103+ -- returned offsets are contiguous.
108− fsts = drop 1 $ map fst pairs
104+ fsts = drop 1 $ map fst pairs
109− snds = take (length pairs - 1) $ map snd pairs
105+ snds = take (length pairs - 1) $ map snd pairs
106+ isContParts =
110− isContParts = length fsts == length snds &&
107+ length fsts == length snds
111− and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
108+ && all (\(a, b) -> a == b + 1) (zip fsts snds)
112−
113− in start < 0 || start > end ||
109+ in start < 0
110+ || start > end
114− (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts)
111+ || (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts),
115−
116− , QC.testProperty "mkSSECKey:" $
112+ QC.testProperty "mkSSECKey:" $
113+ \w8s ->
117− \w8s -> let bs = B.pack w8s
114+ let bs = B.pack w8s
118− r = mkSSECKey bs
115+ r = mkSSECKey bs
119− in case r of
116+ in case r of
120− Just _ -> B.length bs == 32
117+ Just _ -> B.length bs == 32
121 Nothing -> B.length bs /= 32
118 Nothing -> B.length bs /= 32
122− ]
119+ ]
123
120
124unitTests :: TestTree
121unitTests :: TestTree
122+unitTests =
123+ testGroup
124+ "Unit tests"
125−unitTests = testGroup "Unit tests" [ xmlGeneratorTests, xmlParserTests
125+ [ xmlGeneratorTests,
126+ xmlParserTests,
126− , bucketNameValidityTests
127+ bucketNameValidityTests,
127− , objectNameValidityTests
128+ objectNameValidityTests,
128− , parseServerInfoJSONTest
129+ parseServerInfoJSONTest,
129− , parseHealStatusTest
130+ parseHealStatusTest,
130− , parseHealStartRespTest
131+ parseHealStartRespTest,
131− , limitedMapConcurrentlyTests
132+ limitedMapConcurrentlyTests
132− ]
133+ ]
test/cert/private.keynew file+28
@@ -0,0 +1,28 @@
1+-----BEGIN PRIVATE KEY-----
2+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC3G9IiC+adjf0p
3+i/2KYc+4dizeuzUFN7wraSdhiOMdQgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwI
4+rPJ61dRGQSuN12l+mzngFJQjE0sysZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPY
5+fhcUcq03onMGq44yOfE6mIhoe0Y9wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq
6+5KjHL8WW2vGg7G9edpYdxINA/A2fdLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L
7+9yRqItqddriRxJFwOXb5OPW8xx2WGaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A8
8+4TD/RXMbAgMBAAECggEBAJ7r1oUWLyGvinn0tijUm6RNbMQjVvEgXoCO008jr3pF
9+PqxVpgEMrOa/4tmwFBus0jcCNF4t3r2zhddBw3I5A/O1vEdvHnBz6NdDBQ8sP6fP
10+1fF50iEe1Y2MBibQkXFxxVMG2QRB1Gt5nuvXA9ELdqtCovK3EsMk5ukkWb/UvjH5
11+8hcmQsaSqvzFEF4wJSY2mkeGSGIJTphPhhuA22xbhaBMInQyhZu8EHsn0h6s/Wgy
12+C4Cp2+4qZTKaaf6x3/ZjJ8CuKiSX+ZsJKjOEv8sqx7j/Y7QFOmJPewInKDhwazr/
13+xIK+N0KXPbUzeSEz6ZvExNDTxtR5ZlQP2UrRDg28yQECgYEA4Is1O2BvKVzNFOkj
14+bTVz25a/bb0Xrcfgi0Y9rdfLzlNdItFjAkxLTVRSW2Hv9ICl0RDDAG+wTlktXRdh
15+rfvDjwG2CvLQo1VEdMWTTkKVg03SwMEy2hFiWV69lENFGSaY8Y6unZDbia5HQinA
16+EgSS4sCojS+a2jtzG5FVVHJDKlkCgYEA0MKhMhD4SUhr2y1idPBrmLxuW5mVozuW
17+8bYaBeSzmfS0BRsN4fP9JGODPBPDdNbfGfGC9ezWLgD/lmCgjIEyBOq8EmqWSsiS
18+Kihds1+Z7hXtbzGsFGAFJJTIh7blBCsK5QFuyuih2UG0fL9z6K/dy+UUJkzrYqph
19+vSfKixyM8pMCgYEAmUPLsNyw4325aeV8TeWnUCJERaZFDFQa21W1cfyS2yEhuEtN
20+llr3JzBACqn9vFk3VU1onNqfb8sE4L696KCpKeqUFEMK0AG6eS4Gzus53Gb5TKJS
21+kHA/PhshsZp9Bp7G1FJ8s4YVo5N2hh2zQVkn3Wh9Y+kzfHQJrK51nO9lEvkCgYBi
22+BuKWle1gzAcJdnhDHRoJMIJJtQbVDYhFnBMALXJAmu1lcFzGe0GlMq1PKqCfXr6I
23+eiXawQmZtJJP1LPPBmOsd2U06KQGHcS00xucvQmVCOrjSdnZ/3SqxsqbH8DOgj+t
24+ZUzXLwHA+N99rJEK9Hob4kfh7ECjpgobPnIXfKKazQKBgQChAuiXHtf/Qq18hY3u
25+x48zFWjGgfd6GpOBZYkXOwGdCJgnYjZbE26LZEnYbwPh8ZUA2vp7mgHRJkD5e3Fj
26+ERuJLCw86WqyYZmLEuBciYGjCZqR5nbavfwsziWD00jeNruds2ZwKxRfFm4V7o2S
27+WLd/RUatd2Uu9f3B2J78OUdnxg==
28+-----END PRIVATE KEY-----
test/cert/public.crtnew file+19
@@ -0,0 +1,19 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDCzCCAfOgAwIBAgIUaIUOMI78LCu+r1zl0mmFHK8n5/AwDQYJKoZIhvcNAQEL
3+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE5MTAyNDE5NTMxOVoYDzIxMTkw
4+OTMwMTk1MzE5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
5+AQUAA4IBDwAwggEKAoIBAQC3G9IiC+adjf0pi/2KYc+4dizeuzUFN7wraSdhiOMd
6+QgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwIrPJ61dRGQSuN12l+mzngFJQjE0sy
7+sZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPYfhcUcq03onMGq44yOfE6mIhoe0Y9
8+wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq5KjHL8WW2vGg7G9edpYdxINA/A2f
9+dLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L9yRqItqddriRxJFwOXb5OPW8xx2W
10+GaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A84TD/RXMbAgMBAAGjUzBRMB0GA1Ud
11+DgQWBBSEWXQ2JRD+OK7/KTmlD+OW16pGmzAfBgNVHSMEGDAWgBSEWXQ2JRD+OK7/
12+KTmlD+OW16pGmzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCF
13+0zYRaabB3X0jzGI9/Lr3Phrb90GvoL1DFLRuiOuTlDkz0vrm/HrZskwHCgMNrkCj
14+OTD9Vpas4D1QZBbQbRzfnf3OOoG4bgmcCwLFZl3dy27yIDAhrmbUP++g9l1Jmy4v
15+vBR/M4lt2scQ8LcZYEPqhEaE5EzFQEjtaxDcKdWDNKY9W1NUzSIABhF9eHiAUNdH
16+AFNJlYeBlCHxcWIeqgon184Dqp/CsvKtz3z3Ni+rlwPM/zuJCFHh1VF+z++0LJjG
17+roBCV0Tro4XyiEz9yp7Cb5kQYMaj1KL9TqBG0tZx0pmv7y+lXc4TT6DEllXz6USy
18+rbIba9/uUet3BqeIMTqj
19+-----END CERTIFICATE-----