git.iko.soy/typeable/minio-hs
on cc93097 help sign in
72 files changed+5127−7298
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml−266 .stylish-haskell.yaml+233 .travis.yml+52 CHANGELOG.md−62 CNAME−1 README.md+31−98 Setup.hs+18 docs/API.md+182−183 examples/AssumeRole.hs−47 examples/BucketExists.hs+9−6 examples/CopyObject.hs+17−16 examples/FileUploader.hs+28−28 examples/GetConfig.hs+7−7 examples/GetObject.hs+14−9 examples/HeadObject.hs+13−10 examples/Heal.hs+10−13 examples/ListBuckets.hs+7−5 examples/ListIncompleteUploads.hs+21−19 examples/ListObjects.hs+17−15 examples/MakeBucket.hs+11−8 examples/PresignedGetObject.hs+33−37 examples/PresignedPostPolicy.hs+41−44 examples/PresignedPutObject.hs+23−22 examples/PutObject.hs+16−13 examples/README.md−17 examples/RemoveBucket.hs+10−5 examples/RemoveIncompleteUpload.hs+12−9 examples/RemoveObject.hs+9−8 examples/SelectObject.hs+28−25 examples/ServerInfo.hs+8−8 examples/ServiceSendRestart.hs+8−7 examples/ServiceSendStop.hs+8−7 examples/ServiceStatus.hs+8−8 examples/SetConfig.hs+6−5 minio-hs.cabal+170−249 src/Lib/Prelude.hs+11−33 src/Network/Minio.hs+218−254 src/Network/Minio/API.hs+159−259 src/Network/Minio/APICommon.hs+24−36 src/Network/Minio/AdminAPI.hs+391−543 src/Network/Minio/CopyObject.hs+34−48 src/Network/Minio/Credentials.hs−77 src/Network/Minio/Credentials/AssumeRole.hs−266 src/Network/Minio/Credentials/Types.hs−90 src/Network/Minio/Data.hs+557−730 src/Network/Minio/Data/ByteString.hs+18−19 src/Network/Minio/Data/Crypto.hs+33−32 src/Network/Minio/Data/Time.hs+13−17 src/Network/Minio/Errors.hs+52−59 src/Network/Minio/JsonParser.hs+16−23 src/Network/Minio/ListOps.hs+38−98 src/Network/Minio/PresignedOperations.hs+152−215 src/Network/Minio/PutObject.hs+69−85 src/Network/Minio/S3API.hs+314−486 src/Network/Minio/SelectAPI.hs+241−233 src/Network/Minio/Sign/V4.hs+329−432 src/Network/Minio/Utils.hs+123−150 src/Network/Minio/XmlCommon.hs−65 src/Network/Minio/XmlGenerator.hs+99−165 src/Network/Minio/XmlParser.hs+152−107 stack.yaml+4−4 stack.yaml.lock−19 test/LiveServer.hs+461−807 test/Network/Minio/API/Test.hs+61−75 test/Network/Minio/JsonParser/Test.hs+27−26 test/Network/Minio/TestHelpers.hs+8−8 test/Network/Minio/Utils/Test.hs+17−15 test/Network/Minio/XmlGenerator/Test.hs+98−137 test/Network/Minio/XmlParser/Test.hs+276−298 test/Spec.hs+82−83 test/cert/private.key−28 test/cert/public.crt−19
.github/workflows/ci.ymldeleted−266
@@ -1,266 +0,0 @@
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.yamlnew file+233
@@ -0,0 +1,233 @@
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.ymlnew file+52
@@ -0,0 +1,52 @@
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,68 +1,6 @@
11Changelog
22==========
33
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−
664## Version 1.3.1
675
686* Add TLS helpers to check if server uses TLS, and to disable
CNAMEdeleted−1
@@ -1 +0,0 @@
1−minio-hs.min.io
README.md+31−98
@@ -1,65 +1,53 @@
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)
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)
22
3−The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
3+The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and Amazon S3 compatible object storage server.
44
5−This guide assumes that you have a working [Haskell development environment](https://www.haskell.org/downloads/).
5+## Minimum Requirements
66
7−## Installation
7+- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
88
9−### Add to your project
9+## Installation
1010
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.
11+```sh
12+git clone https://github.com/minio/minio-hs.git
1213
13−### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
14+cd minio-hs/
1415
15−#### For a cabal based environment
16+stack install
17+```
1618
17−Download the library source and change to the extracted directory:
19+Tests can be run with:
1820
19−``` sh
20−$ cabal get minio-hs
21−$ cd minio-hs-1.6.0/ # directory name could be different
22−```
21+```sh
2322
24−Then load the `ghci` REPL environment with the library and browse the available APIs:
23+stack test
2524
26−``` sh
27−$ cabal repl
28−ghci> :browse Network.Minio
2925```
3026
31−#### For a stack based environment
27+A section of the tests use the remote MinIO Play server at
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).
3233
33−From your home folder or any non-haskell project directory, just run:
34+Documentation can be locally built with:
3435
3536```sh
36−stack install minio-hs
37−```
3837
39−Then start an interpreter session and browse the available APIs with:
38+stack haddock
4039
41−```sh
42−$ stack ghci
43−> :browse Network.Minio
4440```
4541
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−
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.
42+## Quick-Start Example - File Uploader
5543
5644### FileUploader.hs
5745``` haskell
5846#!/usr/bin/env stack
59−-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
47+-- stack --resolver lts-11.1 runghc --package minio-hs --package optparse-applicative --package filepath
6048
6149--
62−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
50+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6351--
6452-- Licensed under the Apache License, Version 2.0 (the "License");
6553-- you may not use this file except in compliance with the License.
@@ -88,7 +76,7 @@ import UnliftIO (throwIO, try)
8876import Prelude
8977
9078-- | The following example uses minio's play server at
91−-- https://play.min.io. The endpoint and associated
79+-- https://play.min.io:9000. The endpoint and associated
9280-- credentials are provided via the libary constant,
9381--
9482-- > minioPlayCI :: ConnectInfo
@@ -118,19 +106,16 @@ main = do
118106 res <- runMinio minioPlayCI $ do
119107 -- Make a bucket; catch bucket already exists exception if thrown.
120108 bErr <- try $ makeBucket bucket Nothing
121−
122− -- If the bucket already exists, we would get a specific
123− -- `ServiceErr` exception thrown.
124109 case bErr of
125− Left BucketAlreadyOwnedByYou -> return ()
126− Left e -> throwIO e
127− Right _ -> return ()
110+ Left (MErrService BucketAlreadyOwnedByYou) -> return ()
111+ Left e -> throwIO e
112+ Right _ -> return ()
128113
129− -- Upload filepath to bucket; object name is derived from filepath.
130− fPutObject bucket object filepath defaultPutObjectOptions
114+ -- Upload filepath to bucket; object is derived from filepath.
115+ fPutObject bucket object filepath def
131116
132117 case res of
133− Left e -> putStrLn $ "file upload failed due to " ++ show e
118+ Left e -> putStrLn $ "file upload failed due to " ++ (show e)
134119 Right () -> putStrLn "file upload succeeded."
135120```
136121
@@ -144,55 +129,3 @@ main = do
144129## Contribute
145130
146131[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.hsnew file+18
@@ -0,0 +1,18 @@
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+182−183
@@ -20,15 +20,15 @@ awsCI { connectAccesskey = "your-access-key"
2020
2121```
2222
23−| Bucket operations | Object Operations | Presigned Operations |
24−|:--------------------------------------------------|:----------------------------------------------|:--------------------------------------------------|
25−| [`listBuckets`](#listBuckets) | [`getObject`](#getObject) | [`presignedGetObjectUrl`](#presignedGetObjectUrl) |
26−| [`makeBucket`](#makeBucket) | [`putObject`](#putObject) | [`presignedPutObjectUrl`](#presignedPutObjectUrl) |
27−| [`removeBucket`](#removeBucket) | [`fGetObject`](#fGetObject) | [`presignedPostPolicy`](#presignedPostPolicy) |
28−| [`listObjects`](#listObjects) | [`fPutObject`](#fPutObject) | |
29−| [`listObjectsV1`](#listObjectsV1) | [`copyObject`](#copyObject) | |
30−| [`listIncompleteUploads`](#listIncompleteUploads) | [`removeObject`](#removeObject) | |
31−| [`bucketExists`](#bucketExists) | [`selectObjectContent`](#selectObjectContent) | |
23+|Bucket operations|Object Operations|Presigned Operations|
24+|:---|:---|:---|
25+|[`listBuckets`](#listBuckets) |[`getObject`](#getObject)|[`presignedGetObjectUrl`](#presignedGetObjectUrl)|
26+|[`makeBucket`](#makeBucket)|[`putObject`](#putObject)|[`presignedPutObjectUrl`](#presignedPutObjectUrl)|
27+|[`removeBucket`](#removeBucket)|[`fGetObject`](#fGetObject)|[`presignedPostPolicy`](#presignedPostPolicy)|
28+|[`listObjects`](#listObjects)|[`fPutObject`](#fPutObject)||
29+|[`listObjectsV1`](#listObjectsV1)|[`copyObject`](#copyObject)||
30+|[`listIncompleteUploads`](#listIncompleteUploads)|[`removeObject`](#removeObject)||
31+|[`bucketExists`](#bucketExists)|[`selectObjectContent`](#selectObjectContent)||
3232
3333## 1. Connecting and running operations on the storage service
3434
@@ -69,16 +69,16 @@ enable/disable the automatic region discovery behaviour.
6969
7070The parameters in the expression `awsWithRegion region autoDiscover` are:
7171
72−| Parameter | Type | Description |
73−|:---------------|:----------------------------|:---------------------------------------------------------------------------------------------------------------------------|
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. |
72+|Parameter|Type|Description|
73+|:---|:---|:---|
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.|
7676
7777#### minioPlayCI :: ConnectInfo
7878
7979This constructor provides connection and authentication information to
8080connect to the public MinIO Play server at
81−`https://play.min.io/`.
81+`https://play.min.io:9000/`.
8282
8383#### minioCI :: Text -> Int -> Bool -> ConnectInfo
8484
@@ -86,25 +86,25 @@ Use to connect to a MinIO server.
8686
8787The parameters in the expression `minioCI host port isSecure` are:
8888
89−| Parameter | Type | Description |
90−|:-----------|:-------|:--------------------------------------------------------|
91−| `host` | _Text_ | Hostname of the MinIO or other S3-API compatible server |
92−| `port` | _Int_ | Port number to connect to |
93−| `isSecure` | _Bool_ | Does the server use HTTPS? |
89+|Parameter|Type|Description|
90+|:---|:---|:---|
91+| `host` | _Text_ | Hostname of the MinIO or other S3-API compatible server |
92+| `port` | _Int_ | Port number to connect to|
93+| `isSecure` | _Bool_ | Does the server use HTTPS? |
9494
9595#### The ConnectInfo fields and Default instance
9696
9797The following table shows the fields in the `ConnectInfo` record-type:
9898
99−| Field | Type | Description |
100−|:----------------------------|:----------------------------|:--------------------------------------------------------------------------------------------------|
101−| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
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`. |
104−| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
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' |
107−| `connectAutoDiscoverRegion` | _Bool_ | Specifies if the library should automatically discover the region of a bucket. Defaults to `True` |
99+| Field | Type | Description |
100+|:---|:---|:---|
101+| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
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`. |
104+| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
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' |
107+| `connectAutoDiscoverRegion` | _Bool_ | Specifies if the library should automatically discover the region of a bucket. Defaults to `True`|
108108
109109The `def` value of type `ConnectInfo` has all the above default
110110values.
@@ -148,17 +148,17 @@ Lists buckets.
148148
149149__Return Value__
150150
151−| Return type | Description |
152−|:---------------------|:----------------|
153−| _Minio [BucketInfo]_ | List of buckets |
151+|Return type |Description |
152+|:---|:---|
153+| _Minio [BucketInfo]_| List of buckets |
154154
155155
156156__BucketInfo record type__
157157
158−| Field | Type | Description |
159−|:-----------------|:---------------------------|:----------------------------|
160−| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
161−| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
158+|Field |Type |Description |
159+|:---|:---| :---|
160+| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
161+| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
162162
163163
164164<a name="makeBucket"></a>
@@ -171,10 +171,10 @@ __Parameters__
171171
172172In the expression `makeBucket bucketName region` the arguments are:
173173
174−| Param | Type | Description |
175−|--------------|-----------------------------|-----------------------------------------------------------------------------------------------------|
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`. |
174+| Param | Type | Description |
175+|---|---|---|
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`.|
178178
179179__Example__
180180
@@ -200,9 +200,9 @@ __Parameters__
200200
201201In the expression `removeBucket bucketName` the arguments are:
202202
203−| Param | Type | Description |
204−|--------------|-----------------------------|--------------------|
205−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
203+| Param | Type | Description |
204+|---|---|---|
205+|`bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
206206
207207
208208__Example__
@@ -233,27 +233,26 @@ __Parameters__
233233In the expression `listObjects bucketName prefix recursive` the
234234arguments are:
235235
236−| Param | Type | Description |
237−|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
238−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
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 '/'. |
236+|Param |Type |Description |
237+|:---|:---| :---|
238+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
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 '/'. |
241241
242242__Return Value__
243243
244−| Return type | Description |
245−|:------------------------------------|:------------------------------------------------------------------------|
246−| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
244+|Return type |Description |
245+|:---|:---|
246+| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
247247
248248__ObjectInfo record type__
249249
250−| Field | Type | Description |
251−|:-------------|:----------------------------|:-------------------------------------|
252−| `oiObject` | _Object_ (alias for `Text`) | Name of object |
253−| `oiModTime` | _UTCTime_ | Last modified time of the object |
254−| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
255−| `oiSize` | _Int64_ | Size of the object in bytes |
256−| `oiMetadata` | _HashMap Text Text_ | Map of key-value user-metadata pairs |
250+|Field |Type |Description |
251+|:---|:---| :---|
252+|`oiObject` | _Object_ (alias for `Text`) | Name of object |
253+|`oiModTime` | _UTCTime_ | Last modified time of the object |
254+|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
255+|`oiSize` | _Int64_ | Size of the object in bytes |
257256
258257__Example__
259258
@@ -266,7 +265,7 @@ import Prelude
266265
267266
268267-- | The following example uses MinIO play server at
269−-- https://play.min.io. The endpoint and associated
268+-- https://play.min.io:9000. The endpoint and associated
270269-- credentials are provided via the libary constant,
271270--
272271-- > minioPlayCI :: ConnectInfo
@@ -295,26 +294,26 @@ __Parameters__
295294In the expression `listObjectsV1 bucketName prefix recursive` the
296295arguments are:
297296
298−| Param | Type | Description |
299−|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
300−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
301−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
302−| `recursive` | _Bool_ | `True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
297+|Param |Type |Description |
298+|:---|:---| :---|
299+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
300+| `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 '/'. |
303302
304303__Return Value__
305304
306−| Return type | Description |
307−|:------------------------------------|:------------------------------------------------------------------------|
308−| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
305+|Return type |Description |
306+|:---|:---|
307+| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
309308
310309__ObjectInfo record type__
311310
312−| Field | Type | Description |
313−|:------------|:----------------------------|:---------------------------------|
314−| `oiObject` | _Object_ (alias for `Text`) | Name of object |
315−| `oiModTime` | _UTCTime_ | Last modified time of the object |
316−| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
317−| `oiSize` | _Int64_ | Size of the object in bytes |
311+|Field |Type |Description |
312+|:---|:---| :---|
313+|`oiObject` | _Object_ (alias for `Text`) | Name of object |
314+|`oiModTime` | _UTCTime_ | Last modified time of the object |
315+|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
316+|`oiSize` | _Int64_ | Size of the object in bytes |
318317
319318__Example__
320319
@@ -327,7 +326,7 @@ import Prelude
327326
328327
329328-- | The following example uses MinIO play server at
330−-- https://play.min.io. The endpoint and associated
329+-- https://play.min.io:9000. The endpoint and associated
331330-- credentials are provided via the libary constant,
332331--
333332-- > minioPlayCI :: ConnectInfo
@@ -355,11 +354,11 @@ __Parameters__
355354In the expression `listIncompleteUploads bucketName prefix recursive`
356355the parameters are:
357356
358−| Param | Type | Description |
359−|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
360−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
361−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have. |
362−| `recursive` | _Bool_ | `True` indicates recursive style listing and `Talse` indicates directory style listing delimited by '/'. |
357+|Param |Type |Description |
358+|:---|:---| :---|
359+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
360+| `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 '/'. |
363362
364363__Return Value__
365364
@@ -369,11 +368,11 @@ __Return Value__
369368
370369__UploadInfo record type__
371370
372−| Field | Type | Description |
373−|:-------------|:---------|:------------------------------------------|
374−| `uiKey` | _Object_ | Name of incompletely uploaded object |
375−| `uiUploadId` | _String_ | Upload ID of incompletely uploaded object |
376−| `uiSize` | _Int64_ | Size of incompletely uploaded object |
371+|Field |Type |Description |
372+|:---|:---| :---|
373+|`uiKey` | _Object_ |Name of incompletely uploaded object |
374+|`uiUploadId` | _String_ |Upload ID of incompletely uploaded object |
375+|`uiSize` | _Int64_ |Size of incompletely uploaded object |
377376
378377__Example__
379378
@@ -385,7 +384,7 @@ import Conduit
385384import Prelude
386385
387386-- | The following example uses MinIO play server at
388−-- https://play.min.io. The endpoint and associated
387+-- https://play.min.io:9000. The endpoint and associated
389388-- credentials are provided via the libary constant,
390389--
391390-- > minioPlayCI :: ConnectInfo
@@ -416,29 +415,29 @@ __Parameters__
416415In the expression `getObject bucketName objectName opts` the parameters
417416are:
418417
419−| Param | Type | Description |
420−|:-------------|:----------------------------|:----------------------------------------------------------------------------|
421−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
422−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
423−| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
418+|Param |Type |Description |
419+|:---|:---| :---|
420+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
421+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
422+| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
424423
425424__GetObjectOptions record type__
426425
427−| Field | Type | Description |
428−|:-----------------------|:--------------------------------|:------------------------------------------------------------------------------------------------------|
429−| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object |
430−| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
431−| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
432−| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
433−| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
426+|Field |Type |Description |
427+|:---|:---| :---|
428+| `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+| `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+| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
434433
435434__Return Value__
436435
437436The return value can be incrementally read to process the contents of
438437the object.
439−| Return type | Description |
440−|:--------------------------------------------|:-----------------------------------------|
441−| _Minio (C.ConduitM () ByteString Minio ())_ | A Conduit source of `ByteString` values. |
438+|Return type |Description |
439+|:---|:---|
440+| _Minio (C.ConduitM () ByteString Minio ())_ | A Conduit source of `ByteString` values. |
442441
443442__Example__
444443
@@ -452,7 +451,7 @@ import qualified Data.Conduit.Binary as CB
452451import Prelude
453452
454453-- | The following example uses MinIO play server at
455−-- https://play.min.io. The endpoint and associated
454+-- https://play.min.io:9000. The endpoint and associated
456455-- credentials are provided via the libary constant,
457456--
458457-- > minioPlayCI :: ConnectInfo
@@ -483,13 +482,13 @@ __Parameters__
483482In the expression `putObject bucketName objectName inputSrc` the parameters
484483are:
485484
486−| Param | Type | Description |
487−|:-------------|:------------------------------------|:------------------------------------------------------------------|
488−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
489−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
490−| `inputSrc` | _C.ConduitM () ByteString Minio ()_ | A Conduit producer of `ByteString` values |
491−| `size` | _Int64_ | Provide stream size (optional) |
492−| `opts` | _PutObjectOptions_ | Optional parameters to provide additional metadata for the object |
485+|Param |Type |Description |
486+|:---|:---| :---|
487+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
488+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
489+| `inputSrc` | _C.ConduitM () ByteString Minio ()_ | A Conduit producer of `ByteString` values |
490+| `size` | _Int64_ | Provide stream size (optional) |
491+| `opts` | _PutObjectOptions_ | Optional parameters to provide additional metadata for the object |
493492
494493__Example__
495494
@@ -502,7 +501,7 @@ import qualified Data.Conduit.Combinators as CC
502501import Prelude
503502
504503-- | The following example uses MinIO play server at
505−-- https://play.min.io. The endpoint and associated
504+-- https://play.min.io:9000. The endpoint and associated
506505-- credentials are provided via the libary constant,
507506--
508507-- > minioPlayCI :: ConnectInfo
@@ -534,23 +533,23 @@ __Parameters__
534533In the expression `fGetObject bucketName objectName inputFile` the parameters
535534are:
536535
537−| Param | Type | Description |
538−|:-------------|:----------------------------|:----------------------------------------------------------------------------|
539−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
540−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
541−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
542−| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
536+|Param |Type |Description |
537+|:---|:---| :---|
538+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
539+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
540+| `inputFile` | _FilePath_ | Path to the file to be uploaded |
541+| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
543542
544543
545544__GetObjectOptions record type__
546545
547−| Field | Type | Description |
548−|:-----------------------|:--------------------------------|:------------------------------------------------------------------------------------------------------|
549−| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object |
550−| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
551−| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
552−| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
553−| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
546+|Field |Type |Description |
547+|:---|:---| :---|
548+| `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+| `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+| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
554553
555554``` haskell
556555
@@ -562,7 +561,7 @@ import Data.Conduit.Binary (sinkLbs)
562561import Prelude
563562
564563-- | The following example uses MinIO play server at
565−-- https://play.min.io. The endpoint and associated
564+-- https://play.min.io:9000. The endpoint and associated
566565-- credentials are provided via the libary constant,
567566--
568567-- > minioPlayCI :: ConnectInfo
@@ -593,11 +592,11 @@ __Parameters__
593592In the expression `fPutObject bucketName objectName inputFile` the parameters
594593are:
595594
596−| Param | Type | Description |
597−|:-------------|:----------------------------|:--------------------------------|
598−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
599−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
600−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
595+|Param |Type |Description |
596+|:---|:---| :---|
597+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
598+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
599+| `inputFile` | _FilePath_ | Path to the file to be uploaded |
601600
602601__Example__
603602
@@ -630,28 +629,28 @@ __Parameters__
630629In the expression `copyObject dstInfo srcInfo` the parameters
631630are:
632631
633−| Param | Type | Description |
634−|:----------|:------------------|:----------------------------------------------------------|
632+|Param |Type |Description |
633+|:---|:---| :---|
635634| `dstInfo` | _DestinationInfo_ | A value representing properties of the destination object |
636−| `srcInfo` | _SourceInfo_ | A value representing properties of the source object |
635+| `srcInfo` | _SourceInfo_ | A value representing properties of the source object |
637636
638637
639638__SourceInfo record type__
640639
641−| Field | Type | Description |
642−|:-----------------------|:-----------------------|:----------------------------------------------------------------------------------------------------------|
643−| `srcBucket` | `Bucket` | Name of source bucket |
644−| `srcObject` | `Object` | Name 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 |
646−| `srcIfMatch` | `Maybe Text` | (Optional) ETag source object should match |
647−| `srcIfNoneMatch` | `Maybe Text` | (Optional) ETag source object shouldn't match |
648−| `srcIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since source object wasn't modified |
649−| `srcIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since source object was modified |
640+|Field |Type |Description |
641+|:---|:---| :---|
642+| `srcBucket` | `Bucket` | Name of source bucket |
643+| `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+| `srcIfMatch` | `Maybe Text` | (Optional) ETag source object should match |
646+| `srcIfNoneMatch` | `Maybe Text` | (Optional) ETag source object shouldn't match |
647+| `srcIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since source object wasn't modified |
648+| `srcIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since source object was modified |
650649
651650__Destination record type__
652651
653−| Field | Type | Description |
654−|:------------|:---------|:-----------------------------------------------------|
652+|Field |Type |Description |
653+|:---|:---| :---|
655654| `dstBucket` | `Bucket` | Name of destination bucket in server-side copyObject |
656655| `dstObject` | `Object` | Name of destination object in server-side copyObject |
657656
@@ -685,10 +684,10 @@ __Parameters__
685684In the expression `removeObject bucketName objectName` the parameters
686685are:
687686
688−| Param | Type | Description |
689−|:-------------|:----------------------------|:-------------------|
690−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
691−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
687+|Param |Type |Description |
688+|:---|:---| :---|
689+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
690+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
692691
693692__Example__
694693
@@ -719,10 +718,10 @@ __Parameters__
719718In the expression `removeIncompleteUpload bucketName objectName` the parameters
720719are:
721720
722−| Param | Type | Description |
723−|:-------------|:----------------------------|:-------------------|
724−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
725−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
721+|Param |Type |Description |
722+|:---|:---| :---|
723+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
724+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
726725
727726__Example__
728727
@@ -753,11 +752,11 @@ __Parameters__
753752In the expression `selectObjectContent bucketName objectName selReq`
754753the parameters are:
755754
756−| Param | Type | Description |
757−|:-------------|:----------------------------|:--------------------------|
758−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
759−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
760−| `selReq` | _SelectRequest_ | Select request parameters |
755+|Param |Type |Description |
756+|:---|:---| :---|
757+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
758+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
759+| `selReq` | _SelectRequest_ | Select request parameters |
761760
762761__SelectRequest record__
763762
@@ -805,9 +804,9 @@ __Parameters__
805804
806805In the expression `bucketExists bucketName` the parameters are:
807806
808−| Param | Type | Description |
809−|:-------------|:----------------------------|:-------------------|
810−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
807+|Param |Type |Description |
808+|:---|:---| :---|
809+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
811810
812811
813812## 4. Presigned operations
@@ -829,22 +828,22 @@ __Parameters__
829828In the expression `presignedGetObjectUrl bucketName objectName expiry queryParams headers`
830829the parameters are:
831830
832−| Param | Type | Description |
833−|:--------------|:---------------------------------------------------------------|:------------------------------------------------|
834−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
835−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
836−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
837−| `queryParams` | _Query_ (from package `http-types:Network.HTTP.Types`) | Query parameters to add to the URL |
838−| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
831+|Param |Type |Description |
832+|:---|:---| :---|
833+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
834+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
835+| `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+| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
839838
840839__Return Value__
841840
842841Returns the generated URL - it will include authentication
843842information.
844843
845−| Return type | Description |
846−|:-------------|:------------------------|
847−| _ByteString_ | Generated presigned URL |
844+|Return type |Description |
845+|:---|:---|
846+| _ByteString_ | Generated presigned URL |
848847
849848__Example__
850849
@@ -887,21 +886,21 @@ __Parameters__
887886In the expression `presignedPutObjectUrl bucketName objectName expiry headers`
888887the parameters are:
889888
890−| Param | Type | Description |
891−|:-------------|:---------------------------------------------------------------|:------------------------------------------------|
892−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
893−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
894−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
895−| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
889+|Param |Type |Description |
890+|:---|:---| :---|
891+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
892+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
893+| `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 |
896895
897896__Return Value__
898897
899898Returns the generated URL - it will include authentication
900899information.
901900
902−| Return type | Description |
903−|:-------------|:------------------------|
904−| _ByteString_ | Generated presigned URL |
901+|Return type |Description |
902+|:---|:---|
903+| _ByteString_ | Generated presigned URL |
905904
906905__Example__
907906
@@ -929,7 +928,7 @@ main = do
929928```
930929
931930<a name="presignedPostPolicy"></a>
932−### presignedPostPolicy :: PostPolicy -> Minio (ByteString, HashMap Text ByteString)
931+### presignedPostPolicy :: PostPolicy -> Minio (ByteString, Map.Map Text ByteString)
933932
934933Generate a presigned URL and POST policy to upload files via a POST
935934request. This is intended for browser uploads and generates form data
@@ -941,10 +940,10 @@ The `PostPolicy` argument is created using the `newPostPolicy` function:
941940
942941In the expression `newPostPolicy expirationTime conditions` the parameters are:
943942
944−| Param | Type | Description |
945−|:-----------------|:--------------------------------------------------|:---------------------------------------------|
946−| `expirationTime` | _UTCTime_ (from package `time:Data.Time.UTCTime`) | The expiration time for the policy |
947−| `conditions` | _[PostPolicyConditions]_ | List of conditions to be added to the policy |
943+|Param | Type| Description |
944+|:---|:---|:---|
945+| `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 |
948947
949948The policy conditions are created using various helper functions -
950949please refer to the Haddocks for details.
@@ -966,7 +965,7 @@ import Network.Minio
966965
967966import qualified Data.ByteString as B
968967import qualified Data.ByteString.Char8 as Char8
969−import qualified Data.HashMap.Strict as H
968+import qualified Data.Map.Strict as Map
970969import qualified Data.Text.Encoding as Enc
971970import qualified Data.Time as Time
972971
@@ -1006,7 +1005,7 @@ main = do
10061005 let
10071006 formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
10081007 "'", v, "'"]
1009− formOptions = B.intercalate " " $ map formFn $ H.toList formData
1008+ formOptions = B.intercalate " " $ map formFn $ Map.toList formData
10101009
10111010
10121011 return $ B.intercalate " " $
examples/AssumeRole.hsdeleted−47
@@ -1,47 +0,0 @@
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+9−6
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,17 +16,20 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Control.Monad.IO.Class (liftIO)
22−import Network.Minio
23−import Prelude
23+import Control.Monad.IO.Class (liftIO)
24+import Prelude
2425
2526-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
27+-- https://play.min.io:9000. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
32+
3033main :: IO ()
3134main = do
3235 let bucket = "missingbucket"
@@ -36,5 +39,5 @@ main = do
3639 liftIO $ putStrLn $ "Does " ++ show bucket ++ " exist? - " ++ show foundBucket
3740
3841 case res1 of
39− Left e -> putStrLn $ "bucketExists failed." ++ show e
42+ Left e -> putStrLn $ "bucketExists failed." ++ show e
4043 Right () -> return ()
examples/CopyObject.hs+17−16
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,40 +16,41 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Network.Minio
22−import UnliftIO.Exception (catch, throwIO)
23+import Control.Monad.Catch (catchIf)
24+import Prelude
2325
2426-- | The following example uses minio's play server at
25−-- https://play.min.io. The endpoint and associated
27+-- https://play.min.io:9000. The endpoint and associated
2628-- credentials are provided via the libary constant,
2729--
2830-- > minioPlayCI :: ConnectInfo
31+--
32+
33+ignoreMinioErr :: ServiceErr -> Minio ()
34+ignoreMinioErr = return . const ()
35+
2936main :: IO ()
3037main = do
31− let bucket = "test"
38+ let
39+ bucket = "test"
3240 object = "obj"
3341 objectCopy = "obj-copy"
3442 localFile = "/etc/lsb-release"
3543
3644 res1 <- runMinio minioPlayCI $ do
3745 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
38− catch
39− (makeBucket bucket Nothing)
40− ( \e -> case e of
41− BucketAlreadyOwnedByYou -> return ()
42− _ -> throwIO e
43− )
46+ catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
4447
4548 -- 2. Upload a file to bucket/object.
46− fPutObject bucket object localFile defaultPutObjectOptions
49+ fPutObject bucket object localFile
4750
4851 -- 3. Copy bucket/object to bucket/objectCopy.
49− copyObject
50− defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
51− defaultSourceInfo {srcBucket = bucket, srcObject = object}
52+ copyObject def {dstBucket = bucket, dstObject = objectCopy} def { srcBucket = bucket , srcObject = object }
5253
5354 case res1 of
54− Left e -> putStrLn $ "copyObject failed." ++ show e
55+ Left e -> putStrLn $ "copyObject failed." ++ show e
5556 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+28−28
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
2+-- stack --resolver lts-11.1 runghc --package minio-hs --package optparse-applicative --package filepath
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,39 +16,39 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−{-# LANGUAGE OverloadedStrings #-}
19+
20+
21+{-# LANGUAGE OverloadedStrings #-}
2022{-# LANGUAGE ScopedTypeVariables #-}
23+import Network.Minio
2124
22−import Data.Text (pack)
23−import Network.Minio
24−import Options.Applicative
25−import System.FilePath.Posix
26−import UnliftIO (throwIO, try)
27−import Prelude
25+import Data.Monoid ((<>))
26+import Data.Text (pack)
27+import Options.Applicative
28+import System.FilePath.Posix
29+import UnliftIO (throwIO, try)
30+
31+import Prelude
2832
2933-- | The following example uses minio's play server at
30−-- https://play.min.io. The endpoint and associated
34+-- https://play.min.io:9000. The endpoint and associated
3135-- credentials are provided via the libary constant,
3236--
3337-- > minioPlayCI :: ConnectInfo
38+--
3439
3540-- optparse-applicative package based command-line parsing.
3641fileNameArgs :: Parser FilePath
37−fileNameArgs =
38− strArgument
39− ( metavar "FILENAME"
40− <> help "Name of file to upload to AWS S3 or a MinIO server"
41− )
42+fileNameArgs = strArgument
43+ (metavar "FILENAME"
44+ <> help "Name of file to upload to AWS S3 or a MinIO server")
4245
43−cmdParser :: ParserInfo FilePath
44−cmdParser =
45− info
46− (helper <*> fileNameArgs)
47− ( fullDesc
48− <> progDesc "FileUploader"
49− <> header
50− "FileUploader - a simple file-uploader program using minio-hs"
51− )
46+cmdParser = info
47+ (helper <*> fileNameArgs)
48+ (fullDesc
49+ <> progDesc "FileUploader"
50+ <> header
51+ "FileUploader - a simple file-uploader program using minio-hs")
5252
5353main :: IO ()
5454main = do
@@ -62,13 +62,13 @@ main = do
6262 -- Make a bucket; catch bucket already exists exception if thrown.
6363 bErr <- try $ makeBucket bucket Nothing
6464 case bErr of
65− Left BucketAlreadyOwnedByYou -> return ()
66− Left e -> throwIO e
67− Right _ -> return ()
65+ Left (MErrService BucketAlreadyOwnedByYou) -> return ()
66+ Left e -> throwIO e
67+ Right _ -> return ()
6868
6969 -- Upload filepath to bucket; object is derived from filepath.
70− fPutObject bucket object filepath defaultPutObjectOptions
70+ fPutObject bucket object filepath def
7171
7272 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)
7474 Right () -> putStrLn "file upload succeeded."
examples/GetConfig.hs+7−7
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio
28− minioPlayCI
28+ res <- runMinio def $
2929 getConfig
3030 print res
examples/GetObject.hs+14−9
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,26 +16,31 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import qualified Data.Conduit as C
23+import qualified Data.Conduit as C
2224import qualified Data.Conduit.Binary as CB
23−import Network.Minio
24−import Prelude
25+
26+import Prelude
2527
2628-- | The following example uses minio's play server at
27−-- https://play.min.io. The endpoint and associated
29+-- https://play.min.io:9000. The endpoint and associated
2830-- credentials are provided via the libary constant,
2931--
3032-- > minioPlayCI :: ConnectInfo
33+--
34+
3135main :: IO ()
3236main = do
33− let bucket = "my-bucket"
37+ let
38+ bucket = "my-bucket"
3439 object = "my-object"
3540 res <- runMinio minioPlayCI $ do
36− src <- getObject bucket object defaultGetObjectOptions
37− C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
41+ src <- getObject bucket object def
42+ C.connect src $ CB.sinkFileCautious "/tmp/my-object"
3843
3944 case res of
40− Left e -> putStrLn $ "getObject failed." ++ show e
45+ Left e -> putStrLn $ "getObject failed." ++ (show e)
4146 Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs+13−10
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,25 +16,28 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.S3API
2023
21−import Network.Minio
22−import Network.Minio.S3API
23−import Prelude
24+import Prelude
2425
2526-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
27+-- https://play.min.io:9000. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
32+
3033main :: IO ()
3134main = do
32− let bucket = "test"
35+ let
36+ bucket = "test"
3337 object = "passwd"
34− res <-
35− runMinio minioPlayCI $
36− headObject bucket object []
38+ res <- runMinio minioPlayCI $
39+ headObject bucket object
3740
3841 case res of
39− Left e -> putStrLn $ "headObject failed." ++ show e
42+ Left e -> putStrLn $ "headObject failed." ++ show e
4043 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
examples/Heal.hs+10−13
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,21 +17,18 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <- runMinio minioPlayCI $
28+ res <- runMinio def $
2729 do
28− hsr <-
29− startHeal
30− Nothing
31− Nothing
32− HealOpts
33− { hoRecursive = True,
34− hoDryRun = False
35− }
30+ hsr <- startHeal Nothing Nothing HealOpts { hoRecursive = True
31+ , hoDryRun = False
32+ }
3633 getHealStatus Nothing Nothing (hsrClientToken hsr)
3734 print res
examples/ListBuckets.hs+7−5
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,17 +16,19 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Control.Monad.IO.Class (liftIO)
22−import Network.Minio
23−import Prelude
23+import Control.Monad.IO.Class (liftIO)
24+import Prelude
2425
2526-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
27+-- https://play.min.io:9000. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
3032
3133-- This example list buckets that belongs to the user and returns
3234-- region of the first bucket returned.
examples/ListIncompleteUploads.hs+21−19
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,36 +16,38 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Conduit
22−import Network.Minio
23−import Prelude
23+import Conduit
24+import Prelude
2425
2526-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
27+-- https://play.min.io:9000. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
32+
3033main :: IO ()
3134main = do
32− let bucket = "test"
35+ let
36+ bucket = "test"
3337
3438 -- Performs a recursive listing of incomplete uploads under bucket "test"
3539 -- on a local minio server.
36− res <-
37− runMinio minioPlayCI $
38− runConduit $
39− listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
40+ res <- runMinio minioPlayCI $
41+ runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
4042 print res
4143
42−{-
43− Following is the output of the above program on a local MinIO server.
44+ {-
45+ Following is the output of the above program on a local MinIO server.
4446
45− Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
46− , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
47− , uiInitTime = 2017-03-01 10:16:25.698 UTC
48− , uiSize = 17731794
49− }
50− ]
51−-}
47+ Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
48+ , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
49+ , uiInitTime = 2017-03-01 10:16:25.698 UTC
50+ , uiSize = 17731794
51+ }
52+ ]
53+ -}
examples/ListObjects.hs+17−15
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,31 +16,33 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+
23+import Conduit
24+import Prelude
2025
21−import Conduit
22−import Network.Minio
23−import Prelude
2426
2527-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
28+-- https://play.min.io:9000. The endpoint and associated
2729-- credentials are provided via the libary constant,
2830--
2931-- > minioPlayCI :: ConnectInfo
32+--
33+
3034main :: IO ()
3135main = do
32− let bucket = "test"
36+ let
37+ bucket = "test"
3338
3439 -- Performs a recursive listing of all objects under bucket "test"
3540 -- on play.min.io.
36− res <-
37− runMinio minioPlayCI $
38− runConduit $
39− listObjects bucket Nothing True .| mapM_C (liftIO . print)
41+ res <- runMinio minioPlayCI $
42+ runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
4043 print res
44+ {-
45+ Following is the output of the above program on a local MinIO server.
4146
42−{-
43− Following is the output of the above program on a local MinIO server.
44−
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}]
46−-}
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}]
48+ -}
examples/MakeBucket.hs+11−8
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,21 +16,24 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
20+
1921{-# LANGUAGE OverloadedStrings #-}
22+import Network.Minio
2023
21−import Network.Minio
22−import Prelude
24+import Prelude
2325
2426-- | The following example uses minio's play server at
25−-- https://play.min.io. The endpoint and associated
27+-- https://play.min.io:9000. The endpoint and associated
2628-- credentials are provided via the libary constant,
2729--
2830-- > minioPlayCI :: ConnectInfo
31+--
32+
2933main :: IO ()
3034main = do
3135 let bucket = "my-bucket"
32− res <-
33− runMinio minioPlayCI $
34− -- N B the region provided for makeBucket is optional.
35− makeBucket bucket (Just "us-east-1")
36+ res <- runMinio minioPlayCI $
37+ -- N B the region provided for makeBucket is optional.
38+ makeBucket bucket (Just "us-east-1")
3639 print res
examples/PresignedGetObject.hs+33−37
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,40 +16,42 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Control.Monad.IO.Class (liftIO)
22−import qualified Data.ByteString.Char8 as B
23−import Data.CaseInsensitive (original)
23+import Control.Monad.IO.Class (liftIO)
24+import qualified Data.ByteString.Char8 as B
25+import Data.CaseInsensitive (original)
2426import qualified Data.Conduit.Combinators as CC
25−import qualified Data.Text.Encoding as E
26−import Network.Minio
27+import qualified Data.Text.Encoding as E
2728
2829-- | The following example uses minio's play server at
29−-- https://play.min.io. The endpoint and associated
30+-- https://play.min.io:9000. The endpoint and associated
3031-- credentials are provided via the libary constant,
3132--
3233-- > minioPlayCI :: ConnectInfo
34+--
35+
3336main :: IO ()
3437main = do
35− let bucket = "my-bucket"
36− object = "my-object"
37− kb15 = 15 * 1024
38− -- Set query parameter to modify content disposition response
39− -- header
40− queryParam =
41− [ ( "response-content-disposition",
42− Just "attachment; filename=\"your-filename.txt\""
43− )
44− ]
38+ let
39+ bucket = "my-bucket"
40+ object = "my-object"
41+ kb15 = 15*1024
42+
43+ -- Set query parameter to modify content disposition response
44+ -- header
45+ queryParam = [("response-content-disposition",
46+ Just "attachment; filename=\"your-filename.txt\"")]
4547
4648 res <- runMinio minioPlayCI $ do
4749 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
48− putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
49− liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
50+ putObject bucket object (CC.repeat "a") (Just kb15) def
51+ liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
5052
5153 -- Extract Etag of uploaded object
52− oi <- statObject bucket object defaultGetObjectOptions
54+ oi <- statObject bucket object
5355 let etag = oiETag oi
5456
5557 -- Set header to add an if-match constraint - this makes sure
@@ -59,29 +61,23 @@ main = do
5961 -- Generate a URL with 7 days expiry time - note that the headers
6062 -- used above must be added to the request with the signed URL
6163 -- generated.
62− url <-
63− presignedGetObjectUrl
64− "my-bucket"
65− "my-object"
66− (7 * 24 * 3600)
67− queryParam
68− headers
64+ url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)
65+ queryParam headers
6966
7067 return (headers, etag, url)
7168
7269 case res of
7370 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
74− Right (headers, _, url) -> do
71+ Right (headers, etag, url) -> do
72+
7573 -- We generate a curl command to demonstrate usage of the signed
7674 -- URL.
77− let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
78− curlCmd =
79− B.intercalate " " $
80− ["curl --fail"]
81− ++ map hdrOpt headers
82− ++ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
75+ let
76+ hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
77+ curlCmd = B.intercalate " " $
78+ ["curl --fail"] ++ map hdrOpt headers ++
79+ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
8380
84− putStrLn $
85− "The following curl command would use the presigned "
86− ++ "URL to fetch the object and write it to \"/tmp/myfile\":"
81+ putStrLn $ "The following curl command would use the presigned " ++
82+ "URL to fetch the object and write it to \"/tmp/myfile\":"
8783 B.putStrLn curlCmd
examples/PresignedPostPolicy.hs+41−44
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,72 +16,69 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import qualified Data.ByteString as B
23+import qualified Data.ByteString as B
2224import qualified Data.ByteString.Char8 as Char8
23−import qualified Data.HashMap.Strict as H
24−import qualified Data.Text.Encoding as Enc
25−import qualified Data.Time as Time
26−import Network.Minio
25+import qualified Data.Map.Strict as Map
26+import qualified Data.Text.Encoding as Enc
27+import qualified Data.Time as Time
2728
2829-- | The following example uses minio's play server at
29−-- https://play.min.io. The endpoint and associated
30+-- https://play.min.io:9000. The endpoint and associated
3031-- credentials are provided via the libary constant,
3132--
3233-- > minioPlayCI :: ConnectInfo
34+--
35+
3336main :: IO ()
3437main = do
3538 now <- Time.getCurrentTime
36− let bucket = "my-bucket"
37− object = "photos/my-object"
38− -- set an expiration time of 10 days
39− expireTime = Time.addUTCTime (3600 * 24 * 10) now
40− -- create a policy with expiration time and conditions - since the
41− -- conditions are validated, newPostPolicy returns an Either value
42− policyE =
43− newPostPolicy
44− expireTime
45− [ -- set the object name condition
46− ppCondKey object,
47− -- set the bucket name condition
48− ppCondBucket bucket,
49− -- set the size range of object as 1B to 10MiB
50− ppCondContentLengthRange 1 (10 * 1024 * 1024),
51− -- set content type as jpg image
52− ppCondContentType "image/jpeg",
53− -- on success set the server response code to 200
54− ppCondSuccessActionStatus 200
55− ]
39+ let
40+ bucket = "my-bucket"
41+ object = "my-object"
42+
43+ -- set an expiration time of 10 days
44+ expireTime = Time.addUTCTime (3600 * 24 * 10) now
45+
46+ -- create a policy with expiration time and conditions - since the
47+ -- conditions are validated, newPostPolicy returns an Either value
48+ policyE = newPostPolicy expireTime
49+ [ -- set the object name condition
50+ ppCondKey "photos/my-object"
51+ -- set the bucket name condition
52+ , ppCondBucket "my-bucket"
53+ -- set the size range of object as 1B to 10MiB
54+ , ppCondContentLengthRange 1 (10*1024*1024)
55+ -- set content type as jpg image
56+ , ppCondContentType "image/jpeg"
57+ -- on success set the server response code to 200
58+ , ppCondSuccessActionStatus 200
59+ ]
5660
5761 case policyE of
58− Left err -> print err
62+ Left err -> putStrLn $ show err
5963 Right policy -> do
6064 res <- runMinio minioPlayCI $ do
6165 (url, formData) <- presignedPostPolicy policy
6266
6367 -- a curl command is output to demonstrate using the generated
6468 -- URL and form-data
65− let formFn (k, v) =
66− B.concat
67− [ "-F ",
68− Enc.encodeUtf8 k,
69− "=",
70− "'",
71− v,
72− "'"
73− ]
74− formOptions = B.intercalate " " $ map formFn $ H.toList formData
69+ let
70+ formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
71+ "'", v, "'"]
72+ formOptions = B.intercalate " " $ map formFn $ Map.toList formData
73+
7574
76− return $
77− B.intercalate
78− " "
79− ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
75+ return $ B.intercalate " " $
76+ ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
8077
8178 case res of
82− Left e -> putStrLn $ "post-policy error: " ++ show e
79+ Left e -> putStrLn $ "post-policy error: " ++ (show e)
8380 Right cmd -> do
84− putStrLn "Put a photo at /tmp/photo.jpg and run command:\n"
81+ putStrLn $ "Put a photo at /tmp/photo.jpg and run command:\n"
8582
8683 -- print the generated curl command
8784 Char8.putStrLn cmd
examples/PresignedPutObject.hs+23−22
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,43 +16,44 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
2123import qualified Data.ByteString.Char8 as B
22−import Data.CaseInsensitive (original)
23−import Network.Minio
24+import Data.CaseInsensitive (original)
2425
2526-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
27+-- https://play.min.io:9000. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
32+
3033main :: IO ()
3134main = do
32− let -- Use headers to set user-metadata - note that this header will
33− -- need to be set when the URL is used to make an upload.
34− headers =
35− [ ( "x-amz-meta-url-creator",
36− "minio-hs-presigned-put-example"
37− )
38− ]
35+ let
36+ -- 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.
38+ headers = [("x-amz-meta-url-creator",
39+ "minio-hs-presigned-put-example")]
3940 res <- runMinio minioPlayCI $ do
41+
4042 -- generate a URL with 7 days expiry time
41− presignedPutObjectUrl "my-bucket" "my-object" (7 * 24 * 3600) headers
43+ presignedPutObjectUrl "my-bucket" "my-object" (7*24*3600) headers
4244
4345 case res of
4446 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
4547 Right url -> do
48+
4649 -- We generate a curl command to demonstrate usage of the signed
4750 -- URL.
48− let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
49− curlCmd =
50− B.intercalate " " $
51− ["curl "]
52− ++ map hdrOpt headers
53− ++ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
54−
55− putStrLn $
56− "The following curl command would use the presigned "
57− ++ "URL to upload the file at \"/tmp/myfile\":"
51+ let
52+ hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
53+ curlCmd = B.intercalate " " $
54+ ["curl "] ++ map hdrOpt headers ++
55+ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
56+
57+ putStrLn $ "The following curl command would use the presigned " ++
58+ "URL to upload the file at \"/tmp/myfile\":"
5859 B.putStrLn curlCmd
examples/PutObject.hs+16−13
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,36 +16,39 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
2123import qualified Data.Conduit.Combinators as CC
22−import Network.Minio
23−import Prelude
24+
25+import Prelude
2426
2527-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
28+-- https://play.min.io:9000. The endpoint and associated
2729-- credentials are provided via the libary constant,
2830--
2931-- > minioPlayCI :: ConnectInfo
32+--
33+
3034main :: IO ()
3135main = do
32− let bucket = "test"
36+ let
37+ bucket = "test"
3338 object = "obj"
3439 localFile = "/etc/lsb-release"
3540 kb15 = 15 * 1024
3641
3742 -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
38− res1 <-
39− runMinio minioPlayCI $
40− putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
43+ res1 <- runMinio minioPlayCI $
44+ putObject bucket object (CC.repeat "a") (Just kb15) def
4145 case res1 of
42− Left e -> putStrLn $ "putObject failed." ++ show e
46+ Left e -> putStrLn $ "putObject failed." ++ show e
4347 Right () -> putStrLn "putObject succeeded."
4448
4549 -- Eg 2. Upload a file using fPutObject with default options.
46− res2 <-
47− runMinio minioPlayCI $
48− fPutObject bucket object localFile defaultPutObjectOptions
50+ res2 <- runMinio minioPlayCI $
51+ fPutObject bucket object localFile def
4952 case res2 of
50− Left e -> putStrLn $ "fPutObject failed." ++ show e
53+ Left e -> putStrLn $ "fPutObject failed." ++ show e
5154 Right () -> putStrLn "fPutObject succeeded."
examples/README.mddeleted−17
@@ -1,17 +0,0 @@
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+10−5
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,18 +16,23 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
20+
1921{-# LANGUAGE OverloadedStrings #-}
22+import Network.Minio
2023
21−import Network.Minio
22−import Prelude
24+import Prelude
2325
2426-- | The following example uses minio's play server at
25−-- https://play.min.io. The endpoint and associated
27+-- https://play.min.io:9000. The endpoint and associated
2628-- credentials are provided via the libary constant,
2729--
2830-- > minioPlayCI :: ConnectInfo
31+--
32+
2933main :: IO ()
3034main = do
31− let bucket = "my-bucket"
35+ let
36+ bucket = "my-bucket"
3237 res <- runMinio minioPlayCI $ removeBucket bucket
3338 print res
examples/RemoveIncompleteUpload.hs+12−9
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,24 +16,27 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Network.Minio
22−import Prelude
23+import Prelude
2324
2425-- | The following example uses minio's play server at
25−-- https://play.min.io. The endpoint and associated
26+-- https://play.min.io:9000. The endpoint and associated
2627-- credentials are provided via the libary constant,
2728--
2829-- > minioPlayCI :: ConnectInfo
30+--
31+
2932main :: IO ()
3033main = do
31− let bucket = "mybucket"
32− object = "myobject"
34+ let
35+ bucket = "mybucket"
36+ object = "myobject"
3337
34− res <-
35− runMinio minioPlayCI $
36− removeIncompleteUpload bucket object
38+ res <- runMinio minioPlayCI $
39+ removeIncompleteUpload bucket object
3740
3841 case res of
3942 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/RemoveObject.hs+9−8
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,19 +16,20 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
2021
21−import Network.Minio
22−import Prelude
22+import Network.Minio
23+import Prelude
2324
2425main :: IO ()
2526main = do
26− let bucket = "mybucket"
27− object = "myobject"
27+ let
28+ bucket = "mybucket"
29+ object = "myobject"
2830
29− res <-
30− runMinio minioPlayCI $
31− removeObject bucket object
31+ res <- runMinio minioPlayCI $
32+ removeObject bucket object
3233
3334 case res of
3435 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/SelectObject.hs+28−25
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-13.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2019 MinIO, Inc.
@@ -16,32 +16,35 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# 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
2026
21−import qualified Conduit as C
22−import Control.Monad (unless)
23−import Network.Minio
24−import Prelude
27+import Prelude
2528
2629main :: IO ()
2730main = do
28− let bucket = "selectbucket"
29− object = "1.csv"
30− content =
31− "Name,Place,Temperature\n"
32− <> "James,San Jose,76\n"
33− <> "Alicia,San Leandro,88\n"
34− <> "Mark,San Carlos,90\n"
35−
36− res <- runMinio minioPlayCI $ do
37− exists <- bucketExists bucket
38− unless exists $
39− makeBucket bucket Nothing
40−
41− C.liftIO $ putStrLn "Uploading csv object"
42− putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
43−
44− let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
45− res <- selectObjectContent bucket object sr
46− C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
47− print res
31+ let bucket = "selectbucket"
32+ object = "1.csv"
33+ content = "Name,Place,Temperature\n"
34+ <> "James,San Jose,76\n"
35+ <> "Alicia,San Leandro,88\n"
36+ <> "Mark,San Carlos,90\n"
37+
38+ res <- runMinio minioPlayCI $ do
39+
40+ exists <- bucketExists bucket
41+ when (not exists) $
42+ makeBucket bucket Nothing
43+
44+ C.liftIO $ putStrLn "Uploading csv object"
45+ putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
46+
47+ let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
48+ res <- selectObjectContent bucket object sr
49+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
50+ print res
examples/ServerInfo.hs+8−8
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio
28− minioPlayCI
29− getServerInfo
28+ res <- runMinio def $
29+ getServerInfo
3030 print res
examples/ServiceSendRestart.hs+8−7
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,13 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio minioPlayCI $
28− serviceSendAction ServiceActionRestart
28+ res <- runMinio def $
29+ serviceSendAction ServiceActionRestart
2930 print res
examples/ServiceSendStop.hs+8−7
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,13 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio minioPlayCI $
28− serviceSendAction ServiceActionStop
28+ res <- runMinio def $
29+ serviceSendAction ServiceActionStop
2930 print res
examples/ServiceStatus.hs+8−8
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio
28− minioPlayCI
29− serviceStatus
28+ res <- runMinio def $
29+ serviceStatus
3030 print res
examples/SetConfig.hs+6−5
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-11.1 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,15 +16,16 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
2023
21−import Network.Minio
22−import Network.Minio.AdminAPI
23−import Prelude
24+import Prelude
2425
2526main :: IO ()
2627main = do
27− res <- runMinio minioPlayCI $
28+ res <- runMinio def $
2829 do
2930 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\"}}}}"
3031 setConfig config
minio-hs.cabal+170−249
@@ -1,6 +1,5 @@
1−cabal-version: 2.4
21name: minio-hs
3−version: 1.7.0
2+version: 1.4.0
43synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
54 storage.
65description: The MinIO Haskell client library provides simple APIs to
@@ -14,70 +13,22 @@ maintainer: dev@min.io
1413category: Network, AWS, Object Storage
1514build-type: Simple
1615stability: Experimental
17−extra-doc-files:
16+extra-source-files:
1817 CHANGELOG.md
1918 CONTRIBUTING.md
2019 docs/API.md
21− README.md
22−extra-source-files:
2320 examples/*.hs
21+ README.md
2422 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
31−
32−source-repository head
33− type: git
34− location: https://github.com/minio/minio-hs.git
3523
36−Flag dev
37− Description: Build package in development mode
38− Default: False
39− Manual: True
24+cabal-version: >=1.10
4025
41−common base-settings
26+library
27+ hs-source-dirs: src
4228 ghc-options: -Wall
43− -Wcompat
44− -Widentities
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−
29+ exposed-modules: Network.Minio
30+ , Network.Minio.AdminAPI
31+ , Network.Minio.S3API
8132 other-modules: Lib.Prelude
8233 , Network.Minio.API
8334 , Network.Minio.APICommon
@@ -95,31 +46,23 @@ common base-settings
9546 , Network.Minio.Utils
9647 , Network.Minio.XmlGenerator
9748 , Network.Minio.XmlParser
98− , Network.Minio.XmlCommon
9949 , 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−
10850 build-depends: base >= 4.7 && < 5
109− , relude >= 0.7 && < 2
110− , aeson >= 1.2 && < 3
51+ , protolude >= 0.2 && < 0.3
52+ , aeson >= 1.2
11153 , base64-bytestring >= 1.0
11254 , binary >= 0.8.5.0
11355 , bytestring >= 0.10
11456 , case-insensitive >= 1.2
11557 , conduit >= 1.3
11658 , conduit-extra >= 1.3
117− , crypton-connection
59+ , connection
60+ , containers >= 0.5
11861 , cryptonite >= 0.25
11962 , cryptonite-conduit >= 0.2
120− , data-default-class
12163 , digest >= 0.0.1
12264 , directory
65+ , exceptions
12366 , filepath >= 1.4
12467 , http-client >= 0.5
12568 , http-client-tls
@@ -127,227 +70,205 @@ common base-settings
12770 , http-types >= 0.12
12871 , ini
12972 , memory >= 0.14
130− , network-uri
73+ , raw-strings-qq >= 1
13174 , resourcet >= 1.2
13275 , retry
13376 , text >= 1.2
134− , time >= 1.9
135− , time-units ^>= 1.0.0
77+ , time >= 1.8
13678 , transformers >= 0.5
137− , unliftio >= 0.2 && < 0.3
138− , unliftio-core >= 0.2 && < 0.3
79+ , unliftio >= 0.2
80+ , unliftio-core >= 0.1
13981 , unordered-containers >= 0.2
14082 , xml-conduit >= 1.8
141−
142−library
143− import: base-settings
144− hs-source-dirs: src
145− exposed-modules: Network.Minio
146− , Network.Minio.AdminAPI
147− , Network.Minio.S3API
83+ default-language: Haskell2010
84+ default-extensions: BangPatterns
85+ , FlexibleContexts
86+ , FlexibleInstances
87+ , MultiParamTypeClasses
88+ , MultiWayIf
89+ , NoImplicitPrelude
90+ , OverloadedStrings
91+ , RankNTypes
92+ , ScopedTypeVariables
93+ , TypeFamilies
94+ , TupleSections
14895
14996Flag live-test
150− Description: Build the test suite that runs against a live MinIO server
151− Default: False
97+ Default: True
15298 Manual: True
15399
154100test-suite minio-hs-live-server-test
155− import: base-settings
156101 type: exitcode-stdio-1.0
157102 hs-source-dirs: test, src
158103 main-is: LiveServer.hs
159104 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
160− other-modules: Network.Minio
161− , Network.Minio.S3API
162− , Network.Minio.AdminAPI
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
119+ , Network.Minio.API
163120 , Network.Minio.API.Test
121+ , Network.Minio.APICommon
122+ , Network.Minio.AdminAPI
123+ , Network.Minio.CopyObject
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
164130 , 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
165137 , Network.Minio.TestHelpers
138+ , Network.Minio.Utils
166139 , Network.Minio.Utils.Test
140+ , Network.Minio.XmlGenerator
167141 , Network.Minio.XmlGenerator.Test
142+ , Network.Minio.XmlParser
168143 , Network.Minio.XmlParser.Test
169− , Network.Minio.Credentials
170− build-depends: minio-hs
171− , raw-strings-qq
144+ build-depends: base >= 4.7 && < 5
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
170+ , resourcet
171+ , retry
172172 , tasty
173173 , tasty-hunit
174174 , tasty-quickcheck
175175 , tasty-smallcheck
176− , QuickCheck
176+ , temporary
177+ , text
178+ , time
179+ , transformers
180+ , unliftio
181+ , unliftio-core
182+ , unordered-containers
183+ , xml-conduit
177184 if !flag(live-test)
178185 buildable: False
179186
180187test-suite minio-hs-test
181− import: base-settings
182188 type: exitcode-stdio-1.0
183189 hs-source-dirs: test, src
184190 main-is: Spec.hs
185− build-depends: minio-hs
186− , raw-strings-qq
191+ build-depends: base >= 4.7 && < 5
192+ , minio-hs
193+ , protolude >= 0.1.6
187194 , 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
188219 , tasty
189220 , tasty-hunit
190221 , tasty-quickcheck
191222 , tasty-smallcheck
223+ , temporary
224+ , text
225+ , time
226+ , transformers
227+ , unliftio
228+ , unliftio-core
229+ , unordered-containers
230+ , xml-conduit
192231 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
193244 other-modules: Lib.Prelude
194245 , Network.Minio
195− , Network.Minio.S3API
196− , Network.Minio.AdminAPI
197− , Network.Minio.TestHelpers
246+ , Network.Minio.API
198247 , 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
199257 , Network.Minio.JsonParser.Test
258+ , Network.Minio.ListOps
259+ , Network.Minio.PresignedOperations
260+ , Network.Minio.PutObject
261+ , Network.Minio.S3API
262+ , Network.Minio.SelectAPI
263+ , Network.Minio.Sign.V4
264+ , Network.Minio.TestHelpers
265+ , Network.Minio.Utils
200266 , Network.Minio.Utils.Test
267+ , Network.Minio.XmlGenerator
201268 , Network.Minio.XmlGenerator.Test
269+ , Network.Minio.XmlParser
202270 , Network.Minio.XmlParser.Test
203− , Network.Minio.Credentials
204−
205−Flag examples
206− Description: Build the examples
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
244271
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
272+source-repository head
273+ type: git
274+ location: https://github.com/minio/minio-hs
src/Lib/Prelude.hs+11−33
@@ -15,41 +15,19 @@
1515--
1616
1717module Lib.Prelude
18− ( module Exports,
19− both,
20− showBS,
21− toStrictBS,
22− fromStrictBS,
23− lastMay,
24− )
25−where
18+ ( module Exports
19+ , both
20+ ) where
2621
27−import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
28−import qualified Data.ByteString.Lazy as LB
29−import Data.Time as Exports
30− ( UTCTime (..),
31− diffUTCTime,
32− )
33−import UnliftIO as Exports
34− ( Handler,
35− catch,
36− catches,
37− throwIO,
38− try,
39− )
22+import Protolude as Exports hiding (catch, catches,
23+ throwIO, try)
24+
25+import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
26+import Data.Time as Exports (UTCTime (..),
27+ diffUTCTime)
28+import UnliftIO as Exports (catch, catches, throwIO,
29+ try)
4030
4131-- | Apply a function on both elements of a pair
4232both :: (a -> b) -> (a, a) -> (b, b)
4333both 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+218−254
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -16,237 +16,219 @@
1616
1717-- |
1818-- Module: Network.Minio
19−-- Copyright: (c) 2017-2023 MinIO Dev Team
19+-- Copyright: (c) 2017-2019 MinIO Dev Team
2020-- License: Apache 2.0
2121-- Maintainer: MinIO Dev Team <dev@min.io>
2222--
23−-- Types and functions to conveniently access S3 compatible object
24−-- storage servers like MinIO.
23+-- Types and functions to access S3 compatible object storage servers
24+-- like MinIO.
25+
2526module Network.Minio
26− ( -- * Credentials
27− CredentialValue (..),
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,
38− fromAWSConfigFile,
39− fromAWSEnv,
40− fromMinioEnv,
41− findFirst,
42−
43− -- * Connecting to object storage
44− ConnectInfo,
45− setRegion,
46− setCreds,
47− setCredsFrom,
48− isConnectInfoSecure,
49− disableTLSCertValidation,
50− MinioConn,
51− mkMinioConn,
52−
53− -- ** Connection helpers
54−
55− -- | These are helpers to construct 'ConnectInfo' values for common
56− -- cases.
57− minioPlayCI,
58− awsCI,
59− gcsCI,
60−
61− -- ** STS Credential types
62− STSAssumeRole (..),
63− STSAssumeRoleOptions (..),
64− defaultSTSAssumeRoleOptions,
65− requestSTSCredential,
66− setSTSCredential,
67− ExpiryTime (..),
68− STSCredentialProvider,
69−
70− -- * Minio Monad
71−
72− ----------------
73−
74− -- | The Minio Monad provides connection-reuse, bucket-location
75− -- caching, resource management and simpler error handling
76− -- functionality. All actions on object storage are performed within
77− -- this Monad.
78− Minio,
79− runMinioWith,
80− runMinio,
81− runMinioResWith,
82− runMinioRes,
83−
84− -- * Bucket Operations
85−
86− -- ** Creation, removal and querying
87− Bucket,
88− makeBucket,
89− removeBucket,
90− bucketExists,
91− Region,
92− getLocation,
93−
94− -- ** Listing buckets
95− BucketInfo (..),
96− listBuckets,
97−
98− -- ** Listing objects
99− listObjects,
100− listObjectsV1,
101− ListItem (..),
102− ObjectInfo,
103− oiObject,
104− oiModTime,
105− oiETag,
106− oiSize,
107− oiUserMetadata,
108− oiMetadata,
109−
110− -- ** Listing incomplete uploads
111− listIncompleteUploads,
112− UploadId,
113− UploadInfo (..),
114− listIncompleteParts,
115− ObjectPartInfo (..),
116−
117− -- ** Bucket Notifications
118− getBucketNotification,
119− putBucketNotification,
120− removeAllBucketNotification,
121− Notification (..),
122− defaultNotification,
123− NotificationConfig (..),
124− Arn,
125− Event (..),
126− Filter (..),
127− defaultFilter,
128− FilterKey (..),
129− defaultFilterKey,
130− FilterRules (..),
131− defaultFilterRules,
132− FilterRule (..),
133−
134− -- * Object Operations
135− Object,
136−
137− -- ** File-based operations
138− fGetObject,
139− fPutObject,
140−
141− -- ** Conduit-based streaming operations
142− putObject,
143− PutObjectOptions,
144− defaultPutObjectOptions,
145− pooContentType,
146− pooContentEncoding,
147− pooContentDisposition,
148− pooContentLanguage,
149− pooCacheControl,
150− pooStorageClass,
151− pooUserMetadata,
152− pooNumThreads,
153− pooSSE,
154− getObject,
155− GetObjectOptions,
156− defaultGetObjectOptions,
157− gooRange,
158− gooIfMatch,
159− gooIfNoneMatch,
160− gooIfModifiedSince,
161− gooIfUnmodifiedSince,
162− gooSSECKey,
163− GetObjectResponse,
164− gorObjectInfo,
165− gorObjectStream,
166−
167− -- ** Server-side object copying
168− copyObject,
169− SourceInfo,
170− defaultSourceInfo,
171− srcBucket,
172− srcObject,
173− srcRange,
174− srcIfMatch,
175− srcIfNoneMatch,
176− srcIfModifiedSince,
177− srcIfUnmodifiedSince,
178− DestinationInfo,
179− defaultDestinationInfo,
180− dstBucket,
181− dstObject,
182−
183− -- ** Querying object info
184− statObject,
185−
186− -- ** Object removal operations
187− removeObject,
188− removeIncompleteUpload,
189−
190− -- ** Select Object Content with SQL
191− module Network.Minio.SelectAPI,
192−
193− -- * Server-Side Encryption Helpers
194− mkSSECKey,
195− SSECKey,
196− SSE (..),
197−
198− -- * Presigned Operations
199− presignedPutObjectUrl,
200− presignedGetObjectUrl,
201− presignedHeadObjectUrl,
202− UrlExpiry,
203−
204− -- ** POST (browser) upload helpers
205−
206− -- | Please see
207− -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
208− -- for detailed information.
209− newPostPolicy,
210− presignedPostPolicy,
211− showPostPolicy,
212− PostPolicy,
213− PostPolicyError (..),
214−
215− -- *** Post Policy condition helpers
216− PostPolicyCondition,
217− ppCondBucket,
218− ppCondContentLengthRange,
219− ppCondContentType,
220− ppCondKey,
221− ppCondKeyStartsWith,
222− ppCondSuccessActionStatus,
223−
224− -- * Error handling
225−
226− -- | Data types representing various errors that may occur while
227− -- working with an object storage service.
228− MinioErr (..),
229− MErrV (..),
230− ServiceErr (..),
231− )
232−where
27+ (
28+ -- * Credentials
29+ Credentials (..)
30+ , Provider
31+ , fromAWSConfigFile
32+ , fromAWSEnv
33+ , fromMinioEnv
34+ , findFirst
35+
36+ -- * Connecting to object storage
37+ ---------------------------------
38+ , ConnectInfo
39+ , setRegion
40+ , setCreds
41+ , setCredsFrom
42+ , isConnectInfoSecure
43+ , disableTLSCertValidation
44+ , MinioConn
45+ , mkMinioConn
46+
47+ -- ** Connection helpers
48+ ------------------------
49+ , minioPlayCI
50+ , awsCI
51+ , gcsCI
52+
53+
54+ -- * Minio Monad
55+ ----------------
56+ -- | The Minio Monad provides connection-reuse, bucket-location
57+ -- caching, resource management and simpler error handling
58+ -- functionality. All actions on object storage are performed within
59+ -- this Monad.
60+
61+ , Minio
62+ , runMinioWith
63+ , runMinio
64+
65+
66+ -- * Bucket Operations
67+ ----------------------
68+
69+ -- ** Creation, removal and querying
70+ , Bucket
71+ , makeBucket
72+ , removeBucket
73+ , bucketExists
74+ , Region
75+ , getLocation
76+
77+ -- ** Listing
78+ , BucketInfo(..)
79+ , listBuckets
80+
81+ -- ** Object info type represents object metadata information.
82+ , ObjectInfo
83+ , oiObject
84+ , oiModTime
85+ , oiETag
86+ , oiSize
87+ , oiMetadata
88+
89+ , ListItem(..)
90+ , listObjects
91+ , listObjectsV1
92+
93+ , UploadId
94+ , UploadInfo(..)
95+ , listIncompleteUploads
96+ , ObjectPartInfo(..)
97+ , listIncompleteParts
98+
99+ -- ** Bucket Notifications
100+ , Notification(..)
101+ , defaultNotification
102+ , NotificationConfig(..)
103+ , Arn
104+ , Event(..)
105+ , Filter(..)
106+ , defaultFilter
107+ , FilterKey(..)
108+ , defaultFilterKey
109+ , FilterRules(..)
110+ , defaultFilterRules
111+ , FilterRule(..)
112+ , getBucketNotification
113+ , putBucketNotification
114+ , removeAllBucketNotification
115+
116+ -- * Object Operations
117+ ----------------------
118+ , Object
119+
120+ -- ** File operations
121+ , fGetObject
122+ , fPutObject
123+
124+ -- ** Conduit-based streaming operations
125+ , putObject
126+ , PutObjectOptions
127+ , defaultPutObjectOptions
128+ , pooContentType
129+ , pooContentEncoding
130+ , pooContentDisposition
131+ , pooContentLanguage
132+ , pooCacheControl
133+ , pooStorageClass
134+ , pooUserMetadata
135+ , pooNumThreads
136+ , pooSSE
137+
138+ , getObject
139+ , GetObjectOptions
140+ , defaultGetObjectOptions
141+ , gooRange
142+ , gooIfMatch
143+ , gooIfNoneMatch
144+ , gooIfModifiedSince
145+ , gooIfUnmodifiedSince
146+ , gooSSECKey
147+
148+ -- ** Server-side copying
149+ , copyObject
150+ , SourceInfo
151+ , defaultSourceInfo
152+ , srcBucket
153+ , srcObject
154+ , srcRange
155+ , srcIfMatch
156+ , srcIfNoneMatch
157+ , srcIfModifiedSince
158+ , srcIfUnmodifiedSince
159+ , DestinationInfo
160+ , defaultDestinationInfo
161+ , dstBucket
162+ , dstObject
163+
164+ -- ** Querying
165+ , statObject
166+
167+ -- ** Object removal functions
168+ , removeObject
169+ , removeIncompleteUpload
170+
171+ -- ** Select Object Content with SQL
172+ , module Network.Minio.SelectAPI
173+
174+ -- * Server-Size Encryption Helpers
175+ -----------------------------------
176+ , SSECKey
177+ , mkSSECKey
178+ , SSE(..)
179+
180+
181+ -- * Presigned Operations
182+ -------------------------
183+ , UrlExpiry
184+ , presignedPutObjectUrl
185+ , presignedGetObjectUrl
186+ , presignedHeadObjectUrl
187+
188+ -- ** Utilities for POST (browser) uploads
189+ , PostPolicy
190+ , PostPolicyError(..)
191+ , newPostPolicy
192+ , presignedPostPolicy
193+ , showPostPolicy
194+
195+ -- *** Utilities to specify Post Policy conditions
196+ , PostPolicyCondition
197+ , ppCondBucket
198+ , ppCondContentLengthRange
199+ , ppCondContentType
200+ , ppCondKey
201+ , ppCondKeyStartsWith
202+ , ppCondSuccessActionStatus
203+
204+ -- * Error handling
205+ -----------------------
206+ -- | Data types representing various errors that may occur while working
207+ -- with an object storage service.
208+ , MinioErr(..)
209+ , MErrV(..)
210+ , ServiceErr(..)
211+
212+) where
233213
234214{-
235215This module exports the high-level MinIO API for object storage.
236216-}
237217
238−import qualified Data.Conduit as C
239−import qualified Data.Conduit.Binary as CB
218+import qualified Data.Conduit as C
219+import qualified Data.Conduit.Binary as CB
240220import qualified Data.Conduit.Combinators as CC
241−import Network.Minio.API
242−import Network.Minio.CopyObject
243−import Network.Minio.Credentials
244−import Network.Minio.Data
245−import Network.Minio.Errors
246−import Network.Minio.ListOps
247−import Network.Minio.PutObject
248−import Network.Minio.S3API
249−import Network.Minio.SelectAPI
221+
222+import Lib.Prelude
223+
224+import Network.Minio.CopyObject
225+import Network.Minio.Data
226+import Network.Minio.Errors
227+import Network.Minio.ListOps
228+import Network.Minio.PutObject
229+import Network.Minio.S3API
230+import Network.Minio.SelectAPI
231+import Network.Minio.Utils
250232
251233-- | Lists buckets.
252234listBuckets :: Minio [BucketInfo]
@@ -258,15 +240,11 @@ listBuckets = getService
258240fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
259241fGetObject bucket object fp opts = do
260242 src <- getObject bucket object opts
261− C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
243+ C.connect src $ CB.sinkFileCautious fp
262244
263245-- | Upload the given file to the given object.
264−fPutObject ::
265− Bucket ->
266− Object ->
267− FilePath ->
268− PutObjectOptions ->
269− Minio ()
246+fPutObject :: Bucket -> Object -> FilePath
247+ -> PutObjectOptions -> Minio ()
270248fPutObject bucket object f opts =
271249 void $ putObjectInternal bucket object opts $ ODFile f Nothing
272250
@@ -274,13 +252,8 @@ fPutObject bucket object f opts =
274252-- known; this helps the library select optimal part sizes to perform
275253-- a multipart upload. If not specified, it is assumed that the object
276254-- can be potentially 5TiB and selects multipart sizes appropriately.
277−putObject ::
278− Bucket ->
279− Object ->
280− C.ConduitM () ByteString Minio () ->
281− Maybe Int64 ->
282− PutObjectOptions ->
283− Minio ()
255+putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio ()
256+ -> Maybe Int64 -> PutObjectOptions -> Minio ()
284257putObject bucket object src sizeMay opts =
285258 void $ putObjectInternal bucket object opts $ ODStream src sizeMay
286259
@@ -290,25 +263,18 @@ putObject bucket object src sizeMay opts =
290263-- copy operation if the new object is to be greater than 5GiB in
291264-- size.
292265copyObject :: DestinationInfo -> SourceInfo -> Minio ()
293−copyObject dstInfo srcInfo =
294− void $
295− copyObjectInternal
296− (dstBucket dstInfo)
297− (dstObject dstInfo)
298− srcInfo
266+copyObject dstInfo srcInfo = void $ copyObjectInternal (dstBucket dstInfo)
267+ (dstObject dstInfo) srcInfo
299268
300269-- | Remove an object from the object store.
301270removeObject :: Bucket -> Object -> Minio ()
302271removeObject = deleteObject
303272
304−-- | Get an object from the object store.
305−getObject ::
306− Bucket ->
307− Object ->
308− GetObjectOptions ->
309− Minio GetObjectResponse
310−getObject bucket object opts =
311− getObject' bucket object [] $ gooToHeaders opts
273+-- | Get an object from the object store as a resumable source (conduit).
274+getObject :: Bucket -> Object -> GetObjectOptions
275+ -> Minio (C.ConduitM () ByteString Minio ())
276+getObject bucket object opts = snd <$> getObject' bucket object []
277+ (gooToHeaders opts)
312278
313279-- | Get an object's metadata from the object store. It accepts the
314280-- same options as GetObject.
@@ -338,8 +304,6 @@ bucketExists = headBucket
338304-- | Removes an ongoing multipart upload of an object.
339305removeIncompleteUpload :: Bucket -> Object -> Minio ()
340306removeIncompleteUpload bucket object = do
341− uploads <-
342− C.runConduit $
343− listIncompleteUploads bucket (Just object) False
344− C..| CC.sinkList
307+ uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
308+ C..| CC.sinkList
345309 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs+159−259
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,260 +15,169 @@
1515--
1616
1717module Network.Minio.API
18− ( connect,
19− S3ReqInfo (..),
20− runMinio,
21− executeRequest,
22− buildRequest,
23− mkStreamRequest,
24− getLocation,
25− isValidBucketName,
26− checkBucketNameValidity,
27− isValidObjectName,
28− checkObjectNameValidity,
29− requestSTSCredential,
30− )
31−where
32−
33−import Control.Retry
34− ( fullJitterBackoff,
35− limitRetriesByCumulativeDelay,
36− retrying,
37− )
38−import qualified Data.ByteString as B
39−import qualified Data.Char as C
40−import qualified Data.Conduit as C
41−import qualified Data.HashMap.Strict as H
42−import qualified Data.Text as T
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
47−import Network.HTTP.Conduit (Response)
48−import qualified Network.HTTP.Conduit as NC
49−import Network.HTTP.Types (simpleQueryToQuery)
50−import qualified Network.HTTP.Types as HT
51−import Network.HTTP.Types.Header (hHost)
52−import Network.Minio.APICommon
53−import Network.Minio.Credentials
54−import Network.Minio.Data
55−import Network.Minio.Errors
56−import Network.Minio.Sign.V4
57−import Network.Minio.Utils
58−import Network.Minio.XmlParser
18+ ( connect
19+ , S3ReqInfo(..)
20+ , runMinio
21+ , executeRequest
22+ , mkStreamRequest
23+ , getLocation
24+
25+ , isValidBucketName
26+ , checkBucketNameValidity
27+ , isValidObjectName
28+ , checkObjectNameValidity
29+ ) where
30+
31+import Control.Retry (fullJitterBackoff,
32+ limitRetriesByCumulativeDelay,
33+ retrying)
34+import qualified Data.ByteString as B
35+import qualified Data.Char as C
36+import qualified Data.Conduit as C
37+import qualified Data.Map as Map
38+import qualified Data.Text as T
39+import qualified Data.Time.Clock as Time
40+import Network.HTTP.Conduit (Response)
41+import qualified Network.HTTP.Conduit as NC
42+import qualified Network.HTTP.Types as HT
43+import Network.HTTP.Types.Header (hHost)
44+
45+import Lib.Prelude
46+
47+import Network.Minio.APICommon
48+import Network.Minio.Data
49+import Network.Minio.Errors
50+import Network.Minio.Sign.V4
51+import Network.Minio.Utils
52+import Network.Minio.XmlParser
5953
6054-- | Fetch bucket location (region)
6155getLocation :: Bucket -> Minio Region
6256getLocation bucket = do
63− resp <-
64− executeRequest $
65− defaultS3ReqInfo
66− { riBucket = Just bucket,
67− riQueryParams = [("location", Nothing)],
68− riNeedsLocation = False
69− }
57+ resp <- executeRequest $ defaultS3ReqInfo {
58+ riBucket = Just bucket
59+ , riQueryParams = [("location", Nothing)]
60+ , riNeedsLocation = False
61+ }
7062 parseLocation $ NC.responseBody resp
7163
64+
7265-- | Looks for region in RegionMap and updates it using getLocation if
7366-- absent.
7467discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
7568discoverRegion ri = runMaybeT $ do
7669 bucket <- MaybeT $ return $ riBucket ri
7770 regionMay <- lift $ lookupRegionCache bucket
78− maybe
79− ( do
80− l <- lift $ getLocation bucket
81− lift $ addToRegionCache bucket l
82− return l
83− )
84− return
85− regionMay
86−
87−-- | Returns the region to be used for the request.
71+ maybe (do
72+ l <- lift $ getLocation bucket
73+ lift $ addToRegionCache bucket l
74+ return l
75+ ) return regionMay
76+
8877getRegion :: S3ReqInfo -> Minio (Maybe Region)
8978getRegion ri = do
90− ci <- asks mcConnInfo
91−
92− -- getService/makeBucket/getLocation -- don't need location
93− if
94− | not $ riNeedsLocation ri ->
95− return $ Just $ connectRegion ci
96− -- if autodiscovery of location is disabled by user
97− | not $ connectAutoDiscoverRegion ci ->
98− return $ Just $ connectRegion ci
99− -- discover the region for the request
100− | otherwise -> discoverRegion ri
79+ ci <- asks mcConnInfo
80+
81+ -- getService/makeBucket/getLocation -- don't need location
82+ if | not $ riNeedsLocation ri ->
83+ return $ Just $ connectRegion ci
84+
85+ -- if autodiscovery of location is disabled by user
86+ | not $ connectAutoDiscoverRegion ci ->
87+ return $ Just $ connectRegion ci
88+
89+ -- discover the region for the request
90+ | otherwise -> discoverRegion ri
10191
10292getRegionHost :: Region -> Minio Text
10393getRegionHost r = do
104− ci <- asks mcConnInfo
105−
106− if "amazonaws.com" `T.isSuffixOf` connectHost ci
107− then
108− maybe
109− (throwIO $ MErrVRegionNotSupported r)
110− return
111− (H.lookup r awsRegionMap)
112− else return $ connectHost ci
94+ ci <- asks mcConnInfo
11395
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
96+ if "amazonaws.com" `T.isSuffixOf` connectHost ci
97+ then maybe (throwIO $ MErrVRegionNotSupported r)
98+ return (Map.lookup r awsRegionMap)
99+ else return $ connectHost ci
163100
164101buildRequest :: S3ReqInfo -> Minio NC.Request
165102buildRequest ri = do
166− maybe (return ()) checkBucketNameValidity $ riBucket ri
167− maybe (return ()) checkObjectNameValidity $ riObject ri
168−
169− ci <- asks mcConnInfo
170−
171− (host, path, regionMay) <- getHostPathRegion ri
172−
173− let ci' = ci {connectHost = host}
174− hostHeader = (hHost, getHostAddr ci')
175− ri' =
176− ri
177− { riHeaders = hostHeader : riHeaders ri,
178− riRegion = regionMay
179− }
180− -- Does not contain body and auth info.
181− baseRequest =
182− NC.defaultRequest
183− { NC.method = riMethod ri',
184− NC.secure = connectIsSecure ci',
185− NC.host = encodeUtf8 $ connectHost ci',
186− NC.port = connectPort ci',
187− NC.path = path,
188− NC.requestHeaders = riHeaders ri',
189− NC.queryString = HT.renderQuery False $ riQueryParams ri'
190− }
191−
192− timeStamp <- liftIO Time.getCurrentTime
103+ maybe (return ()) checkBucketNameValidity $ riBucket ri
104+ maybe (return ()) checkObjectNameValidity $ riObject ri
105+
106+ ci <- asks mcConnInfo
107+
108+ regionMay <- getRegion ri
109+
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 }
116+ hostHeader = (hHost, getHostAddr ci')
117+
118+ -- Does not contain body and auth info.
119+ baseRequest = NC.defaultRequest
120+ { NC.method = riMethod ri'
121+ , NC.secure = connectIsSecure ci'
122+ , NC.host = encodeUtf8 $ connectHost ci'
123+ , NC.port = connectPort ci'
124+ , NC.path = getS3Path (riBucket ri') (riObject ri')
125+ , NC.requestHeaders = riHeaders ri'
126+ , NC.queryString = HT.renderQuery False $ riQueryParams ri'
127+ }
128+
129+ 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+ }
193168
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− }
258169
259170retryAPIRequest :: Minio a -> Minio a
260171retryAPIRequest apiCall = do
261− resE <-
262− retrying retryPolicy (const shouldRetry) $
263− const $
264− try apiCall
172+ resE <- retrying retryPolicy (const shouldRetry) $
173+ const $ try apiCall
265174 either throwIO return resE
266175 where
267176 -- Retry using the full-jitter backoff method for up to 10 mins
268177 -- total
269− retryPolicy =
270− limitRetriesByCumulativeDelay tenMins $
271− fullJitterBackoff oneMilliSecond
178+ retryPolicy = limitRetriesByCumulativeDelay tenMins
179+ $ fullJitterBackoff oneMilliSecond
180+
272181 oneMilliSecond = 1000 -- in microseconds
273182 tenMins = 10 * 60 * 1000000 -- in microseconds
274183 -- retry on connection related failure
@@ -280,23 +189,23 @@ retryAPIRequest apiCall = do
280189 -- API request failed with a retryable exception
281190 Left httpExn@(NC.HttpExceptionRequest _ exn) ->
282191 case (exn :: NC.HttpExceptionContent) of
283− NC.ResponseTimeout -> return True
284− NC.ConnectionTimeout -> return True
192+ NC.ResponseTimeout -> return True
193+ NC.ConnectionTimeout -> return True
285194 NC.ConnectionFailure _ -> return True
286195 -- We received an unexpected exception
287− _ -> throwIO httpExn
196+ _ -> throwIO httpExn
288197 -- We received an unexpected exception
289198 Left someOtherExn -> throwIO someOtherExn
290199
200+
291201executeRequest :: S3ReqInfo -> Minio (Response LByteString)
292202executeRequest ri = do
293203 req <- buildRequest ri
294204 mgr <- asks mcConnManager
295205 retryAPIRequest $ httpLbs req mgr
296206
297−mkStreamRequest ::
298− S3ReqInfo ->
299− Minio (Response (C.ConduitM () ByteString Minio ()))
207+mkStreamRequest :: S3ReqInfo
208+ -> Minio (Response (C.ConduitM () ByteString Minio ()))
300209mkStreamRequest ri = do
301210 req <- buildRequest ri
302211 mgr <- asks mcConnManager
@@ -305,50 +214,41 @@ mkStreamRequest ri = do
305214-- Bucket name validity check according to AWS rules.
306215isValidBucketName :: Bucket -> Bool
307216isValidBucketName bucket =
308− not
309− ( or
310− [ len < 3 || len > 63,
311− any labelCheck labels,
312− any labelCharsCheck labels,
313− isIPCheck
314− ]
315− )
217+ not (or [ len < 3 || len > 63
218+ , or (map labelCheck labels)
219+ , or (map labelCharsCheck labels)
220+ , isIPCheck
221+ ])
316222 where
317223 len = T.length bucket
318224 labels = T.splitOn "." bucket
225+
319226 -- does label `l` fail basic checks of length and start/end?
320227 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
228+
321229 -- does label `l` have non-allowed characters?
322− labelCharsCheck l =
323− isJust $
324− T.find
325− ( \x ->
326− not
327− ( C.isAsciiLower x
328− || x == '-'
329− || C.isDigit x
330− )
331− )
332− l
230+ labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
231+ x == '-' ||
232+ C.isDigit x)) l
233+
333234 -- does label `l` have non-digit characters?
334235 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
335236 labelAsNums = map (not . labelNonDigits) labels
237+
336238 -- check if bucket name looks like an IP
337239 isIPCheck = and labelAsNums && length labelAsNums == 4
338240
339241-- Throws exception iff bucket name is invalid according to AWS rules.
340−checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
242+checkBucketNameValidity :: MonadIO m => Bucket -> m ()
341243checkBucketNameValidity bucket =
342− unless (isValidBucketName bucket) $
343− throwIO $
344− MErrVInvalidBucketName bucket
244+ when (not $ isValidBucketName bucket) $
245+ throwIO $ MErrVInvalidBucketName bucket
345246
346247isValidObjectName :: Object -> Bool
347248isValidObjectName object =
348249 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
349250
350−checkObjectNameValidity :: (MonadIO m) => Object -> m ()
251+checkObjectNameValidity :: MonadIO m => Object -> m ()
351252checkObjectNameValidity object =
352− unless (isValidObjectName object) $
353− throwIO $
354− MErrVInvalidObjectName object
253+ when (not $ isValidObjectName object) $
254+ throwIO $ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hs+24−36
@@ -16,39 +16,37 @@
1616
1717module Network.Minio.APICommon where
1818
19−import qualified Conduit as C
20−import qualified Data.ByteString as BS
21−import qualified Data.ByteString.Lazy as LB
22−import Data.Conduit.Binary (sourceHandleRange)
23−import qualified Data.Text as T
24−import Lib.Prelude
25−import qualified Network.HTTP.Conduit as NC
26−import qualified Network.HTTP.Types as HT
27−import Network.Minio.Data
28−import Network.Minio.Data.Crypto
29−import Network.Minio.Errors
19+import qualified Conduit as C
20+import qualified Data.ByteString as BS
21+import qualified Data.ByteString.Lazy as LB
22+import Data.Conduit.Binary (sourceHandleRange)
23+import qualified Network.HTTP.Conduit as NC
24+import qualified Network.HTTP.Types as HT
25+
26+import Lib.Prelude
27+
28+import Network.Minio.Data
29+import Network.Minio.Data.Crypto
30+import Network.Minio.Errors
3031
3132sha256Header :: ByteString -> HT.Header
32−sha256Header = ("x-amz-content-sha256",)
33+sha256Header = ("x-amz-content-sha256", )
3334
3435-- | This function throws an error if the payload is a conduit (as it
3536-- will not be possible to re-read the conduit after it is consumed).
3637getPayloadSHA256Hash :: Payload -> Minio ByteString
3738getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
38−getPayloadSHA256Hash (PayloadH h off size) =
39− hashSHA256FromSource $
40− sourceHandleRange
41− h
42− (return . fromIntegral $ off)
43− (return . fromIntegral $ size)
39+getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
40+ sourceHandleRange h
41+ (return . fromIntegral $ off)
42+ (return . fromIntegral $ size)
4443getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
4544
4645getRequestBody :: Payload -> NC.RequestBody
4746getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
4847getRequestBody (PayloadH h off size) =
49− NC.requestBodySource size $
50− sourceHandleRange
51− h
48+ NC.requestBodySource (fromIntegral size) $
49+ sourceHandleRange h
5250 (return . fromIntegral $ off)
5351 (return . fromIntegral $ size)
5452getRequestBody (PayloadC n src) = NC.requestBodySource n src
@@ -57,24 +55,14 @@ mkStreamingPayload :: Payload -> Payload
5755mkStreamingPayload payload =
5856 case payload of
5957 PayloadBS bs ->
60− PayloadC
61− (fromIntegral $ BS.length bs)
58+ PayloadC (fromIntegral $ BS.length bs)
6259 (C.sourceLazy $ LB.fromStrict bs)
6360 PayloadH h off len ->
64− PayloadC len $
65− sourceHandleRange
66− h
67− (return . fromIntegral $ off)
68− (return . fromIntegral $ len)
61+ PayloadC len $ sourceHandleRange h
62+ (return . fromIntegral $ off)
63+ (return . fromIntegral $ len)
6964 _ -> payload
7065
7166isStreamingPayload :: Payload -> Bool
7267isStreamingPayload (PayloadC _ _) = True
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
68+isStreamingPayload _ = False
src/Network/Minio/AdminAPI.hs+391−543
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2018-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -16,341 +16,270 @@
1616
1717module Network.Minio.AdminAPI
1818 ( -- * MinIO Admin API
19−
2019 --------------------
21−
2220 -- | Provides MinIO admin API and related types. It is in
2321 -- experimental state.
24− DriveInfo (..),
25− ErasureInfo (..),
26− Backend (..),
27− ConnStats (..),
28− HttpStats (..),
29− ServerProps (..),
30− CountNAvgTime (..),
31− StorageClass (..),
32− StorageInfo (..),
33− SIData (..),
34− ServerInfo (..),
35− getServerInfo,
36− HealOpts (..),
37− HealResultItem (..),
38− HealStatus (..),
39− HealStartResp (..),
40− startHeal,
41− forceStartHeal,
42− getHealStatus,
43− SetConfigResult (..),
44− NodeSummary (..),
45− setConfig,
46− getConfig,
47− ServerVersion (..),
48− ServiceStatus (..),
49− serviceStatus,
50− ServiceAction (..),
51− serviceSendAction,
52− )
53−where
54−
55−import Data.Aeson
56− ( FromJSON,
57− ToJSON,
58− Value (Object),
59− eitherDecode,
60− object,
61− pairs,
62− parseJSON,
63− toEncoding,
64− toJSON,
65− withObject,
66− withText,
67− (.:),
68− (.:?),
69− (.=),
70− )
71−import qualified Data.Aeson as A
72−import Data.Aeson.Types (typeMismatch)
73−import qualified Data.ByteString as B
74−import qualified Data.ByteString.Lazy as LBS
75−import qualified Data.Text as T
76−import Data.Time (NominalDiffTime, getCurrentTime)
77−import Lib.Prelude
78−import Network.HTTP.Conduit (Response)
79−import qualified Network.HTTP.Conduit as NC
80−import qualified Network.HTTP.Types as HT
81−import Network.HTTP.Types.Header (hHost)
82−import Network.Minio.APICommon
83−import Network.Minio.Credentials
84−import Network.Minio.Data
85−import Network.Minio.Errors
86−import Network.Minio.Sign.V4
87−import Network.Minio.Utils
22+ DriveInfo(..)
23+ , ErasureInfo(..)
24+ , Backend(..)
25+ , ConnStats(..)
26+ , HttpStats(..)
27+ , ServerProps(..)
28+ , CountNAvgTime(..)
29+ , StorageClass(..)
30+ , StorageInfo(..)
31+ , SIData(..)
32+ , ServerInfo(..)
33+ , getServerInfo
34+
35+ , HealOpts(..)
36+ , HealResultItem(..)
37+ , HealStatus(..)
38+ , HealStartResp(..)
39+ , startHeal
40+ , forceStartHeal
41+ , getHealStatus
42+
43+ , SetConfigResult(..)
44+ , NodeSummary(..)
45+ , setConfig
46+ , getConfig
47+
48+ , ServerVersion(..)
49+ , ServiceStatus(..)
50+ , serviceStatus
51+
52+ , ServiceAction(..)
53+ , serviceSendAction
54+ ) where
55+
56+import Data.Aeson (FromJSON, ToJSON, Value (Object),
57+ eitherDecode, object, pairs,
58+ parseJSON, toEncoding, toJSON,
59+ withObject, withText, (.:), (.:?),
60+ (.=))
61+import qualified Data.Aeson as A
62+import Data.Aeson.Types (typeMismatch)
63+import qualified Data.ByteString as B
64+import qualified Data.ByteString.Lazy as LBS
65+import qualified Data.Text as T
66+import Data.Time (NominalDiffTime, getCurrentTime)
67+import Network.HTTP.Conduit (Response)
68+import qualified Network.HTTP.Conduit as NC
69+import qualified Network.HTTP.Types as HT
70+import Network.HTTP.Types.Header (hHost)
71+
72+import Lib.Prelude
73+
74+import Network.Minio.APICommon
75+import Network.Minio.Data
76+import Network.Minio.Errors
77+import Network.Minio.Sign.V4
78+import Network.Minio.Utils
8879
8980data DriveInfo = DriveInfo
90− { diUuid :: Text,
91− diEndpoint :: Text,
92− diState :: Text
93− }
94− deriving stock (Show, Eq)
81+ { diUuid :: Text
82+ , diEndpoint :: Text
83+ , diState :: Text
84+ } deriving (Eq, Show)
9585
9686instance FromJSON DriveInfo where
97− parseJSON = withObject "DriveInfo" $ \v ->
98− DriveInfo
99− <$> v
100− .: "uuid"
101− <*> v
102− .: "endpoint"
103− <*> v
104− .: "state"
87+ parseJSON = withObject "DriveInfo" $ \v -> DriveInfo
88+ <$> v .: "uuid"
89+ <*> v .: "endpoint"
90+ <*> v .: "state"
10591
10692data StorageClass = StorageClass
107− { scParity :: Int,
108− scData :: Int
109− }
110− deriving stock (Show, Eq)
93+ { scParity :: Int
94+ , scData :: Int
95+ } deriving (Eq, Show)
11196
11297data ErasureInfo = ErasureInfo
113− { eiOnlineDisks :: Int,
114− eiOfflineDisks :: Int,
115− eiStandard :: StorageClass,
116− eiReducedRedundancy :: StorageClass,
117− eiSets :: [[DriveInfo]]
118− }
119− deriving stock (Show, Eq)
98+ { eiOnlineDisks :: Int
99+ , eiOfflineDisks :: Int
100+ , eiStandard :: StorageClass
101+ , eiReducedRedundancy :: StorageClass
102+ , eiSets :: [[DriveInfo]]
103+ } deriving (Eq, Show)
120104
121105instance FromJSON ErasureInfo where
122− parseJSON = withObject "ErasureInfo" $ \v -> do
123− onlineDisks <- v .: "OnlineDisks"
124− offlineDisks <- v .: "OfflineDisks"
125− stdClass <-
126− StorageClass
127− <$> v
128− .: "StandardSCData"
129− <*> v
130− .: "StandardSCParity"
131− rrClass <-
132− StorageClass
133− <$> v
134− .: "RRSCData"
135− <*> v
136− .: "RRSCParity"
137− sets <- v .: "Sets"
138− return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
139−
140−data Backend
141− = BackendFS
142− | BackendErasure ErasureInfo
143− deriving stock (Show, Eq)
106+ parseJSON = withObject "ErasureInfo" $ \v -> do
107+ onlineDisks <- v .: "OnlineDisks"
108+ offlineDisks <- v .: "OfflineDisks"
109+ stdClass <- StorageClass
110+ <$> v .: "StandardSCData"
111+ <*> v .: "StandardSCParity"
112+ rrClass <- StorageClass
113+ <$> v .: "RRSCData"
114+ <*> v .: "RRSCParity"
115+ sets <- v .: "Sets"
116+ return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
117+
118+data Backend = BackendFS
119+ | BackendErasure ErasureInfo
120+ deriving (Eq, Show)
144121
145122instance FromJSON Backend where
146− parseJSON = withObject "Backend" $ \v -> do
147− typ <- v .: "Type"
148− case typ :: Int of
149− 1 -> return BackendFS
150− 2 -> BackendErasure <$> parseJSON (Object v)
151− _ -> typeMismatch "BackendType" (Object v)
123+ parseJSON = withObject "Backend" $ \v -> do
124+ typ <- v .: "Type"
125+ case typ :: Int of
126+ 1 -> return BackendFS
127+ 2 -> BackendErasure <$> parseJSON (Object v)
128+ _ -> typeMismatch "BackendType" (Object v)
152129
153130data ConnStats = ConnStats
154− { csTransferred :: Int64,
155− csReceived :: Int64
156− }
157− deriving stock (Show, Eq)
131+ { csTransferred :: Int64
132+ , csReceived :: Int64
133+ } deriving (Eq, Show)
158134
159135instance FromJSON ConnStats where
160− parseJSON = withObject "ConnStats" $ \v ->
161− ConnStats
162− <$> v
163− .: "transferred"
164− <*> v
165− .: "received"
136+ parseJSON = withObject "ConnStats" $ \v -> ConnStats
137+ <$> v .: "transferred"
138+ <*> v .: "received"
166139
167140data ServerProps = ServerProps
168− { spUptime :: NominalDiffTime,
169− spVersion :: Text,
170− spCommitId :: Text,
171− spRegion :: Text,
172− spSqsArns :: [Text]
173− }
174− deriving stock (Show, Eq)
141+ { spUptime :: NominalDiffTime
142+ , spVersion :: Text
143+ , spCommitId :: Text
144+ , spRegion :: Text
145+ , spSqsArns :: [Text]
146+ } deriving (Eq, Show)
175147
176148instance FromJSON ServerProps where
177− parseJSON = withObject "SIServer" $ \v -> do
178− uptimeNs <- v .: "uptime"
179− let uptime = uptimeNs / 1e9
180− ver <- v .: "version"
181− commitId <- v .: "commitID"
182− region <- v .: "region"
183− arn <- v .: "sqsARN"
184− return $ ServerProps uptime ver commitId region arn
149+ parseJSON = withObject "SIServer" $ \v -> do
150+ uptimeNs <- v .: "uptime"
151+ let uptime = uptimeNs / 1e9
152+ ver <- v .: "version"
153+ commitId <- v .: "commitID"
154+ region <- v .: "region"
155+ arn <- v .: "sqsARN"
156+ return $ ServerProps uptime ver commitId region arn
185157
186158data StorageInfo = StorageInfo
187− { siUsed :: Int64,
188− siBackend :: Backend
189− }
190− deriving stock (Show, Eq)
159+ { siUsed :: Int64
160+ , siBackend :: Backend
161+ } deriving (Eq, Show)
191162
192163instance FromJSON StorageInfo where
193− parseJSON = withObject "StorageInfo" $ \v ->
194− StorageInfo
195− <$> v
196− .: "Used"
197− <*> v
198− .: "Backend"
164+ parseJSON = withObject "StorageInfo" $ \v -> StorageInfo
165+ <$> v .: "Used"
166+ <*> v .: "Backend"
199167
200168data CountNAvgTime = CountNAvgTime
201− { caCount :: Int64,
202− caAvgDuration :: Text
203− }
204− deriving stock (Show, Eq)
169+ { caCount :: Int64
170+ , caAvgDuration :: Text
171+ } deriving (Eq, Show)
205172
206173instance FromJSON CountNAvgTime where
207− parseJSON = withObject "CountNAvgTime" $ \v ->
208− CountNAvgTime
209− <$> v
210− .: "count"
211− <*> v
212− .: "avgDuration"
174+ parseJSON = withObject "CountNAvgTime" $ \v -> CountNAvgTime
175+ <$> v .: "count"
176+ <*> v .: "avgDuration"
213177
214178data HttpStats = HttpStats
215− { hsTotalHeads :: CountNAvgTime,
216− hsSuccessHeads :: CountNAvgTime,
217− hsTotalGets :: CountNAvgTime,
218− hsSuccessGets :: CountNAvgTime,
219− hsTotalPuts :: CountNAvgTime,
220− hsSuccessPuts :: CountNAvgTime,
221− hsTotalPosts :: CountNAvgTime,
222− hsSuccessPosts :: CountNAvgTime,
223− hsTotalDeletes :: CountNAvgTime,
224− hsSuccessDeletes :: CountNAvgTime
225− }
226− deriving stock (Show, Eq)
179+ { hsTotalHeads :: CountNAvgTime
180+ , hsSuccessHeads :: CountNAvgTime
181+ , hsTotalGets :: CountNAvgTime
182+ , hsSuccessGets :: CountNAvgTime
183+ , hsTotalPuts :: CountNAvgTime
184+ , hsSuccessPuts :: CountNAvgTime
185+ , hsTotalPosts :: CountNAvgTime
186+ , hsSuccessPosts :: CountNAvgTime
187+ , hsTotalDeletes :: CountNAvgTime
188+ , hsSuccessDeletes :: CountNAvgTime
189+ } deriving (Eq, Show)
227190
228191instance FromJSON HttpStats where
229− parseJSON = withObject "HttpStats" $ \v ->
230− HttpStats
231− <$> v
232− .: "totalHEADs"
233− <*> v
234− .: "successHEADs"
235− <*> v
236− .: "totalGETs"
237− <*> v
238− .: "successGETs"
239− <*> v
240− .: "totalPUTs"
241− <*> v
242− .: "successPUTs"
243− <*> v
244− .: "totalPOSTs"
245− <*> v
246− .: "successPOSTs"
247− <*> v
248− .: "totalDELETEs"
249− <*> v
250− .: "successDELETEs"
192+ parseJSON = withObject "HttpStats" $ \v -> HttpStats
193+ <$> v .: "totalHEADs"
194+ <*> v .: "successHEADs"
195+ <*> v .: "totalGETs"
196+ <*> v .: "successGETs"
197+ <*> v .: "totalPUTs"
198+ <*> v .: "successPUTs"
199+ <*> v .: "totalPOSTs"
200+ <*> v .: "successPOSTs"
201+ <*> v .: "totalDELETEs"
202+ <*> v .: "successDELETEs"
251203
252204data SIData = SIData
253− { sdStorage :: StorageInfo,
254− sdConnStats :: ConnStats,
255− sdHttpStats :: HttpStats,
256− sdProps :: ServerProps
257− }
258− deriving stock (Show, Eq)
205+ { sdStorage :: StorageInfo
206+ , sdConnStats :: ConnStats
207+ , sdHttpStats :: HttpStats
208+ , sdProps :: ServerProps
209+ } deriving (Eq, Show)
259210
260211instance FromJSON SIData where
261− parseJSON = withObject "SIData" $ \v ->
262− SIData
263− <$> v
264− .: "storage"
265− <*> v
266− .: "network"
267− <*> v
268− .: "http"
269− <*> v
270− .: "server"
212+ parseJSON = withObject "SIData" $ \v -> SIData
213+ <$> v .: "storage"
214+ <*> v .: "network"
215+ <*> v .: "http"
216+ <*> v .: "server"
271217
272218data ServerInfo = ServerInfo
273− { siError :: Text,
274− siAddr :: Text,
275− siData :: SIData
276− }
277− deriving stock (Show, Eq)
219+ { siError :: Text
220+ , siAddr :: Text
221+ , siData :: SIData
222+ } deriving (Eq, Show)
278223
279224instance FromJSON ServerInfo where
280− parseJSON = withObject "ServerInfo" $ \v ->
281− ServerInfo
282− <$> v
283− .: "error"
284− <*> v
285− .: "addr"
286− <*> v
287− .: "data"
225+ parseJSON = withObject "ServerInfo" $ \v -> ServerInfo
226+ <$> v .: "error"
227+ <*> v .: "addr"
228+ <*> v .: "data"
288229
289230data ServerVersion = ServerVersion
290− { svVersion :: Text,
291− svCommitId :: Text
292− }
293− deriving stock (Show, Eq)
231+ { svVersion :: Text
232+ , svCommitId :: Text
233+ } deriving (Eq, Show)
294234
295235instance FromJSON ServerVersion where
296− parseJSON = withObject "ServerVersion" $ \v ->
297− ServerVersion
298− <$> v
299− .: "version"
300− <*> v
301− .: "commitID"
236+ parseJSON = withObject "ServerVersion" $ \v -> ServerVersion
237+ <$> v .: "version"
238+ <*> v .: "commitID"
302239
303240data ServiceStatus = ServiceStatus
304− { ssVersion :: ServerVersion,
305− ssUptime :: NominalDiffTime
306− }
307− deriving stock (Show, Eq)
241+ { ssVersion :: ServerVersion
242+ , ssUptime :: NominalDiffTime
243+ } deriving (Eq, Show)
308244
309245instance FromJSON ServiceStatus where
310− parseJSON = withObject "ServiceStatus" $ \v -> do
311− serverVersion <- v .: "serverVersion"
312− uptimeNs <- v .: "uptime"
313− let uptime = uptimeNs / 1e9
314− return $ ServiceStatus serverVersion uptime
246+ parseJSON = withObject "ServiceStatus" $ \v -> do
247+ serverVersion <- v .: "serverVersion"
248+ uptimeNs <- v .: "uptime"
249+ let uptime = uptimeNs / 1e9
250+ return $ ServiceStatus serverVersion uptime
315251
316−data ServiceAction
317− = ServiceActionRestart
318− | ServiceActionStop
319− deriving stock (Show, Eq)
252+data ServiceAction = ServiceActionRestart
253+ | ServiceActionStop
254+ deriving (Eq, Show)
320255
321256instance ToJSON ServiceAction where
322− toJSON a = object ["action" .= serviceActionToText a]
257+ toJSON a = object [ "action" .= serviceActionToText a ]
323258
324259serviceActionToText :: ServiceAction -> Text
325260serviceActionToText a = case a of
326261 ServiceActionRestart -> "restart"
327− ServiceActionStop -> "stop"
262+ ServiceActionStop -> "stop"
328263
329264adminPath :: ByteString
330265adminPath = "/minio/admin"
331266
332267data HealStartResp = HealStartResp
333− { hsrClientToken :: Text,
334− hsrClientAddr :: Text,
335− hsrStartTime :: UTCTime
336− }
337− deriving stock (Show, Eq)
268+ { hsrClientToken :: Text
269+ , hsrClientAddr :: Text
270+ , hsrStartTime :: UTCTime
271+ } deriving (Eq, Show)
338272
339273instance FromJSON HealStartResp where
340− parseJSON = withObject "HealStartResp" $ \v ->
341− HealStartResp
342− <$> v
343− .: "clientToken"
344− <*> v
345− .: "clientAddress"
346− <*> v
347− .: "startTime"
274+ parseJSON = withObject "HealStartResp" $ \v -> HealStartResp
275+ <$> v .: "clientToken"
276+ <*> v .: "clientAddress"
277+ <*> v .: "startTime"
348278
349279data HealOpts = HealOpts
350− { hoRecursive :: Bool,
351− hoDryRun :: Bool
352− }
353− deriving stock (Show, Eq)
280+ { hoRecursive :: Bool
281+ , hoDryRun :: Bool
282+ } deriving (Eq, Show)
354283
355284instance ToJSON HealOpts where
356285 toJSON (HealOpts r d) =
@@ -359,259 +288,197 @@ instance ToJSON HealOpts where
359288 pairs ("recursive" .= r <> "dryRun" .= d)
360289
361290instance FromJSON HealOpts where
362− parseJSON = withObject "HealOpts" $ \v ->
363− HealOpts
364− <$> v
365− .: "recursive"
366− <*> v
367− .: "dryRun"
368−
369−data HealItemType
370− = HealItemMetadata
371− | HealItemBucket
372− | HealItemBucketMetadata
373− | HealItemObject
374− deriving stock (Show, Eq)
291+ parseJSON = withObject "HealOpts" $ \v -> HealOpts
292+ <$> v .: "recursive"
293+ <*> v .: "dryRun"
294+
295+data HealItemType = HealItemMetadata
296+ | HealItemBucket
297+ | HealItemBucketMetadata
298+ | HealItemObject
299+ deriving (Eq, Show)
375300
376301instance FromJSON HealItemType where
377− parseJSON = withText "HealItemType" $ \v -> case v of
378− "metadata" -> return HealItemMetadata
379− "bucket" -> return HealItemBucket
380− "object" -> return HealItemObject
381− "bucket-metadata" -> return HealItemBucketMetadata
382− _ -> typeMismatch "HealItemType" (A.String v)
302+ parseJSON = withText "HealItemType" $ \v -> case v of
303+ "metadata" -> return HealItemMetadata
304+ "bucket" -> return HealItemBucket
305+ "object" -> return HealItemObject
306+ "bucket-metadata" -> return HealItemBucketMetadata
307+ _ -> typeMismatch "HealItemType" (A.String v)
383308
384309data NodeSummary = NodeSummary
385− { nsName :: Text,
386− nsErrSet :: Bool,
387− nsErrMessage :: Text
388− }
389− deriving stock (Show, Eq)
310+ { nsName :: Text
311+ , nsErrSet :: Bool
312+ , nsErrMessage :: Text
313+ } deriving (Eq, Show)
390314
391315instance FromJSON NodeSummary where
392− parseJSON = withObject "NodeSummary" $ \v ->
393− NodeSummary
394− <$> v
395− .: "name"
396− <*> v
397− .: "errSet"
398− <*> v
399− .: "errMsg"
316+ parseJSON = withObject "NodeSummary" $ \v -> NodeSummary
317+ <$> v .: "name"
318+ <*> v .: "errSet"
319+ <*> v .: "errMsg"
400320
401321data SetConfigResult = SetConfigResult
402− { scrStatus :: Bool,
403− scrNodeSummary :: [NodeSummary]
404− }
405− deriving stock (Show, Eq)
322+ { scrStatus :: Bool
323+ , scrNodeSummary :: [NodeSummary]
324+ } deriving (Eq, Show)
406325
407326instance FromJSON SetConfigResult where
408− parseJSON = withObject "SetConfigResult" $ \v ->
409− SetConfigResult
410− <$> v
411− .: "status"
412− <*> v
413− .: "nodeResults"
327+ parseJSON = withObject "SetConfigResult" $ \v -> SetConfigResult
328+ <$> v .: "status"
329+ <*> v .: "nodeResults"
414330
415331data HealResultItem = HealResultItem
416− { hriResultIdx :: Int,
417− hriType :: HealItemType,
418− hriBucket :: Bucket,
419− hriObject :: Object,
420− hriDetail :: Text,
421− hriParityBlocks :: Maybe Int,
422− hriDataBlocks :: Maybe Int,
423− hriDiskCount :: Int,
424− hriSetCount :: Int,
425− hriObjectSize :: Int,
426− hriBefore :: [DriveInfo],
427− hriAfter :: [DriveInfo]
428− }
429− deriving stock (Show, Eq)
332+ { hriResultIdx :: Int
333+ , hriType :: HealItemType
334+ , hriBucket :: Bucket
335+ , hriObject :: Object
336+ , hriDetail :: Text
337+ , hriParityBlocks :: Maybe Int
338+ , hriDataBlocks :: Maybe Int
339+ , hriDiskCount :: Int
340+ , hriSetCount :: Int
341+ , hriObjectSize :: Int
342+ , hriBefore :: [DriveInfo]
343+ , hriAfter :: [DriveInfo]
344+ } deriving (Eq, Show)
430345
431346instance FromJSON HealResultItem where
432− parseJSON = withObject "HealResultItem" $ \v ->
433− HealResultItem
434− <$> v
435− .: "resultId"
436− <*> v
437− .: "type"
438− <*> v
439− .: "bucket"
440− <*> v
441− .: "object"
442− <*> v
443− .: "detail"
444− <*> v
445− .:? "parityBlocks"
446− <*> v
447− .:? "dataBlocks"
448− <*> v
449− .: "diskCount"
450− <*> v
451− .: "setCount"
452− <*> v
453− .: "objectSize"
454− <*> ( do
455− before <- v .: "before"
456− before .: "drives"
457− )
458− <*> ( do
459− after <- v .: "after"
460− after .: "drives"
461− )
347+ parseJSON = withObject "HealResultItem" $ \v -> HealResultItem
348+ <$> v .: "resultId"
349+ <*> v .: "type"
350+ <*> v .: "bucket"
351+ <*> v .: "object"
352+ <*> v .: "detail"
353+ <*> v .:? "parityBlocks"
354+ <*> v .:? "dataBlocks"
355+ <*> v .: "diskCount"
356+ <*> v .: "setCount"
357+ <*> v .: "objectSize"
358+ <*> (do before <- v .: "before"
359+ before .: "drives")
360+ <*> (do after <- v .: "after"
361+ after .: "drives")
462362
463363data HealStatus = HealStatus
464− { hsSummary :: Text,
465− hsStartTime :: UTCTime,
466− hsSettings :: HealOpts,
467− hsNumDisks :: Int,
468− hsFailureDetail :: Maybe Text,
469− hsItems :: Maybe [HealResultItem]
470− }
471− deriving stock (Show, Eq)
364+ { hsSummary :: Text
365+ , hsStartTime :: UTCTime
366+ , hsSettings :: HealOpts
367+ , hsNumDisks :: Int
368+ , hsFailureDetail :: Maybe Text
369+ , hsItems :: Maybe [HealResultItem]
370+ } deriving (Eq, Show)
472371
473372instance FromJSON HealStatus where
474− parseJSON = withObject "HealStatus" $ \v ->
475− HealStatus
476− <$> v
477− .: "Summary"
478− <*> v
479− .: "StartTime"
480− <*> v
481− .: "Settings"
482− <*> v
483− .: "NumDisks"
484− <*> v
485− .:? "Detail"
486− <*> v
487− .: "Items"
373+ parseJSON = withObject "HealStatus" $ \v -> HealStatus
374+ <$> v .: "Summary"
375+ <*> v .: "StartTime"
376+ <*> v .: "Settings"
377+ <*> v .: "NumDisks"
378+ <*> v .:? "Detail"
379+ <*> v .: "Items"
488380
489381healPath :: Maybe Bucket -> Maybe Text -> ByteString
490382healPath bucket prefix = do
491− if isJust bucket
492− then
493− encodeUtf8 $
494− "v1/heal/"
495− <> fromMaybe "" bucket
496− <> "/"
497− <> fromMaybe "" prefix
498− else encodeUtf8 ("v1/heal/" :: Text)
383+ if (isJust bucket)
384+ then encodeUtf8 $ "v1/heal/" <> fromMaybe "" bucket <> "/"
385+ <> fromMaybe "" prefix
386+ else encodeUtf8 $ "v1/heal/"
499387
500388-- | Get server version and uptime.
501389serviceStatus :: Minio ServiceStatus
502390serviceStatus = do
503− rsp <-
504− executeAdminRequest
505− AdminReqInfo
506− { ariMethod = HT.methodGet,
507− ariPayload = PayloadBS B.empty,
508− ariPayloadHash = Nothing,
509− ariPath = "v1/service",
510− ariHeaders = [],
511− ariQueryParams = []
512− }
513−
514− let rspBS = NC.responseBody rsp
515− case eitherDecode rspBS of
516− Right ss -> return ss
517− Left err -> throwIO $ MErrVJsonParse $ T.pack err
391+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
392+ , ariPayload = PayloadBS B.empty
393+ , ariPayloadHash = Nothing
394+ , ariPath = "v1/service"
395+ , ariHeaders = []
396+ , ariQueryParams = []
397+ }
398+
399+ let rspBS = NC.responseBody rsp
400+ case eitherDecode rspBS of
401+ Right ss -> return ss
402+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
518403
519404-- | Send service restart or stop action to MinIO server.
520405serviceSendAction :: ServiceAction -> Minio ()
521406serviceSendAction action = do
522− let payload = PayloadBS $ LBS.toStrict $ A.encode action
523− void $
524− executeAdminRequest
525− AdminReqInfo
526− { ariMethod = HT.methodPost,
527− ariPayload = payload,
528− ariPayloadHash = Nothing,
529− ariPath = "v1/service",
530− ariHeaders = [],
531− ariQueryParams = []
532− }
407+ let payload = PayloadBS $ LBS.toStrict $ A.encode action
408+ void $ executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
409+ , ariPayload = payload
410+ , ariPayloadHash = Nothing
411+ , ariPath = "v1/service"
412+ , ariHeaders = []
413+ , ariQueryParams = []
414+ }
533415
534416-- | Get the current config file from server.
535417getConfig :: Minio ByteString
536418getConfig = do
537− rsp <-
538− executeAdminRequest
539− AdminReqInfo
540− { ariMethod = HT.methodGet,
541− ariPayload = PayloadBS B.empty,
542− ariPayloadHash = Nothing,
543− ariPath = "v1/config",
544− ariHeaders = [],
545− ariQueryParams = []
546− }
547− return $ LBS.toStrict $ NC.responseBody rsp
419+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
420+ , ariPayload = PayloadBS B.empty
421+ , ariPayloadHash = Nothing
422+ , ariPath = "v1/config"
423+ , ariHeaders = []
424+ , ariQueryParams = []
425+ }
426+ return $ LBS.toStrict $ NC.responseBody rsp
548427
549428-- | Set a new config to the server.
550429setConfig :: ByteString -> Minio SetConfigResult
551430setConfig config = do
552− rsp <-
553− executeAdminRequest
554− AdminReqInfo
555− { ariMethod = HT.methodPut,
556− ariPayload = PayloadBS config,
557− ariPayloadHash = Nothing,
558− ariPath = "v1/config",
559− ariHeaders = [],
560− ariQueryParams = []
561− }
562−
563− let rspBS = NC.responseBody rsp
564− case eitherDecode rspBS of
565− Right scr -> return scr
566− Left err -> throwIO $ MErrVJsonParse $ T.pack err
431+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPut
432+ , ariPayload = PayloadBS config
433+ , ariPayloadHash = Nothing
434+ , ariPath = "v1/config"
435+ , ariHeaders = []
436+ , ariQueryParams = []
437+ }
438+
439+ let rspBS = NC.responseBody rsp
440+ case eitherDecode rspBS of
441+ Right scr -> return scr
442+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
567443
568444-- | Get the progress of currently running heal task, this API should be
569445-- invoked right after `startHeal`. `token` is obtained after `startHeal`
570446-- which should be used to get the heal status.
571447getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
572448getHealStatus bucket prefix token = do
573− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
574− let qparams = HT.queryTextToQuery [("clientToken", Just token)]
575− rsp <-
576− executeAdminRequest
577− AdminReqInfo
578− { ariMethod = HT.methodPost,
579− ariPayload = PayloadBS B.empty,
580− ariPayloadHash = Nothing,
581− ariPath = healPath bucket prefix,
582− ariHeaders = [],
583− ariQueryParams = qparams
584− }
585− let rspBS = NC.responseBody rsp
586− case eitherDecode rspBS of
587− Right hs -> return hs
588− Left err -> throwIO $ MErrVJsonParse $ T.pack err
449+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
450+ let qparams = HT.queryTextToQuery [("clientToken", Just token)]
451+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
452+ , ariPayload = PayloadBS B.empty
453+ , ariPayloadHash = Nothing
454+ , ariPath = healPath bucket prefix
455+ , ariHeaders = []
456+ , ariQueryParams = qparams
457+ }
458+ let rspBS = NC.responseBody rsp
459+ case eitherDecode rspBS of
460+ Right hs -> return hs
461+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
589462
590463doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
591464doHeal bucket prefix opts forceStart = do
592− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
593− let payload = PayloadBS $ LBS.toStrict $ A.encode opts
594− let qparams =
595− bool
596− []
597− (HT.queryTextToQuery [("forceStart", Just "true")])
598− forceStart
599−
600− rsp <-
601− executeAdminRequest
602− AdminReqInfo
603− { ariMethod = HT.methodPost,
604− ariPayload = payload,
605− ariPayloadHash = Nothing,
606− ariPath = healPath bucket prefix,
607− ariHeaders = [],
608− ariQueryParams = qparams
609− }
610−
611− let rspBS = NC.responseBody rsp
612− case eitherDecode rspBS of
613− Right hsr -> return hsr
614− Left err -> throwIO $ MErrVJsonParse $ T.pack err
465+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
466+ let payload = PayloadBS $ LBS.toStrict $ A.encode opts
467+ let qparams = bool [] (HT.queryTextToQuery [("forceStart", Just "true")])
468+ forceStart
469+
470+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
471+ , ariPayload = payload
472+ , ariPayloadHash = Nothing
473+ , ariPath = healPath bucket prefix
474+ , ariHeaders = []
475+ , ariQueryParams = qparams
476+ }
477+
478+ let rspBS = NC.responseBody rsp
479+ case eitherDecode rspBS of
480+ Right hsr -> return hsr
481+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
615482
616483-- | Start a heal sequence that scans data under given (possible empty)
617484-- `bucket` and `prefix`. The `recursive` bool turns on recursive
@@ -633,79 +500,60 @@ forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
633500-- properties, storage information, network statistics, etc.
634501getServerInfo :: Minio [ServerInfo]
635502getServerInfo = do
636− rsp <-
637− executeAdminRequest
638− AdminReqInfo
639− { ariMethod = HT.methodGet,
640− ariPayload = PayloadBS B.empty,
641− ariPayloadHash = Nothing,
642− ariPath = "v1/info",
643− ariHeaders = [],
644− ariQueryParams = []
645− }
646− let rspBS = NC.responseBody rsp
647− case eitherDecode rspBS of
648− Right si -> return si
649− Left err -> throwIO $ MErrVJsonParse $ T.pack err
503+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
504+ , ariPayload = PayloadBS B.empty
505+ , ariPayloadHash = Nothing
506+ , ariPath = "v1/info"
507+ , ariHeaders = []
508+ , ariQueryParams = []
509+ }
510+ let rspBS = NC.responseBody rsp
511+ case eitherDecode rspBS of
512+ Right si -> return si
513+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
650514
651515executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
652516executeAdminRequest ari = do
653− req <- buildAdminRequest ari
654− mgr <- asks mcConnManager
655− httpLbs req mgr
517+ req <- buildAdminRequest ari
518+ mgr <- asks mcConnManager
519+ httpLbs req mgr
656520
657521buildAdminRequest :: AdminReqInfo -> Minio NC.Request
658522buildAdminRequest areq = do
659− ci <- asks mcConnInfo
660− sha256Hash <-
661− if connectIsSecure ci
662− then -- if secure connection
663− return "UNSIGNED-PAYLOAD"
664− else -- otherwise compute sha256
665− getPayloadSHA256Hash (ariPayload areq)
666−
667− timeStamp <- liftIO getCurrentTime
668−
669− mgr <- asks mcConnManager
670− cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671−
672− let hostHeader = (hHost, getHostAddr ci)
673− newAreq =
674− areq
675− { ariPayloadHash = Just sha256Hash,
676− ariHeaders =
677− hostHeader
678− : sha256Header sha256Hash
679− : ariHeaders areq
680− }
681− signReq = toRequest ci newAreq
682− sp =
683− SignParams
684− (coerce $ cvAccessKey cv)
685− (coerce $ cvSecretKey cv)
686− (coerce $ cvSessionToken cv)
687− ServiceS3
688− timeStamp
689− Nothing
690− Nothing
691− (ariPayloadHash newAreq)
692− signHeaders = signV4 sp signReq
693−
694− -- Update signReq with Authorization header containing v4 signature
695− return
696− signReq
697− { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
698− }
523+ ci <- asks mcConnInfo
524+ sha256Hash <- if | connectIsSecure ci ->
525+ -- if secure connection
526+ return "UNSIGNED-PAYLOAD"
527+
528+ -- otherwise compute sha256
529+ | otherwise -> getPayloadSHA256Hash (ariPayload areq)
530+
531+ timeStamp <- liftIO getCurrentTime
532+
533+ let hostHeader = (hHost, getHostAddr ci)
534+ newAreq = areq { ariPayloadHash = Just sha256Hash
535+ , ariHeaders = hostHeader
536+ : sha256Header sha256Hash
537+ : ariHeaders areq
538+ }
539+ signReq = toRequest ci newAreq
540+ sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
541+ timeStamp Nothing Nothing (ariPayloadHash newAreq)
542+ signHeaders = signV4 sp signReq
543+
544+ -- Update signReq with Authorization header containing v4 signature
545+ return signReq {
546+ NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
547+ }
699548 where
700549 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
701− toRequest ci aReq =
702− NC.defaultRequest
703− { NC.method = ariMethod aReq,
704− NC.secure = connectIsSecure ci,
705− NC.host = encodeUtf8 $ connectHost ci,
706− NC.port = connectPort ci,
707− NC.path = B.intercalate "/" [adminPath, ariPath aReq],
708− NC.requestHeaders = ariHeaders aReq,
709− NC.queryString = HT.renderQuery False $ ariQueryParams aReq,
710− NC.requestBody = getRequestBody (ariPayload aReq)
550+ toRequest ci aReq = NC.defaultRequest
551+ { NC.method = ariMethod aReq
552+ , NC.secure = connectIsSecure ci
553+ , NC.host = encodeUtf8 $ connectHost ci
554+ , NC.port = connectPort ci
555+ , NC.path = B.intercalate "/" [adminPath, ariPath aReq]
556+ , NC.requestHeaders = ariHeaders aReq
557+ , NC.queryString = HT.renderQuery False $ ariQueryParams aReq
558+ , NC.requestBody = getRequestBody (ariPayload aReq)
711559 }
src/Network/Minio/CopyObject.hs+34−48
@@ -16,19 +16,19 @@
1616
1717module Network.Minio.CopyObject where
1818
19−import qualified Data.List as List
20−import Lib.Prelude
21−import Network.Minio.Data
22−import Network.Minio.Errors
23−import Network.Minio.S3API
24−import Network.Minio.Utils
19+import qualified Data.List as List
20+
21+import Lib.Prelude
22+
23+import Network.Minio.Data
24+import Network.Minio.Errors
25+import Network.Minio.S3API
26+import Network.Minio.Utils
27+
2528
2629-- | Copy an object using single or multipart copy strategy.
27−copyObjectInternal ::
28− Bucket ->
29− Object ->
30− SourceInfo ->
31− Minio ETag
30+copyObjectInternal :: Bucket -> Object -> SourceInfo
31+ -> Minio ETag
3232copyObjectInternal b' o srcInfo = do
3333 let sBucket = srcBucket srcInfo
3434 sObject = srcObject srcInfo
@@ -43,33 +43,27 @@ copyObjectInternal b' o srcInfo = do
4343 startOffset = fst range
4444 endOffset = snd range
4545
46− when
47− ( isJust rangeMay
48− && ( (startOffset < 0)
49− || (endOffset < startOffset)
50− || (endOffset >= srcSize)
51− )
52− )
53− $ throwIO
54− $ MErrVInvalidSrcObjByteRange range
46+ when (isJust rangeMay &&
47+ or [startOffset < 0, endOffset < startOffset,
48+ endOffset >= fromIntegral srcSize]) $
49+ throwIO $ MErrVInvalidSrcObjByteRange range
5550
5651 -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
5752 -- 2. If startOffset /= 0 use multipart copy
58− let destSize =
59− (\(a, b) -> b - a + 1) $
60− maybe (0, srcSize - 1) identity rangeMay
53+ let destSize = (\(a, b) -> b - a + 1 ) $
54+ maybe (0, srcSize - 1) identity rangeMay
6155
6256 if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
6357 then multiPartCopyObject b' o srcInfo srcSize
64− else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} []
58+
59+ else fst <$> copyObjectSingle b' o srcInfo{srcRange = Nothing} []
6560
6661-- | Given the input byte range of the source object, compute the
6762-- splits for a multipart copy object procedure. Minimum part size
6863-- used is minPartSize.
6964selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
70−selectCopyRanges (st, end) =
71− zip pns $
72− zipWith (\x y -> (st + x, st + x + y - 1)) startOffsets partSizes
65+selectCopyRanges (st, end) = zip pns $
66+ map (\(x, y) -> (st + x, st + x + y - 1)) $ zip startOffsets partSizes
7367 where
7468 size = end - st + 1
7569 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
@@ -77,30 +71,22 @@ selectCopyRanges (st, end) =
7771-- | Perform a multipart copy object action. Since we cannot verify
7872-- existing parts based on the source object, there is no resuming
7973-- copy action support.
80−multiPartCopyObject ::
81− Bucket ->
82− Object ->
83− SourceInfo ->
84− Int64 ->
85− Minio ETag
74+multiPartCopyObject :: Bucket -> Object -> SourceInfo -> Int64
75+ -> Minio ETag
8676multiPartCopyObject b o cps srcSize = do
8777 uid <- newMultipartUpload b o []
8878
89− let byteRange = maybe (0, srcSize - 1) identity $ srcRange cps
79+ let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $ srcRange cps
9080 partRanges = selectCopyRanges byteRange
91− partSources =
92− map
93− (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end)}))
94− partRanges
95− dstInfo = defaultDestinationInfo {dstBucket = b, dstObject = o}
96−
97− copiedParts <-
98− limitedMapConcurrently
99− 10
100− ( \(pn, cps') -> do
101− (etag, _) <- copyObjectPart dstInfo cps' uid pn []
102− return (pn, etag)
103− )
104− partSources
81+ partSources = map (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end) }))
82+ partRanges
83+ dstInfo = defaultDestinationInfo { dstBucket = b, dstObject = o}
84+
85+ copiedParts <- limitedMapConcurrently 10
86+ (\(pn, cps') -> do
87+ (etag, _) <- copyObjectPart dstInfo cps' uid pn []
88+ return (pn, etag)
89+ )
90+ partSources
10591
10692 completeMultipartUpload b o uid copiedParts
src/Network/Minio/Credentials.hsdeleted−77
@@ -1,77 +0,0 @@
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.hsdeleted−266
@@ -1,266 +0,0 @@
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.hsdeleted−90
@@ -1,90 +0,0 @@
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+557−730
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,64 +13,47 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE CPP #-}
17−{-# LANGUAGE GADTs #-}
18−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
19−{-# LANGUAGE StrictData #-}
20−{-# LANGUAGE TypeFamilies #-}
2116
17+{-# LANGUAGE CPP #-}
18+{-# LANGUAGE GADTs #-}
19+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
20+{-# LANGUAGE TypeFamilies #-}
2221module Network.Minio.Data where
2322
24−import qualified Conduit as C
25−import qualified Control.Concurrent.MVar as M
26−import Control.Monad.Trans.Except (throwE)
27−import Control.Monad.Trans.Resource
28− ( MonadResource,
29− MonadThrow (..),
30− MonadUnliftIO,
31− ResourceT,
32− runResourceT,
33− )
34−import qualified Data.Aeson as A
35−import qualified Data.ByteArray as BA
36−import qualified Data.ByteString as B
37−import qualified Data.ByteString.Lazy as LB
38−import Data.Default.Class (def)
39−import qualified Data.HashMap.Strict as H
40−import qualified Data.Ini as Ini
41−import qualified Data.List as List
42−import qualified Data.Text as T
43−import qualified Data.Text.Encoding as TE
44−import Data.Time (defaultTimeLocale, formatTime)
45−import Lib.Prelude (UTCTime, throwIO)
46−import qualified Network.Connection as Conn
47−import Network.HTTP.Client (defaultManagerSettings)
48−import qualified Network.HTTP.Client.TLS as TLS
49−import qualified Network.HTTP.Conduit as NC
50−import Network.HTTP.Types
51− ( ByteRange,
52− Header,
53− Method,
54− Query,
55− hRange,
56− )
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)
64−import Network.Minio.Errors
65− ( MErrV (MErrVInvalidEncryptionKeyLength, MErrVMissingCredentials),
66− MinioErr (..),
67− )
68−import Network.Minio.Utils
69−import System.Directory (doesFileExist, getHomeDirectory)
70−import qualified System.Environment as Env
71−import System.FilePath.Posix (combine)
72−import qualified UnliftIO as U
73−import qualified UnliftIO.MVar as UM
23+import qualified Conduit as C
24+import Control.Concurrent.MVar (MVar)
25+import qualified Control.Concurrent.MVar as M
26+import Control.Monad.IO.Unlift (MonadUnliftIO, UnliftIO (..),
27+ askUnliftIO, withUnliftIO)
28+import Control.Monad.Trans.Resource
29+import qualified Data.Aeson as A
30+import qualified Data.ByteArray as BA
31+import qualified Data.ByteString as B
32+import Data.CaseInsensitive (mk)
33+import qualified Data.HashMap.Strict as H
34+import qualified Data.Ini as Ini
35+import qualified Data.Map as Map
36+import Data.String (IsString (..))
37+import qualified Data.Text as T
38+import qualified Data.Text.Encoding as TE
39+import Data.Time (defaultTimeLocale, formatTime)
40+import GHC.Show (Show (show))
41+import qualified Network.Connection as Conn
42+import Network.HTTP.Client (defaultManagerSettings)
43+import qualified Network.HTTP.Client.TLS as TLS
44+import qualified Network.HTTP.Conduit as NC
45+import Network.HTTP.Types (ByteRange, Header, Method, Query,
46+ hRange)
47+import qualified Network.HTTP.Types as HT
48+import Network.Minio.Errors
49+import System.Directory (doesFileExist, getHomeDirectory)
50+import qualified System.Environment as Env
51+import System.FilePath.Posix (combine)
52+import Text.XML
53+import qualified UnliftIO as U
54+
55+import Lib.Prelude
56+import Network.Minio.Data.Crypto
7457
7558-- | max obj size is 5TiB
7659maxObjectSize :: Int64
@@ -90,157 +73,124 @@ maxMultipartParts = 10000
9073-- TODO: Add a type which provides typed constants for region. this
9174-- type should have a IsString instance to infer the appropriate
9275-- constant.
93−
9476-- | awsRegionMap - library constant
95−awsRegionMap :: H.HashMap Text Text
96−awsRegionMap =
97− H.fromList
98− [ ("us-east-1", "s3.us-east-1.amazonaws.com"),
99− ("us-east-2", "s3.us-east-2.amazonaws.com"),
100− ("us-west-1", "s3.us-west-1.amazonaws.com"),
101− ("us-west-2", "s3.us-west-2.amazonaws.com"),
102− ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
103− ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
104− ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
105− ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
106− ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
107− ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
108− ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
109− ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
110− ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
111− ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
112− ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
113− ("eu-west-3", "s3.eu-west-3.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"),
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")
128− ]
77+awsRegionMap :: Map.Map Text Text
78+awsRegionMap = Map.fromList [
79+ ("us-east-1", "s3.amazonaws.com")
80+ , ("us-east-2", "s3-us-east-2.amazonaws.com")
81+ , ("us-west-1", "s3-us-west-1.amazonaws.com")
82+ , ("us-east-2", "s3-us-west-2.amazonaws.com")
83+ , ("ca-central-1", "s3-ca-central-1.amazonaws.com")
84+ , ("ap-south-1", "s3-ap-south-1.amazonaws.com")
85+ , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
86+ , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
87+ , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
88+ , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
89+ , ("eu-west-1", "s3-eu-west-1.amazonaws.com")
90+ , ("eu-west-2", "s3-eu-west-2.amazonaws.com")
91+ , ("eu-central-1", "s3-eu-central-1.amazonaws.com")
92+ , ("sa-east-1", "s3-sa-east-1.amazonaws.com")
93+ ]
12994
13095-- | Connection Info data type. To create a 'ConnectInfo' value,
13196-- enable the @OverloadedStrings@ language extension and use the
13297-- `IsString` instance to provide a URL, for example:
13398--
134−-- > let c :: ConnectInfo = "https://play.min.io"
135−data ConnectInfo = ConnectInfo
136− { connectHost :: Text,
137− connectPort :: Int,
138− connectCreds :: Creds,
139− connectIsSecure :: Bool,
140− connectRegion :: Region,
141− connectAutoDiscoverRegion :: Bool,
142− connectDisableTLSCertValidation :: Bool
143− }
144−
145−getEndpoint :: ConnectInfo -> Endpoint
146−getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
99+-- > let c :: ConnectInfo = "https://play.min.io:9000"
100+data ConnectInfo =
101+ ConnectInfo { connectHost :: Text
102+ , connectPort :: Int
103+ , connectAccessKey :: Text
104+ , connectSecretKey :: Text
105+ , connectIsSecure :: Bool
106+ , connectRegion :: Region
107+ , connectAutoDiscoverRegion :: Bool
108+ , connectDisableTLSCertValidation :: Bool
109+ } deriving (Eq, Show)
147110
148111instance IsString ConnectInfo where
149− fromString str =
150− let req = NC.parseRequest_ str
151− in ConnectInfo
152− { connectHost = TE.decodeUtf8 $ NC.host req,
153− connectPort = NC.port req,
154− connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
155− connectIsSecure = NC.secure req,
156− connectRegion = "",
157− connectAutoDiscoverRegion = True,
158− connectDisableTLSCertValidation = False
159− }
160−
161−-- | A 'CredentialLoader' is an action that may return a 'CredentialValue'.
162−-- Loaders may be chained together using 'findFirst'.
163−--
164−-- @since 1.7.0
165−type CredentialLoader = IO (Maybe CredentialValue)
166−
167−-- | Combines the given list of loaders, by calling each one in
168−-- order until a 'CredentialValue' is returned.
169−findFirst :: [CredentialLoader] -> IO (Maybe CredentialValue)
112+ fromString str =
113+ let req = NC.parseRequest_ str
114+ in ConnectInfo
115+ { connectHost = TE.decodeUtf8 $ NC.host req
116+ , connectPort = NC.port req
117+ , connectAccessKey = ""
118+ , connectSecretKey = ""
119+ , connectIsSecure = NC.secure req
120+ , connectRegion = ""
121+ , connectAutoDiscoverRegion = True
122+ , connectDisableTLSCertValidation = False
123+ }
124+
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
131+type Provider = IO (Maybe Credentials)
132+
133+-- | Combines the given list of providers, by calling each one in
134+-- order until Credentials are found.
135+findFirst :: [Provider] -> Provider
170136findFirst [] = return Nothing
171−findFirst (f : fs) = do
172− c <- f
173− maybe (findFirst fs) (return . Just) c
137+findFirst (f:fs) = do c <- f
138+ maybe (findFirst fs) (return . Just) c
174139
175−-- | This action returns a 'CredentialValue' populated from
176−-- @~\/.aws\/credentials@
177−fromAWSConfigFile :: CredentialLoader
140+-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
141+fromAWSConfigFile :: Provider
178142fromAWSConfigFile = do
179− credsE <- runExceptT $ do
180− homeDir <- lift getHomeDirectory
181− let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
182− fileExists <- lift $ doesFileExist awsCredsFile
183− bool (throwE "FileNotFound") (return ()) fileExists
184− ini <- ExceptT $ Ini.readIniFile awsCredsFile
185− akey <-
186− ExceptT $
187− return $
188− Ini.lookupValue "default" "aws_access_key_id" ini
189− skey <-
190− ExceptT $
191− return $
192− Ini.lookupValue "default" "aws_secret_access_key" ini
193− return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
194− return $ either (const Nothing) Just credsE
195−
196−-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
197−-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
198−fromAWSEnv :: CredentialLoader
143+ credsE <- runExceptT $ do
144+ homeDir <- lift $ getHomeDirectory
145+ let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
146+ fileExists <- lift $ doesFileExist awsCredsFile
147+ bool (throwE "FileNotFound") (return ()) fileExists
148+ ini <- ExceptT $ Ini.readIniFile awsCredsFile
149+ akey <- ExceptT $ return
150+ $ Ini.lookupValue "default" "aws_access_key_id" ini
151+ skey <- ExceptT $ return
152+ $ Ini.lookupValue "default" "aws_secret_access_key" ini
153+ return $ Credentials akey skey
154+ return $ hush credsE
155+
156+-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
157+-- @AWS_SECRET_ACCESS_KEY@ environment variables.
158+fromAWSEnv :: Provider
199159fromAWSEnv = runMaybeT $ do
200− akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
201− skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
202− return $ CredentialValue (fromString akey) (fromString skey) Nothing
160+ akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
161+ skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
162+ return $ Credentials (T.pack akey) (T.pack skey)
203163
204−-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
205−-- and @MINIO_SECRET_KEY@ environment variables.
206−fromMinioEnv :: CredentialLoader
164+-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
165+-- @MINIO_SECRET_KEY@ environment variables.
166+fromMinioEnv :: Provider
207167fromMinioEnv = runMaybeT $ do
208− akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
209− skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
210− return $ CredentialValue (fromString akey) (fromString skey) Nothing
211−
212−-- | setCredsFrom retrieves access credentials from the first action in the
213−-- given list that succeeds and sets it in the 'ConnectInfo'.
214−setCredsFrom :: [CredentialLoader] -> ConnectInfo -> IO ConnectInfo
215−setCredsFrom ps ci = do
216− pMay <- findFirst ps
217− maybe
218− (throwIO MErrVMissingCredentials)
219− (return . (`setCreds` ci))
220− pMay
221−
222−-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
223−setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
224−setCreds cv connInfo =
225− connInfo
226− { connectCreds = CredsStatic cv
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}
168+ akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
169+ skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
170+ return $ Credentials (T.pack akey) (T.pack skey)
171+
172+-- | setCredsFrom retrieves access credentials from the first
173+-- `Provider` form the given list that succeeds and sets it in the
174+-- `ConnectInfo`.
175+setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo
176+setCredsFrom ps ci = do pMay <- findFirst ps
177+ maybe
178+ (throwIO MErrVMissingCredentials)
179+ (return . (flip setCreds ci))
180+ pMay
181+
182+-- | setCreds sets the given `Credentials` in the `ConnectInfo`.
183+setCreds :: Credentials -> ConnectInfo -> ConnectInfo
184+setCreds (Credentials accessKey secretKey) connInfo =
185+ connInfo { connectAccessKey = accessKey
186+ , connectSecretKey = secretKey
187+ }
236188
237189-- | Set the S3 region parameter in the `ConnectInfo`
238190setRegion :: Region -> ConnectInfo -> ConnectInfo
239−setRegion r connInfo =
240− connInfo
241− { connectRegion = r,
242− connectAutoDiscoverRegion = False
243− }
191+setRegion r connInfo = connInfo { connectRegion = r
192+ , connectAutoDiscoverRegion = False
193+ }
244194
245195-- | Check if the connection to object storage server is secure
246196-- (i.e. uses TLS)
@@ -252,37 +202,39 @@ isConnectInfoSecure = connectIsSecure
252202-- certificates. Note that this option has no effect, if you provide
253203-- your own Manager in `mkMinioConn`.
254204disableTLSCertValidation :: ConnectInfo -> ConnectInfo
255−disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
205+disableTLSCertValidation c = c { connectDisableTLSCertValidation = True }
256206
257207getHostAddr :: ConnectInfo -> ByteString
258−getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
208+getHostAddr ci = if | port == 80 || port == 443 -> toS host
209+ | otherwise -> toS $
210+ T.concat [ host, ":" , Lib.Prelude.show port]
211+ where
212+ port = connectPort ci
213+ host = connectHost ci
259214
260−-- | Default Google Compute Storage ConnectInfo. Works only for
261−-- "Simple Migration" use-case with interoperability mode enabled on
262−-- GCP console. For more information -
263−-- https://cloud.google.com/storage/docs/migrating
264−--
215+
216+-- | Default GCS ConnectInfo. Works only for "Simple Migration"
217+-- use-case with interoperability mode enabled on GCP console. For
218+-- more information - https://cloud.google.com/storage/docs/migrating
265219-- Credentials should be supplied before use.
266220gcsCI :: ConnectInfo
267−gcsCI =
268− setRegion
269− "us"
270− "https://storage.googleapis.com"
221+gcsCI = setRegion "us"
222+ "https://storage.googleapis.com"
223+
271224
272−-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
225+-- | Default AWS ConnectInfo. Connects to "us-east-1". Credentials
273226-- should be supplied before use.
274227awsCI :: ConnectInfo
275228awsCI = "https://s3.amazonaws.com"
276229
277−-- | <https://play.min.io MinIO Play Server>
230+
231+-- | <https://play.min.io:9000 MinIO Play Server>
278232-- ConnectInfo. Credentials are already filled in.
279233minioPlayCI :: ConnectInfo
280−minioPlayCI =
281− let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
282− in setCreds playCreds $
283− setRegion
284− "us-east-1"
285− "https://play.min.io"
234+minioPlayCI = let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
235+ in setCreds playCreds
236+ $ setRegion "us-east-1"
237+ "https://play.min.io:9000"
286238
287239-- |
288240-- Represents a bucket in the object store
@@ -292,7 +244,9 @@ type Bucket = Text
292244-- Represents an object name
293245type Object = Text
294246
295−-- | Represents a region
247+-- |
248+-- Represents a region
249+-- TODO: This could be a Sum Type with all defined regions for AWS.
296250type Region = Text
297251
298252-- | A type alias to represent an Entity-Tag returned by S3-compatible APIs.
@@ -301,147 +255,127 @@ type ETag = Text
301255-- | Data type to represent an object encryption key. Create one using
302256-- the `mkSSECKey` function.
303257newtype SSECKey = SSECKey BA.ScrubbedBytes
304− deriving stock (Eq, Show)
258+ deriving (Eq, Show)
305259
306260-- | Validates that the given ByteString is 32 bytes long and creates
307261-- an encryption key.
308−mkSSECKey :: (MonadThrow m) => ByteString -> m SSECKey
309−mkSSECKey keyBytes
310− | B.length keyBytes /= 32 =
311− throwM MErrVInvalidEncryptionKeyLength
312− | otherwise =
313− return $ SSECKey $ BA.convert keyBytes
262+mkSSECKey :: MonadThrow m => ByteString -> m SSECKey
263+mkSSECKey keyBytes | B.length keyBytes /= 32 =
264+ throwM MErrVInvalidEncryptionKeyLength
265+ | otherwise =
266+ return $ SSECKey $ BA.convert keyBytes
314267
315268-- | Data type to represent Server-Side-Encryption settings
316269data SSE where
317− -- | Specifies SSE S3 encryption - server manages encryption keys
318− SSE :: SSE
319− -- | Specifies that KMS service should be used. The first argument
320− -- to the constructor is the Key Id to be used by the server (if
321− -- not specified, the default KMS key id is used). The second
322− -- argument is the optional KMS context that must have a
323− -- `A.ToJSON` instance - please refer to the AWS S3 documentation
324− -- for detailed information.
325− SSEKMS :: (A.ToJSON a) => Maybe ByteString -> Maybe a -> SSE
326− -- | Specifies server-side encryption with customer provided
327− -- key. The argument is the encryption key to be used.
328− SSEC :: SSECKey -> SSE
270+ -- | Specifies SSE S3 encryption - server manages encryption keys
271+ SSE :: SSE
272+ -- | 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
274+ -- not specified, the default KMS key id is used). The second
275+ -- argument is the optional KMS context that must have a
276+ -- `A.ToJSON` instance - please refer to the AWS S3 documentation
277+ -- for detailed information.
278+ SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
279+ -- | Specifies server-side encryption with customer provided
280+ -- key. The argument is the encryption key to be used.
281+ SSEC :: SSECKey -> SSE
329282
330283toPutObjectHeaders :: SSE -> [HT.Header]
331284toPutObjectHeaders sseArg =
332− let sseHeader = "x-amz-server-side-encryption"
333− sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
334− sseKmsContextHeader = sseHeader <> "-context"
335− ssecAlgo = sseHeader <> "-customer-algorithm"
336− ssecKey = sseHeader <> "-customer-key"
337− ssecKeyMD5 = ssecKey <> "-MD5"
338− in case sseArg of
339− SSE -> [(sseHeader, "AES256")]
340− SSEKMS keyIdMay ctxMay ->
341− [(sseHeader, "aws:kms")]
342− ++ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay
343− ++ maybe [] (\k -> [(sseKmsContextHeader, LB.toStrict $ A.encode k)]) ctxMay
344− SSEC (SSECKey sb) ->
345− [ (ssecAlgo, "AES256"),
346− (ssecKey, encodeToBase64 sb),
347− (ssecKeyMD5, hashMD5ToBase64 sb)
348− ]
285+ let sseHeader = "x-amz-server-side-encryption"
286+ sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
287+ sseKmsContextHeader = sseHeader <> "-context"
288+ ssecAlgo = sseHeader <> "-customer-algorithm"
289+ ssecKey = sseHeader <> "-customer-key"
290+ ssecKeyMD5 = ssecKey <> "-MD5"
291+
292+ in case sseArg of
293+ SSE -> [(sseHeader, "AES256")]
294+
295+ SSEKMS keyIdMay ctxMay ->
296+ [(sseHeader, "aws:kms")] ++
297+ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay ++
298+ maybe [] (\k -> [(sseKmsContextHeader, toS $ A.encode k)]) ctxMay
299+
300+ SSEC (SSECKey sb) ->
301+ [(ssecAlgo, "AES256"),
302+ (ssecKey, encodeToBase64 sb),
303+ (ssecKeyMD5, hashMD5ToBase64 sb)]
349304
350305-- | Data type for options in PutObject call. Start with the empty
351306-- `defaultPutObjectOptions` and use various the various poo*
352307-- accessors.
353−data PutObjectOptions = PutObjectOptions
354− { -- | Set a standard MIME type describing the format of the object.
355− pooContentType :: Maybe Text,
356− -- | Set what content encodings have been applied to the object and thus
357− -- what decoding mechanisms must be applied to obtain the media-type
358− -- referenced by the Content-Type header field.
359− pooContentEncoding :: Maybe Text,
360− -- | Set presentational information for the object.
361− pooContentDisposition :: Maybe Text,
362− -- | Set to specify caching behavior for the object along the
363− -- request/reply chain.
364− pooCacheControl :: Maybe Text,
365− -- | Set to describe the language(s) intended for the audience.
366− pooContentLanguage :: Maybe Text,
367− -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
368− -- performance needs, storage class is @STANDARD@ by default (i.e
369− -- when Nothing is passed).
370− pooStorageClass :: Maybe Text,
371− -- | Set user defined metadata to store with the object.
372− pooUserMetadata :: [(Text, Text)],
373− -- | Set number of worker threads used to upload an object.
374− pooNumThreads :: Maybe Word,
375− -- | Set object encryption parameters for the request.
376− pooSSE :: Maybe SSE
308+data PutObjectOptions = PutObjectOptions {
309+ -- | Set a standard MIME type describing the format of the object.
310+ pooContentType :: Maybe Text
311+ -- | Set what content encodings have been applied to the object and thus
312+ -- what decoding mechanisms must be applied to obtain the media-type
313+ -- referenced by the Content-Type header field.
314+ , pooContentEncoding :: Maybe Text
315+ -- | Set presentational information for the object.
316+ , pooContentDisposition :: Maybe Text
317+ -- | Set to specify caching behavior for the object along the
318+ -- request/reply chain.
319+ , pooCacheControl :: Maybe Text
320+ -- | Set to describe the language(s) intended for the audience.
321+ , pooContentLanguage :: Maybe Text
322+ -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
323+ -- performance needs, storage class is @STANDARD@ by default (i.e
324+ -- when Nothing is passed).
325+ , pooStorageClass :: Maybe Text
326+ -- | Set user defined metadata to store with the object.
327+ , pooUserMetadata :: [(Text, Text)]
328+ -- | Set number of worker threads used to upload an object.
329+ , pooNumThreads :: Maybe Word
330+ -- | Set object encryption parameters for the request.
331+ , pooSSE :: Maybe SSE
377332 }
378333
379334-- | Provide default `PutObjectOptions`.
380335defaultPutObjectOptions :: PutObjectOptions
381336defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
382337
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+
383347pooToHeaders :: PutObjectOptions -> [HT.Header]
384−pooToHeaders poo =
385− userMetadata
386− ++ mapMaybe tupToMaybe (zip names values)
387− ++ maybe [] toPutObjectHeaders (pooSSE poo)
348+pooToHeaders poo = userMetadata
349+ ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
350+ ++ maybe [] toPutObjectHeaders (pooSSE poo)
388351 where
389− tupToMaybe (k, Just v) = Just (k, v)
352+ tupToMaybe (k, Just v) = Just (k, v)
390353 tupToMaybe (_, Nothing) = Nothing
354+
391355 userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
392− names =
393− [ "content-type",
394− "content-encoding",
395− "content-disposition",
396− "content-language",
397− "cache-control",
398− "x-amz-storage-class"
399− ]
400− values =
401− map
402− (fmap encodeUtf8 . (poo &))
403− [ pooContentType,
404− pooContentEncoding,
405− pooContentDisposition,
406− pooContentLanguage,
407− pooCacheControl,
408− pooStorageClass
409− ]
356+
357+ names = ["content-type",
358+ "content-encoding",
359+ "content-disposition",
360+ "content-language",
361+ "cache-control",
362+ "x-amz-storage-class"]
363+ values = map (fmap encodeUtf8 . (poo &))
364+ [pooContentType, pooContentEncoding,
365+ pooContentDisposition, pooContentLanguage,
366+ pooCacheControl, pooStorageClass]
367+
410368
411369-- |
412370-- BucketInfo returned for list buckets call
413−data BucketInfo = BucketInfo
414− { biName :: Bucket,
415− biCreationDate :: UTCTime
416− }
417− deriving stock (Show, Eq)
371+data BucketInfo = BucketInfo {
372+ biName :: Bucket
373+ , biCreationDate :: UTCTime
374+ } deriving (Show, Eq)
418375
419376-- | A type alias to represent a part-number for multipart upload
420377type PartNumber = Int16
421378
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−
445379-- | A type alias to represent an upload-id for multipart upload
446380type UploadId = Text
447381
@@ -450,116 +384,88 @@ type PartTuple = (PartNumber, ETag)
450384
451385-- | Represents result from a listing of object parts of an ongoing
452386-- multipart upload.
453−data ListPartsResult = ListPartsResult
454− { lprHasMore :: Bool,
455− lprNextPart :: Maybe Int,
456− lprParts :: [ObjectPartInfo]
457− }
458− deriving stock (Show, Eq)
387+data ListPartsResult = ListPartsResult {
388+ lprHasMore :: Bool
389+ , lprNextPart :: Maybe Int
390+ , lprParts :: [ObjectPartInfo]
391+ } deriving (Show, Eq)
459392
460393-- | Represents information about an object part in an ongoing
461394-- multipart upload.
462−data ObjectPartInfo = ObjectPartInfo
463− { opiNumber :: PartNumber,
464− opiETag :: ETag,
465− opiSize :: Int64,
466− opiModTime :: UTCTime
467− }
468− deriving stock (Show, Eq)
395+data ObjectPartInfo = ObjectPartInfo {
396+ opiNumber :: PartNumber
397+ , opiETag :: ETag
398+ , opiSize :: Int64
399+ , opiModTime :: UTCTime
400+ } deriving (Show, Eq)
469401
470402-- | Represents result from a listing of incomplete uploads to a
471403-- bucket.
472−data ListUploadsResult = ListUploadsResult
473− { lurHasMore :: Bool,
474− lurNextKey :: Maybe Text,
475− lurNextUpload :: Maybe Text,
476− lurUploads :: [(Object, UploadId, UTCTime)],
477− lurCPrefixes :: [Text]
478− }
479− deriving stock (Show, Eq)
404+data ListUploadsResult = ListUploadsResult {
405+ lurHasMore :: Bool
406+ , lurNextKey :: Maybe Text
407+ , lurNextUpload :: Maybe Text
408+ , lurUploads :: [(Object, UploadId, UTCTime)]
409+ , lurCPrefixes :: [Text]
410+ } deriving (Show, Eq)
480411
481412-- | Represents information about a multipart upload.
482−data UploadInfo = UploadInfo
483− { uiKey :: Object,
484− uiUploadId :: UploadId,
485− uiInitTime :: UTCTime,
486− uiSize :: Int64
487− }
488− deriving stock (Show, Eq)
413+data UploadInfo = UploadInfo {
414+ uiKey :: Object
415+ , uiUploadId :: UploadId
416+ , uiInitTime :: UTCTime
417+ , uiSize :: Int64
418+ } deriving (Show, Eq)
489419
490420-- | Represents result from a listing of objects in a bucket.
491−data ListObjectsResult = ListObjectsResult
492− { lorHasMore :: Bool,
493− lorNextToken :: Maybe Text,
494− lorObjects :: [ObjectInfo],
495− lorCPrefixes :: [Text]
496− }
497− deriving stock (Show, Eq)
421+data ListObjectsResult = ListObjectsResult {
422+ lorHasMore :: Bool
423+ , lorNextToken :: Maybe Text
424+ , lorObjects :: [ObjectInfo]
425+ , lorCPrefixes :: [Text]
426+ } deriving (Show, Eq)
498427
499428-- | Represents result from a listing of objects version 1 in a bucket.
500−data ListObjectsV1Result = ListObjectsV1Result
501− { lorHasMore' :: Bool,
502− lorNextMarker :: Maybe Text,
503− lorObjects' :: [ObjectInfo],
504− lorCPrefixes' :: [Text]
505− }
506− deriving stock (Show, Eq)
429+data ListObjectsV1Result = ListObjectsV1Result {
430+ lorHasMore' :: Bool
431+ , lorNextMarker :: Maybe Text
432+ , lorObjects' :: [ObjectInfo]
433+ , lorCPrefixes' :: [Text]
434+ } deriving (Show, Eq)
507435
508436-- | Represents information about an object.
509437data ObjectInfo = ObjectInfo
510− { -- | Object key
511− oiObject :: Object,
512− -- | Modification time of the object
513− oiModTime :: UTCTime,
514− -- | ETag of the object
515− oiETag :: ETag,
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,
524− -- | A map of metadata
525− -- key-value pairs (not
526− -- including the
527− -- user-metadata pairs)
528− oiMetadata :: H.HashMap Text Text
529− }
530− deriving stock (Show, Eq)
438+ { oiObject :: Object -- ^ Object key
439+ , oiModTime :: UTCTime -- ^ Mdification time of the object
440+ , oiETag :: ETag -- ^ ETag of the object
441+ , oiSize :: Int64 -- ^ Size of the object in bytes
442+ , oiMetadata :: Map.Map Text Text -- ^ A map of the metadata
443+ -- key-value pairs
444+ } deriving (Show, Eq)
531445
532446-- | Represents source object in server-side copy object
533447data SourceInfo = SourceInfo
534− { -- | Bucket containing the source object
535− srcBucket :: Text,
536− -- | Source object key
537− srcObject :: Text,
538− -- | Source object
539− -- byte-range
540− -- (inclusive)
541− srcRange :: Maybe (Int64, Int64),
542− -- | ETag condition on source -
543− -- object is copied only if the
544− -- source object's ETag matches
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.
551− srcIfNoneMatch :: Maybe Text,
552− -- | Copy source object only
553− -- if the source has been
554− -- modified since this time.
555− srcIfModifiedSince :: Maybe UTCTime,
556− -- | Copy source object only
557− -- if the source has been
558− -- un-modified since this
559− -- given time.
560− srcIfUnmodifiedSince :: Maybe UTCTime
561− }
562− deriving stock (Show, Eq)
448+ { srcBucket :: Text -- ^ Bucket containing the source object
449+ , srcObject :: Text -- ^ Source object key
450+ , srcRange :: Maybe (Int64, Int64) -- ^ Source object
451+ -- byte-range
452+ -- (inclusive)
453+ , srcIfMatch :: Maybe Text -- ^ ETag condition on source -
454+ -- object is copied only if the
455+ -- source object's ETag matches
456+ -- this value.
457+ , srcIfNoneMatch :: Maybe Text -- ^ ETag not match condition
458+ -- on source - object is copied
459+ -- if ETag does not match this
460+ -- value.
461+ , srcIfModifiedSince :: Maybe UTCTime -- ^ Copy source object only
462+ -- if the source has been
463+ -- modified since this time.
464+ , srcIfUnmodifiedSince :: Maybe UTCTime -- ^ Copy source object only
465+ -- if the source has been
466+ -- un-modified since this
467+ -- given time.
468+ } deriving (Show, Eq)
563469
564470-- | Provide a default for `SourceInfo`
565471defaultSourceInfo :: SourceInfo
@@ -567,12 +473,9 @@ defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing
567473
568474-- | Represents destination object in server-side copy object
569475data DestinationInfo = DestinationInfo
570− { -- | Destination bucket
571− dstBucket :: Text,
572− -- | Destination object key
573− dstObject :: Text
574− }
575− deriving stock (Show, Eq)
476+ { dstBucket :: Text -- ^ Destination bucket
477+ , dstObject :: Text -- ^ Destination object key
478+ } deriving (Show, Eq)
576479
577480-- | Provide a default for `DestinationInfo`
578481defaultDestinationInfo :: DestinationInfo
@@ -581,106 +484,88 @@ defaultDestinationInfo = DestinationInfo "" ""
581484-- | Data type for options when getting an object from the
582485-- service. Start with the empty `defaultGetObjectOptions` and modify
583486-- it using the goo* functions.
584−data GetObjectOptions = GetObjectOptions
585− { -- | Set object's data of given offset begin and end,
487+data GetObjectOptions = GetObjectOptions {
488+ -- | Set object's data of given offset begin and end,
586489 -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
587− gooRange :: Maybe ByteRange,
490+ gooRange :: Maybe ByteRange
588491 -- | Set matching ETag condition, GetObject which matches the following
589492 -- ETag.
590− gooIfMatch :: Maybe ETag,
493+ , gooIfMatch :: Maybe ETag
591494 -- | Set matching ETag none condition, GetObject which does not match
592495 -- the following ETag.
593− gooIfNoneMatch :: Maybe ETag,
496+ , gooIfNoneMatch :: Maybe ETag
594497 -- | Set object unmodified condition, GetObject unmodified since given time.
595− gooIfUnmodifiedSince :: Maybe UTCTime,
498+ , gooIfUnmodifiedSince :: Maybe UTCTime
596499 -- | Set object modified condition, GetObject modified since given time.
597− gooIfModifiedSince :: Maybe UTCTime,
500+ , gooIfModifiedSince :: Maybe UTCTime
598501 -- | Specify SSE-C key
599− gooSSECKey :: Maybe SSECKey
502+ , gooSSECKey :: Maybe SSECKey
600503 }
601504
602505-- | Provide default `GetObjectOptions`.
603506defaultGetObjectOptions :: GetObjectOptions
604507defaultGetObjectOptions =
605− GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
508+ GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
606509
607510gooToHeaders :: GetObjectOptions -> [HT.Header]
608−gooToHeaders goo =
609− rangeHdr
610− ++ zip names values
611− ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
511+gooToHeaders goo = rangeHdr ++ zip names values
512+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
612513 where
613− names =
614− [ "If-Match",
615− "If-None-Match",
616− "If-Unmodified-Since",
617− "If-Modified-Since"
618− ]
619− values =
620− mapMaybe
621− (fmap encodeUtf8 . (goo &))
622− [ gooIfMatch,
623− gooIfNoneMatch,
624− fmap formatRFC1123 . gooIfUnmodifiedSince,
625− fmap formatRFC1123 . gooIfModifiedSince
626− ]
627− rangeHdr =
628− maybe [] (\a -> [(hRange, HT.renderByteRanges [a])]) $
629− gooRange goo
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− }
514+ names = ["If-Match",
515+ "If-None-Match",
516+ "If-Unmodified-Since",
517+ "If-Modified-Since"]
518+ values = mapMaybe (fmap encodeUtf8 . (goo &))
519+ [gooIfMatch, gooIfNoneMatch,
520+ fmap formatRFC1123 . gooIfUnmodifiedSince,
521+ fmap formatRFC1123 . gooIfModifiedSince]
522+ rangeHdr = maybe [] (\a -> [(hRange, HT.renderByteRanges [a])])
523+ $ gooRange goo
524+
639525
640526-- | A data-type for events that can occur in the object storage
641527-- server. Reference:
642528-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
643−data Event
644− = ObjectCreated
645− | ObjectCreatedPut
646− | ObjectCreatedPost
647− | ObjectCreatedCopy
648− | ObjectCreatedMultipartUpload
649− | ObjectRemoved
650− | ObjectRemovedDelete
651− | ObjectRemovedDeleteMarkerCreated
652− | ReducedRedundancyLostObject
653− deriving stock (Eq, Show)
654−
655−instance ToText Event where
656− toText ObjectCreated = "s3:ObjectCreated:*"
657− toText ObjectCreatedPut = "s3:ObjectCreated:Put"
658− toText ObjectCreatedPost = "s3:ObjectCreated:Post"
659− toText ObjectCreatedCopy = "s3:ObjectCreated:Copy"
660− toText ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
661− toText ObjectRemoved = "s3:ObjectRemoved:*"
662− toText ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
663− toText ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
664− toText ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
529+data Event = ObjectCreated
530+ | ObjectCreatedPut
531+ | ObjectCreatedPost
532+ | ObjectCreatedCopy
533+ | ObjectCreatedMultipartUpload
534+ | ObjectRemoved
535+ | ObjectRemovedDelete
536+ | ObjectRemovedDeleteMarkerCreated
537+ | ReducedRedundancyLostObject
538+ deriving (Eq)
539+
540+instance Show Event where
541+ show ObjectCreated = "s3:ObjectCreated:*"
542+ show ObjectCreatedPut = "s3:ObjectCreated:Put"
543+ show ObjectCreatedPost = "s3:ObjectCreated:Post"
544+ show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
545+ show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
546+ show ObjectRemoved = "s3:ObjectRemoved:*"
547+ show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
548+ show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
549+ show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
665550
666551textToEvent :: Text -> Maybe Event
667552textToEvent t = case t of
668− "s3:ObjectCreated:*" -> Just ObjectCreated
669− "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
670− "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
671− "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
672− "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
673− "s3:ObjectRemoved:*" -> Just ObjectRemoved
674− "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
553+ "s3:ObjectCreated:*" -> Just ObjectCreated
554+ "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
555+ "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
556+ "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
557+ "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
558+ "s3:ObjectRemoved:*" -> Just ObjectRemoved
559+ "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
675560 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
676− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
677− _ -> Nothing
561+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
562+ _ -> Nothing
563+
678564
679565-- | Filter data type - part of notification configuration
680−newtype Filter = Filter
566+data Filter = Filter
681567 { fFilter :: FilterKey
682− }
683− deriving stock (Show, Eq)
568+ } deriving (Show, Eq)
684569
685570-- | defaultFilter is empty, used to create a notification
686571-- configuration.
@@ -688,10 +573,9 @@ defaultFilter :: Filter
688573defaultFilter = Filter defaultFilterKey
689574
690575-- | FilterKey contains FilterRules, and is part of a Filter.
691−newtype FilterKey = FilterKey
576+data FilterKey = FilterKey
692577 { fkKey :: FilterRules
693− }
694− deriving stock (Show, Eq)
578+ } deriving (Show, Eq)
695579
696580-- | defaultFilterKey is empty, used to create notification
697581-- configuration.
@@ -699,16 +583,16 @@ defaultFilterKey :: FilterKey
699583defaultFilterKey = FilterKey defaultFilterRules
700584
701585-- | FilterRules represents a collection of `FilterRule`s.
702−newtype FilterRules = FilterRules
586+data FilterRules = FilterRules
703587 { frFilterRules :: [FilterRule]
704− }
705− deriving stock (Show, Eq)
588+ } deriving (Show, Eq)
706589
707590-- | defaultFilterRules is empty, used to create notification
708591-- configuration.
709592defaultFilterRules :: FilterRules
710593defaultFilterRules = FilterRules []
711594
595+
712596-- | A filter rule that can act based on the suffix or prefix of an
713597-- object. As an example, let's create two filter rules:
714598--
@@ -719,10 +603,9 @@ defaultFilterRules = FilterRules []
719603-- for objects having a suffix of ".jpg", and the @prefixRule@
720604-- restricts it to objects having a prefix of "images/".
721605data FilterRule = FilterRule
722− { frName :: Text,
723− frValue :: Text
724− }
725− deriving stock (Show, Eq)
606+ { frName :: Text
607+ , frValue :: Text
608+ } deriving (Show, Eq)
726609
727610-- | Arn is an alias of Text
728611type Arn = Text
@@ -731,12 +614,11 @@ type Arn = Text
731614-- notification system. It could represent a Queue, Topic or Lambda
732615-- Function configuration.
733616data NotificationConfig = NotificationConfig
734− { ncId :: Text,
735− ncArn :: Arn,
736− ncEvents :: [Event],
737− ncFilter :: Filter
738− }
739− deriving stock (Show, Eq)
617+ { ncId :: Text
618+ , ncArn :: Arn
619+ , ncEvents :: [Event]
620+ , ncFilter :: Filter
621+ } deriving (Show, Eq)
740622
741623-- | A data-type to represent bucket notification configuration. It is
742624-- a collection of queue, topic or lambda function configurations. The
@@ -744,16 +626,16 @@ data NotificationConfig = NotificationConfig
744626-- described at
745627-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
746628data Notification = Notification
747− { nQueueConfigurations :: [NotificationConfig],
748− nTopicConfigurations :: [NotificationConfig],
749− nCloudFunctionConfigurations :: [NotificationConfig]
750− }
751− deriving stock (Show, Eq)
629+ { nQueueConfigurations :: [NotificationConfig]
630+ , nTopicConfigurations :: [NotificationConfig]
631+ , nCloudFunctionConfigurations :: [NotificationConfig]
632+ } deriving (Eq, Show)
752633
753634-- | The default notification configuration is empty.
754635defaultNotification :: Notification
755636defaultNotification = Notification [] [] []
756637
638+
757639--------------------------------------------------------------------------
758640-- Select API Related Types
759641--------------------------------------------------------------------------
@@ -761,42 +643,38 @@ defaultNotification = Notification [] [] []
761643-- | SelectRequest represents the Select API call. Use the
762644-- `selectRequest` function to create a value of this type.
763645data SelectRequest = SelectRequest
764− { srExpression :: Text,
765− srExpressionType :: ExpressionType,
766− srInputSerialization :: InputSerialization,
767− srOutputSerialization :: OutputSerialization,
768− srRequestProgressEnabled :: Maybe Bool
769− }
770− deriving stock (Show, Eq)
646+ { srExpression :: Text
647+ , srExpressionType :: ExpressionType
648+ , srInputSerialization :: InputSerialization
649+ , srOutputSerialization :: OutputSerialization
650+ , srRequestProgressEnabled :: Maybe Bool
651+ } deriving (Eq, Show)
771652
772653data ExpressionType = SQL
773− deriving stock (Show, Eq)
654+ deriving (Eq, Show)
774655
775656-- | InputSerialization represents format information of the input
776657-- object being queried. Use one of the smart constructors such as
777658-- `defaultCsvInput` as a starting value, and add compression info
778659-- using `setInputCompressionType`
779660data InputSerialization = InputSerialization
780− { isCompressionType :: Maybe CompressionType,
781− isFormatInfo :: InputFormatInfo
782− }
783− deriving stock (Show, Eq)
661+ { isCompressionType :: Maybe CompressionType
662+ , isFormatInfo :: InputFormatInfo
663+ } deriving (Eq, Show)
784664
785665-- | Data type representing the compression setting in a Select
786666-- request.
787−data CompressionType
788− = CompressionTypeNone
789− | CompressionTypeGzip
790− | CompressionTypeBzip2
791− deriving stock (Show, Eq)
667+data CompressionType = CompressionTypeNone
668+ | CompressionTypeGzip
669+ | CompressionTypeBzip2
670+ deriving (Eq, Show)
792671
793672-- | Data type representing input object format information in a
794673-- Select request.
795−data InputFormatInfo
796− = InputFormatCSV CSVInputProp
797− | InputFormatJSON JSONInputProp
798− | InputFormatParquet
799− deriving stock (Show, Eq)
674+data InputFormatInfo = InputFormatCSV CSVInputProp
675+ | InputFormatJSON JSONInputProp
676+ | InputFormatParquet
677+ deriving (Eq, Show)
800678
801679-- | defaultCsvInput returns InputSerialization with default CSV
802680-- format, and without any compression setting.
@@ -806,18 +684,14 @@ defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
806684-- | linesJsonInput returns InputSerialization with JSON line based
807685-- format with no compression setting.
808686linesJsonInput :: InputSerialization
809−linesJsonInput =
810− InputSerialization
811− Nothing
812− (InputFormatJSON $ JSONInputProp JSONTypeLines)
687+linesJsonInput = InputSerialization Nothing
688+ (InputFormatJSON $ JSONInputProp JSONTypeLines)
813689
814690-- | documentJsonInput returns InputSerialization with JSON document
815691-- based format with no compression setting.
816692documentJsonInput :: InputSerialization
817−documentJsonInput =
818− InputSerialization
819− Nothing
820− (InputFormatJSON $ JSONInputProp JSONTypeDocument)
693+documentJsonInput = InputSerialization Nothing
694+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)
821695
822696-- | defaultParquetInput returns InputSerialization with Parquet
823697-- format, and no compression setting.
@@ -826,14 +700,12 @@ defaultParquetInput = InputSerialization Nothing InputFormatParquet
826700
827701-- | setInputCompressionType sets the compression type for the input
828702-- of the SelectRequest
829−setInputCompressionType ::
830− CompressionType ->
831− SelectRequest ->
832− SelectRequest
703+setInputCompressionType :: CompressionType -> SelectRequest
704+ -> SelectRequest
833705setInputCompressionType c i =
834− let is = srInputSerialization i
835− is' = is {isCompressionType = Just c}
836− in i {srInputSerialization = is'}
706+ let is = srInputSerialization i
707+ is' = is { isCompressionType = Just c }
708+ in i { srInputSerialization = is' }
837709
838710-- | defaultCsvOutput returns OutputSerialization with default CSV
839711-- format.
@@ -850,42 +722,40 @@ defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
850722-- SelectRequest with the SQL query text given by @query@, the input
851723-- serialization settings (compression format and format information)
852724-- @inputSer@ and the output serialization settings @outputSer@.
853−selectRequest ::
854− Text ->
855− InputSerialization ->
856− OutputSerialization ->
857− SelectRequest
725+selectRequest :: Text -> InputSerialization -> OutputSerialization
726+ -> SelectRequest
858727selectRequest sqlQuery inputSer outputSer =
859− SelectRequest
860− { srExpression = sqlQuery,
861− srExpressionType = SQL,
862− srInputSerialization = inputSer,
863− srOutputSerialization = outputSer,
864− srRequestProgressEnabled = Nothing
865− }
728+ SelectRequest { srExpression = sqlQuery
729+ , srExpressionType = SQL
730+ , srInputSerialization = inputSer
731+ , srOutputSerialization = outputSer
732+ , srRequestProgressEnabled = Nothing
733+ }
866734
867735-- | setRequestProgressEnabled sets the flag for turning on progress
868736-- messages when the Select response is being streamed back to the
869737-- client.
870738setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
871739setRequestProgressEnabled enabled sr =
872− sr {srRequestProgressEnabled = Just enabled}
740+ sr { srRequestProgressEnabled = Just enabled }
873741
874742type CSVInputProp = CSVProp
875743
876744-- | CSVProp represents CSV format properties. It is built up using
877745-- the Monoid instance.
878−newtype CSVProp = CSVProp (H.HashMap Text Text)
879− deriving stock (Show, Eq)
746+data CSVProp = CSVProp (H.HashMap Text Text)
747+ deriving (Eq, Show)
880748
749+#if (__GLASGOW_HASKELL__ >= 804)
881750instance Semigroup CSVProp where
882− (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
751+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
752+#endif
883753
884754instance Monoid CSVProp where
885− mempty = CSVProp mempty
886−
887−csvPropsList :: CSVProp -> [(Text, Text)]
888−csvPropsList (CSVProp h) = sort $ H.toList h
755+ mempty = CSVProp mempty
756+#if (__GLASGOW_HASKELL__ < 804)
757+ mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
758+#endif
889759
890760defaultCSVProp :: CSVProp
891761defaultCSVProp = mempty
@@ -909,21 +779,18 @@ quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
909779-- | FileHeaderInfo specifies information about column headers for CSV
910780-- format.
911781data FileHeaderInfo
912− = -- | No column headers are present
913− FileHeaderNone
914− | -- | Headers are present and they should be used
915− FileHeaderUse
916− | -- | Header are present, but should be ignored
917− FileHeaderIgnore
918− deriving stock (Show, Eq)
782+ = FileHeaderNone -- ^ No column headers are present
783+ | FileHeaderUse -- ^ Headers are present and they should be used
784+ | FileHeaderIgnore -- ^ Header are present, but should be ignored
785+ deriving (Eq, Show)
919786
920787-- | Specify the CSV file header info property.
921788fileHeaderInfo :: FileHeaderInfo -> CSVProp
922−fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toStr
789+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString
923790 where
924− toStr FileHeaderNone = "NONE"
925− toStr FileHeaderUse = "USE"
926− toStr FileHeaderIgnore = "IGNORE"
791+ toString FileHeaderNone = "NONE"
792+ toString FileHeaderUse = "USE"
793+ toString FileHeaderIgnore = "IGNORE"
927794
928795-- | Specify the CSV comment character property. Lines starting with
929796-- this character are ignored by the server.
@@ -936,69 +803,64 @@ allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter"
936803
937804-- | Set the CSV format properties in the InputSerialization.
938805setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
939−setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
806+setInputCSVProps p is = is { isFormatInfo = InputFormatCSV p }
940807
941808-- | Set the CSV format properties in the OutputSerialization.
942809outputCSVFromProps :: CSVProp -> OutputSerialization
943−outputCSVFromProps = OutputSerializationCSV
810+outputCSVFromProps p = OutputSerializationCSV p
944811
945−newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
946− deriving stock (Show, Eq)
812+data JSONInputProp = JSONInputProp { jsonipType :: JSONType }
813+ deriving (Eq, Show)
947814
948815data JSONType = JSONTypeDocument | JSONTypeLines
949− deriving stock (Show, Eq)
816+ deriving (Eq, Show)
817+
950818
951819-- | OutputSerialization represents output serialization settings for
952820-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
953821-- a starting point.
954−data OutputSerialization
955− = OutputSerializationJSON JSONOutputProp
956− | OutputSerializationCSV CSVOutputProp
957− deriving stock (Show, Eq)
822+data OutputSerialization = OutputSerializationJSON JSONOutputProp
823+ | OutputSerializationCSV CSVOutputProp
824+ deriving (Eq, Show)
958825
959826type CSVOutputProp = CSVProp
960827
961828-- | quoteFields is an output serialization parameter
962829quoteFields :: QuoteFields -> CSVProp
963−quoteFields q = CSVProp $
964− H.singleton "QuoteFields" $
965− case q of
966− QuoteFieldsAsNeeded -> "ASNEEDED"
967− QuoteFieldsAlways -> "ALWAYS"
830+quoteFields q = CSVProp $ H.singleton "QuoteFields" $
831+ case q of
832+ QuoteFieldsAsNeeded -> "ASNEEDED"
833+ QuoteFieldsAlways -> "ALWAYS"
968834
969835-- | Represent the QuoteField setting.
970836data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
971− deriving stock (Show, Eq)
837+ deriving (Eq, Show)
972838
973−newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
974− deriving stock (Show, Eq)
839+data JSONOutputProp = JSONOutputProp { jsonopRecordDelimiter :: Maybe Text }
840+ deriving (Eq, Show)
975841
976842-- | Set the output record delimiter for JSON format
977843outputJSONFromRecordDelimiter :: Text -> OutputSerialization
978844outputJSONFromRecordDelimiter t =
979− OutputSerializationJSON (JSONOutputProp $ Just t)
845+ OutputSerializationJSON (JSONOutputProp $ Just t)
980846
981847-- Response related types
982848
983849-- | An EventMessage represents each kind of message received from the server.
984−data EventMessage
985− = ProgressEventMessage Progress
986− | StatsEventMessage Stats
987− | RequestLevelErrorMessage
988− Text
989− -- ^ Error code
990− Text
991− -- ^ Error message
992− | RecordPayloadEventMessage ByteString
993− deriving stock (Show, Eq)
994−
995−data MsgHeaderName
996− = MessageType
997− | EventType
998− | ContentType
999− | ErrorCode
1000− | ErrorMessage
1001− deriving stock (Show, Eq)
850+data EventMessage = ProgressEventMessage { emProgress :: Progress }
851+ | StatsEventMessage { emStats :: Stats }
852+ | RequestLevelErrorMessage { emErrorCode :: Text
853+ , emErrorMessage :: Text
854+ }
855+ | RecordPayloadEventMessage { emPayloadBytes :: ByteString }
856+ deriving (Eq, Show)
857+
858+data MsgHeaderName = MessageType
859+ | EventType
860+ | ContentType
861+ | ErrorCode
862+ | ErrorMessage
863+ deriving (Eq, Show)
1002864
1003865msgHeaderValueType :: Word8
1004866msgHeaderValueType = 7
@@ -1006,12 +868,11 @@ msgHeaderValueType = 7
1006868type MessageHeader = (MsgHeaderName, Text)
1007869
1008870-- | Represent the progress event returned in the Select response.
1009−data Progress = Progress
1010− { pBytesScanned :: Int64,
1011− pBytesProcessed :: Int64,
1012− pBytesReturned :: Int64
1013− }
1014− deriving stock (Show, Eq)
871+data Progress = Progress { pBytesScanned :: Int64
872+ , pBytesProcessed :: Int64
873+ , pBytesReturned :: Int64
874+ }
875+ deriving (Eq, Show)
1015876
1016877-- | Represent the stats event returned at the end of the Select
1017878-- response.
@@ -1024,89 +885,86 @@ type Stats = Progress
1024885-- | Represents different kinds of payload that are used with S3 API
1025886-- requests.
1026887data Payload
1027− = PayloadBS ByteString
1028− | PayloadH Handle Int64 Int64 -- file handle, offset and length
1029− | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
888+ = PayloadBS ByteString
889+ | PayloadH Handle Int64 Int64 -- file handle, offset and length
890+ | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
1030891
1031892defaultPayload :: Payload
1032893defaultPayload = PayloadBS ""
1033894
1034−data AdminReqInfo = AdminReqInfo
1035− { ariMethod :: Method,
1036− ariPayloadHash :: Maybe ByteString,
1037− ariPayload :: Payload,
1038− ariPath :: ByteString,
1039− ariHeaders :: [Header],
1040− ariQueryParams :: Query
895+data AdminReqInfo = AdminReqInfo {
896+ ariMethod :: Method
897+ , ariPayloadHash :: Maybe ByteString
898+ , ariPayload :: Payload
899+ , ariPath :: ByteString
900+ , ariHeaders :: [Header]
901+ , ariQueryParams :: Query
1041902 }
1042903
1043904data S3ReqInfo = S3ReqInfo
1044− { riMethod :: Method,
1045− riBucket :: Maybe Bucket,
1046− riObject :: Maybe Object,
1047− riQueryParams :: Query,
1048− riHeaders :: [Header],
1049− riPayload :: Payload,
1050− riPayloadHash :: Maybe ByteString,
1051− riRegion :: Maybe Region,
1052− riNeedsLocation :: Bool,
1053− riPresignExpirySecs :: Maybe UrlExpiry
905+ { riMethod :: Method
906+ , riBucket :: Maybe Bucket
907+ , riObject :: Maybe Object
908+ , riQueryParams :: Query
909+ , riHeaders :: [Header]
910+ , riPayload :: Payload
911+ , riPayloadHash :: Maybe ByteString
912+ , riRegion :: Maybe Region
913+ , riNeedsLocation :: Bool
1054914 }
1055915
1056916defaultS3ReqInfo :: S3ReqInfo
1057−defaultS3ReqInfo =
1058− S3ReqInfo
1059− HT.methodGet
1060− Nothing
1061− Nothing
1062− []
1063− []
1064− defaultPayload
1065− Nothing
1066− Nothing
1067− True
1068− Nothing
917+defaultS3ReqInfo = S3ReqInfo HT.methodGet Nothing Nothing
918+ [] [] defaultPayload Nothing Nothing True
1069919
1070920getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
1071921getS3Path b o =
1072− let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
1073− in B.concat ["/", B.intercalate "/" segments]
922+ let segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
923+ in
924+ 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
1074929
1075−type RegionMap = H.HashMap Bucket Region
930+type RegionMap = Map.Map Bucket Region
1076931
1077932-- | The Minio Monad - all computations accessing object storage
1078933-- happens in it.
1079−newtype Minio a = Minio
1080− { unMinio :: ReaderT MinioConn (ResourceT IO) a
934+newtype Minio a = Minio {
935+ unMinio :: ReaderT MinioConn (ResourceT IO) a
1081936 }
1082− deriving newtype
1083− ( Functor,
1084− Applicative,
1085− Monad,
1086− MonadIO,
1087− MonadReader MinioConn,
1088− MonadResource,
1089− MonadUnliftIO
937+ deriving (
938+ Functor
939+ , Applicative
940+ , Monad
941+ , MonadIO
942+ , MonadReader MinioConn
943+ , MonadResource
1090944 )
1091945
946+instance MonadUnliftIO Minio where
947+ askUnliftIO = Minio $ ReaderT $ \r ->
948+ withUnliftIO $ \u ->
949+ return (UnliftIO (unliftIO u . flip runReaderT r . unMinio))
950+
1092951-- | MinioConn holds connection info and a connection pool to allow
1093952-- for efficient resource re-use.
1094953data MinioConn = MinioConn
1095− { mcConnInfo :: ConnectInfo,
1096− mcConnManager :: NC.Manager,
1097− mcRegionMap :: MVar RegionMap
954+ { mcConnInfo :: ConnectInfo
955+ , mcConnManager :: NC.Manager
956+ , mcRegionMap :: MVar RegionMap
1098957 }
1099958
1100959class HasSvcNamespace env where
1101960 getSvcNamespace :: env -> Text
1102961
1103962instance HasSvcNamespace MinioConn where
1104− getSvcNamespace env =
1105− let host = connectHost $ mcConnInfo env
1106− in ( if host == "storage.googleapis.com"
1107− then "http://doc.s3.amazonaws.com/2006-03-01"
1108− else "http://s3.amazonaws.com/doc/2006-03-01/"
1109− )
963+ getSvcNamespace env = let host = connectHost $ mcConnInfo env
964+ in if | host == "storage.googleapis.com" ->
965+ "http://doc.s3.amazonaws.com/2006-03-01"
966+ | otherwise ->
967+ "http://s3.amazonaws.com/doc/2006-03-01/"
1110968
1111969-- | Takes connection information and returns a connection object to
1112970-- be passed to 'runMinio'. The returned value can be kept in the
@@ -1114,16 +972,11 @@ instance HasSvcNamespace MinioConn where
1114972-- object storage is accessed.
1115973connect :: ConnectInfo -> IO MinioConn
1116974connect ci = do
1117− let settings
1118− | connectIsSecure ci && connectDisableTLSCertValidation ci =
1119− let badTlsSettings =
1120− case def of
1121− tlsSettings@Conn.TLSSettingsSimple {} ->
1122− tlsSettings {Conn.settingDisableCertificateValidation = True}
1123− _ -> def
1124− in TLS.mkManagerSettings badTlsSettings Nothing
1125− | connectIsSecure ci = NC.tlsManagerSettings
1126− | otherwise = defaultManagerSettings
975+ let settings | connectIsSecure ci && connectDisableTLSCertValidation ci =
976+ let badTlsSettings = Conn.TLSSettingsSimple True False False
977+ in TLS.mkManagerSettings badTlsSettings Nothing
978+ | connectIsSecure ci = NC.tlsManagerSettings
979+ | otherwise = defaultManagerSettings
1127980 mgr <- NC.newManager settings
1128981 mkMinioConn ci mgr
1129982
@@ -1131,61 +984,35 @@ connect ci = do
1131984-- `MinioConn`. This reuses connections, but otherwise it is similar
1132985-- to `runMinio`.
1133986runMinioWith :: MinioConn -> Minio a -> IO (Either MinioErr a)
1134−runMinioWith conn m = runResourceT $ runMinioResWith conn m
987+runMinioWith conn m = runResourceT . flip runReaderT conn . unMinio $
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
1135999
11361000-- | Given `ConnectInfo` and a HTTP connection manager, create a
11371001-- `MinioConn`.
11381002mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
11391003mkMinioConn ci mgr = do
1140− rMapMVar <- M.newMVar H.empty
1141− return $ MinioConn ci mgr rMapMVar
1004+ rMapMVar <- M.newMVar Map.empty
1005+ return $ MinioConn ci mgr rMapMVar
11421006
11431007-- | Run the Minio action and return the result or an error.
11441008runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
11451009runMinio ci m = do
11461010 conn <- connect ci
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
1011+ runMinioWith conn m
11651012
1166−-- | Similar to 'runMinio'. Allows applications to allocate/release
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
1013+s3Name :: Text -> Text -> Name
1014+s3Name ns s = Name s (Just ns) Nothing
11721015
11731016-- | Format as per RFC 1123.
11741017formatRFC1123 :: UTCTime -> T.Text
11751018formatRFC1123 = 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+18−19
@@ -13,21 +13,23 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE FlexibleInstances #-}
1716
17+{-# LANGUAGE FlexibleInstances #-}
1818module Network.Minio.Data.ByteString
19− ( stripBS,
20− UriEncodable (..),
21− )
22−where
19+ (
20+ stripBS
21+ , UriEncodable(..)
22+ ) where
2323
2424import qualified Data.ByteString as B
2525import qualified Data.ByteString.Builder as BB
2626import qualified Data.ByteString.Char8 as BC8
2727import qualified Data.ByteString.Lazy as LB
28−import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace, toUpper)
28+import Data.Char (isSpace, toUpper, isAsciiUpper, isAsciiLower, isDigit)
2929import qualified Data.Text as T
30−import Numeric (showHex)
30+import Numeric (showHex)
31+
32+import Lib.Prelude
3133
3234stripBS :: ByteString -> ByteString
3335stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
@@ -37,10 +39,8 @@ class UriEncodable s where
3739
3840instance UriEncodable [Char] where
3941 uriEncode encodeSlash payload =
40− LB.toStrict $
41− BB.toLazyByteString $
42− mconcat $
43− map (`uriEncodeChar` encodeSlash) payload
42+ LB.toStrict $ BB.toLazyByteString $ mconcat $
43+ map (`uriEncodeChar` encodeSlash) payload
4444
4545instance UriEncodable ByteString where
4646 -- assumes that uriEncode is passed ASCII encoded strings.
@@ -59,17 +59,16 @@ uriEncodeChar '/' True = BB.byteString "%2F"
5959uriEncodeChar '/' False = BB.char7 '/'
6060uriEncodeChar ch _
6161 | isAsciiUpper ch
62− || isAsciiLower ch
63− || isDigit ch
64− || (ch == '_')
65− || (ch == '-')
66− || (ch == '.')
67− || (ch == '~') =
68− BB.char7 ch
62+ || isAsciiLower ch
63+ || isDigit ch
64+ || (ch == '_')
65+ || (ch == '-')
66+ || (ch == '.')
67+ || (ch == '~') = BB.char7 ch
6968 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
7069 where
7170 f :: Word8 -> BB.Builder
7271 f n = BB.char7 '%' <> BB.string7 hexStr
7372 where
7473 hexStr = map toUpper $ showHex q $ showHex r ""
75− (q, r) = divMod n (16 :: Word8)
74+ (q, r) = divMod (fromIntegral n) (16::Word8)
src/Network/Minio/Data/Crypto.hs+33−32
@@ -15,54 +15,55 @@
1515--
1616
1717module Network.Minio.Data.Crypto
18− ( hashSHA256,
19− hashSHA256FromSource,
20− hashMD5,
21− hashMD5ToBase64,
22− hashMD5FromSource,
23− hmacSHA256,
24− hmacSHA256RawBS,
25− digestToBS,
26− digestToBase16,
27− encodeToBase64,
28− )
29−where
30−
31−import Crypto.Hash
32− ( Digest,
33− MD5 (..),
34− SHA256 (..),
35− hashWith,
36− )
37−import Crypto.Hash.Conduit (sinkHash)
38−import Crypto.MAC.HMAC (HMAC, hmac)
39−import Data.ByteArray (ByteArrayAccess, convert)
40−import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
41−import qualified Data.Conduit as C
18+ (
19+ hashSHA256
20+ , hashSHA256FromSource
21+
22+ , hashMD5
23+ , hashMD5ToBase64
24+ , hashMD5FromSource
25+
26+ , hmacSHA256
27+ , hmacSHA256RawBS
28+ , digestToBS
29+ , digestToBase16
30+
31+ , encodeToBase64
32+ ) where
33+
34+import Crypto.Hash (Digest, MD5 (..), SHA256 (..),
35+ hashWith)
36+import Crypto.Hash.Conduit (sinkHash)
37+import Crypto.MAC.HMAC (HMAC, hmac)
38+import Data.ByteArray (ByteArrayAccess, convert)
39+import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
40+import qualified Data.Conduit as C
41+
42+import Lib.Prelude
4243
4344hashSHA256 :: ByteString -> ByteString
4445hashSHA256 = digestToBase16 . hashWith SHA256
4546
46−hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
47+hashSHA256FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
4748hashSHA256FromSource src = do
4849 digest <- C.connect src sinkSHA256Hash
4950 return $ digestToBase16 digest
5051 where
5152 -- To help with type inference
52− sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
53+ sinkSHA256Hash :: Monad m => C.ConduitM ByteString Void m (Digest SHA256)
5354 sinkSHA256Hash = sinkHash
5455
5556-- Returns MD5 hash hex encoded.
5657hashMD5 :: ByteString -> ByteString
5758hashMD5 = digestToBase16 . hashWith MD5
5859
59−hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
60+hashMD5FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
6061hashMD5FromSource src = do
6162 digest <- C.connect src sinkMD5Hash
6263 return $ digestToBase16 digest
6364 where
6465 -- To help with type inference
65− sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
66+ sinkMD5Hash :: Monad m => C.ConduitM ByteString Void m (Digest MD5)
6667 sinkMD5Hash = sinkHash
6768
6869hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -71,15 +72,15 @@ hmacSHA256 message key = hmac key message
7172hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
7273hmacSHA256RawBS message key = convert $ hmacSHA256 message key
7374
74−digestToBS :: (ByteArrayAccess a) => a -> ByteString
75+digestToBS :: ByteArrayAccess a => a -> ByteString
7576digestToBS = convert
7677
77−digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
78+digestToBase16 :: ByteArrayAccess a => a -> ByteString
7879digestToBase16 = convertToBase Base16
7980
8081-- Returns MD5 hash base 64 encoded.
81−hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
82+hashMD5ToBase64 :: ByteArrayAccess a => a -> ByteString
8283hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
8384
84−encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
85+encodeToBase64 :: ByteArrayAccess a => a -> ByteString
8586encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+13−17
@@ -15,24 +15,20 @@
1515--
1616
1717module Network.Minio.Data.Time
18− ( awsTimeFormat,
19− awsTimeFormatBS,
20− awsDateFormat,
21− awsDateFormatBS,
22− awsParseTime,
23− iso8601TimeFormat,
24− UrlExpiry,
25− )
26−where
27−
28−import Data.ByteString.Char8 (pack)
18+ (
19+ awsTimeFormat
20+ , awsTimeFormatBS
21+ , awsDateFormat
22+ , awsDateFormatBS
23+ , awsParseTime
24+ , iso8601TimeFormat
25+ ) where
26+
27+
28+import Data.ByteString.Char8 (pack)
2929import qualified Data.Time as Time
30−import Data.Time.Format.ISO8601 (iso8601Show)
31−import Lib.Prelude
3230
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
31+import Lib.Prelude
3632
3733awsTimeFormat :: UTCTime -> [Char]
3834awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
@@ -50,4 +46,4 @@ awsParseTime :: [Char] -> Maybe UTCTime
5046awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
5147
5248iso8601TimeFormat :: UTCTime -> [Char]
53−iso8601TimeFormat = iso8601Show
49+iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
src/Network/Minio/Errors.hs+52−59
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -14,83 +14,76 @@
1414-- limitations under the License.
1515--
1616
17−module Network.Minio.Errors
18− ( MErrV (..),
19− ServiceErr (..),
20− MinioErr (..),
21− toServiceErr,
22− )
23−where
17+module Network.Minio.Errors where
2418
25−import Control.Exception (IOException)
19+import Control.Exception
2620import qualified Network.HTTP.Conduit as NC
2721
22+import Lib.Prelude
23+
24+
2825---------------------------------
2926-- Errors
3027---------------------------------
31−
3228-- | Various validation errors
33−data MErrV
34− = MErrVSinglePUTSizeExceeded Int64
35− | MErrVPutSizeExceeded Int64
36− | MErrVETagHeaderNotFound
37− | MErrVInvalidObjectInfoResponse
38− | MErrVInvalidSrcObjSpec Text
39− | MErrVInvalidSrcObjByteRange (Int64, Int64)
40− | MErrVCopyObjSingleNoRangeAccepted
41− | MErrVRegionNotSupported Text
42− | MErrVXmlParse Text
43− | MErrVInvalidBucketName Text
44− | MErrVInvalidObjectName Text
45− | MErrVInvalidUrlExpiry Int
46− | MErrVJsonParse Text
47− | MErrVInvalidHealPath
48− | MErrVMissingCredentials
49− | MErrVInvalidEncryptionKeyLength
50− | MErrVStreamingBodyUnexpectedEOF
51− | MErrVUnexpectedPayload
52− | MErrVSTSEndpointNotFound
53− deriving stock (Show, Eq)
29+data MErrV = MErrVSinglePUTSizeExceeded Int64
30+ | MErrVPutSizeExceeded Int64
31+ | MErrVETagHeaderNotFound
32+ | MErrVInvalidObjectInfoResponse
33+ | MErrVInvalidSrcObjSpec Text
34+ | MErrVInvalidSrcObjByteRange (Int64, Int64)
35+ | MErrVCopyObjSingleNoRangeAccepted
36+ | MErrVRegionNotSupported Text
37+ | MErrVXmlParse Text
38+ | MErrVInvalidBucketName Text
39+ | MErrVInvalidObjectName Text
40+ | MErrVInvalidUrlExpiry Int
41+ | MErrVJsonParse Text
42+ | MErrVInvalidHealPath
43+ | MErrVMissingCredentials
44+ | MErrVInvalidEncryptionKeyLength
45+ | MErrVStreamingBodyUnexpectedEOF
46+ | MErrVUnexpectedPayload
47+ deriving (Show, Eq)
5448
5549instance Exception MErrV
5650
5751-- | Errors returned by S3 compatible service
58−data ServiceErr
59− = BucketAlreadyExists
60− | BucketAlreadyOwnedByYou
61− | NoSuchBucket
62− | InvalidBucketName
63− | NoSuchKey
64− | SelectErr Text Text
65− | ServiceErr Text Text
66− deriving stock (Show, Eq)
52+data ServiceErr = BucketAlreadyExists
53+ | BucketAlreadyOwnedByYou
54+ | NoSuchBucket
55+ | InvalidBucketName
56+ | NoSuchKey
57+ | SelectErr Text Text
58+ | ServiceErr Text Text
59+ deriving (Show, Eq)
6760
6861instance Exception ServiceErr
6962
7063toServiceErr :: Text -> Text -> ServiceErr
71−toServiceErr "NoSuchKey" _ = NoSuchKey
72−toServiceErr "NoSuchBucket" _ = NoSuchBucket
73−toServiceErr "InvalidBucketName" _ = InvalidBucketName
64+toServiceErr "NoSuchKey" _ = NoSuchKey
65+toServiceErr "NoSuchBucket" _ = NoSuchBucket
66+toServiceErr "InvalidBucketName" _ = InvalidBucketName
7467toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
75−toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
76−toServiceErr code message = ServiceErr code message
68+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
69+toServiceErr code message = ServiceErr code message
70+
7771
7872-- | Errors thrown by the library
79−data MinioErr
80− = MErrHTTP NC.HttpException
81− | MErrIO IOException
82− | MErrService ServiceErr
83− | MErrValidation MErrV
84− deriving stock (Show)
73+data MinioErr = MErrHTTP NC.HttpException
74+ | MErrIO IOException
75+ | MErrService ServiceErr
76+ | MErrValidation MErrV
77+ deriving (Show)
8578
8679instance Eq MinioErr where
87− MErrHTTP _ == MErrHTTP _ = True
88− MErrHTTP _ == _ = False
89− MErrIO _ == MErrIO _ = True
90− MErrIO _ == _ = False
91− MErrService a == MErrService b = a == b
92− MErrService _ == _ = False
93− MErrValidation a == MErrValidation b = a == b
94− MErrValidation _ == _ = False
80+ MErrHTTP _ == MErrHTTP _ = True
81+ MErrHTTP _ == _ = False
82+ MErrIO _ == MErrIO _ = True
83+ MErrIO _ == _ = False
84+ MErrService a == MErrService b = a == b
85+ MErrService _ == _ = False
86+ MErrValidation a == MErrValidation b = a == b
87+ MErrValidation _ == _ = False
9588
9689instance Exception MinioErr
src/Network/Minio/JsonParser.hs+16−23
@@ -15,35 +15,28 @@
1515--
1616
1717module Network.Minio.JsonParser
18− ( parseErrResponseJSON,
19− )
20−where
18+ (
19+ parseErrResponseJSON
20+ ) where
2121
22−import Data.Aeson
23− ( FromJSON,
24− eitherDecode,
25− parseJSON,
26− withObject,
27− (.:),
28− )
29−import qualified Data.Text as T
30−import Lib.Prelude
31−import Network.Minio.Errors
22+import Data.Aeson (FromJSON, eitherDecode, parseJSON,
23+ withObject, (.:))
24+import qualified Data.Text as T
3225
33−data AdminErrJSON = AdminErrJSON
34− { aeCode :: Text,
35− aeMessage :: Text
36− }
37− deriving stock (Eq, Show)
26+import Lib.Prelude
3827
28+import Network.Minio.Errors
29+
30+data AdminErrJSON = AdminErrJSON { aeCode :: Text
31+ , aeMessage :: Text
32+ } deriving (Eq, Show)
3933instance FromJSON AdminErrJSON where
40− parseJSON = withObject "AdminErrJSON" $ \v ->
41− AdminErrJSON
42− <$> v .: "Code"
43− <*> v .: "Message"
34+ parseJSON = withObject "AdminErrJSON" $ \v -> AdminErrJSON
35+ <$> v .: "Code"
36+ <*> v .: "Message"
4437
4538parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
4639parseErrResponseJSON jsondata =
4740 case eitherDecode jsondata of
4841 Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
49− Left err -> throwIO $ MErrVJsonParse $ T.pack err
42+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs+38−98
@@ -16,50 +16,20 @@
1616
1717module Network.Minio.ListOps where
1818
19−import qualified Data.Conduit as C
19+import qualified Data.Conduit as C
2020import qualified Data.Conduit.Combinators as CC
21−import qualified Data.Conduit.List as CL
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− )
50−import Network.Minio.S3API
51− ( listIncompleteParts',
52− listIncompleteUploads',
53− listObjects',
54− listObjectsV1',
55− )
21+import qualified Data.Conduit.List as CL
22+
23+import Lib.Prelude
24+
25+import Network.Minio.Data
26+import Network.Minio.S3API
5627
5728-- | Represents a list output item - either an object or an object
5829-- prefix (i.e. a directory).
59−data ListItem
60− = ListItemObject ObjectInfo
61− | ListItemPrefix Text
62− deriving stock (Show, Eq)
30+data ListItem = ListItemObject ObjectInfo
31+ | ListItemPrefix Text
32+ deriving (Show, Eq)
6333
6434-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
6535-- similar to a file system tree traversal.
@@ -78,103 +48,73 @@ listObjects bucket prefix recurse = loop Nothing
7848 where
7949 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
8050 loop nextToken = do
81− let delimiter = bool (Just "/") Nothing recurse
51+ let
52+ delimiter = bool (Just "/") Nothing recurse
8253
8354 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
8455 CL.sourceList $ map ListItemObject $ lorObjects res
8556 unless recurse $
86− CL.sourceList $
87− map ListItemPrefix $
88− lorCPrefixes res
57+ CL.sourceList $ map ListItemPrefix $ lorCPrefixes res
8958 when (lorHasMore res) $
9059 loop (lorNextToken res)
9160
9261-- | Lists objects - similar to @listObjects@, however uses the older
9362-- V1 AWS S3 API. Prefer @listObjects@ to this.
94−listObjectsV1 ::
95− Bucket ->
96− Maybe Text ->
97− Bool ->
98− C.ConduitM () ListItem Minio ()
63+listObjectsV1 :: Bucket -> Maybe Text -> Bool
64+ -> C.ConduitM () ListItem Minio ()
9965listObjectsV1 bucket prefix recurse = loop Nothing
10066 where
10167 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
10268 loop nextMarker = do
103− let delimiter = bool (Just "/") Nothing recurse
69+ let
70+ delimiter = bool (Just "/") Nothing recurse
10471
10572 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
10673 CL.sourceList $ map ListItemObject $ lorObjects' res
10774 unless recurse $
108− CL.sourceList $
109− map ListItemPrefix $
110− lorCPrefixes' res
75+ CL.sourceList $ map ListItemPrefix $ lorCPrefixes' res
11176 when (lorHasMore' res) $
11277 loop (lorNextMarker res)
11378
11479-- | List incomplete uploads in a bucket matching the given prefix. If
11580-- recurse is set to True incomplete uploads for the given prefix are
11681-- recursively listed.
117−listIncompleteUploads ::
118− Bucket ->
119− Maybe Text ->
120− Bool ->
121− C.ConduitM () UploadInfo Minio ()
82+listIncompleteUploads :: Bucket -> Maybe Text -> Bool
83+ -> C.ConduitM () UploadInfo Minio ()
12284listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
12385 where
12486 loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
12587 loop nextKeyMarker nextUploadIdMarker = do
126− let delimiter = bool (Just "/") Nothing recurse
127−
128− res <-
129− lift $
130− listIncompleteUploads'
131− bucket
132− prefix
133− delimiter
134− nextKeyMarker
135− nextUploadIdMarker
136− Nothing
137−
138− aggrSizes <- lift $
139− forM (lurUploads res) $ \(uKey, uId, _) -> do
140− partInfos <-
141− C.runConduit $
142− listIncompleteParts bucket uKey uId
143− C..| CC.sinkList
144− return $ foldl' (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
88+ let
89+ delimiter = bool (Just "/") Nothing recurse
90+
91+ res <- lift $ listIncompleteUploads' bucket prefix delimiter
92+ nextKeyMarker nextUploadIdMarker Nothing
93+
94+ aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do
95+ partInfos <- C.runConduit $ listIncompleteParts bucket uKey uId
96+ C..| CC.sinkList
97+ return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
14598
14699 CL.sourceList $
147− zipWith
148− ( curry
149− ( \((uKey, uId, uInitTime), size) ->
150− UploadInfo uKey uId uInitTime size
151− )
152− )
153− (lurUploads res)
154− aggrSizes
100+ map (\((uKey, uId, uInitTime), size) ->
101+ UploadInfo uKey uId uInitTime size
102+ ) $ zip (lurUploads res) aggrSizes
155103
156104 when (lurHasMore res) $
157105 loop (lurNextKey res) (lurNextUpload res)
158106
107+
159108-- | List object parts of an ongoing multipart upload for given
160109-- bucket, object and uploadId.
161−listIncompleteParts ::
162− Bucket ->
163− Object ->
164− UploadId ->
165− C.ConduitM () ObjectPartInfo Minio ()
110+listIncompleteParts :: Bucket -> Object -> UploadId
111+ -> C.ConduitM () ObjectPartInfo Minio ()
166112listIncompleteParts bucket object uploadId = loop Nothing
167113 where
168114 loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
169115 loop nextPartMarker = do
170− res <-
171− lift $
172− listIncompleteParts'
173− bucket
174− object
175− uploadId
176− Nothing
177− nextPartMarker
116+ res <- lift $ listIncompleteParts' bucket object uploadId Nothing
117+ nextPartMarker
178118 CL.sourceList $ lprParts res
179119 when (lprHasMore res) $
180120 loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs+152−215
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,51 +13,45 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE CPP #-}
1716
1817module Network.Minio.PresignedOperations
19− ( UrlExpiry,
20− makePresignedUrl,
21− presignedPutObjectUrl,
22− presignedGetObjectUrl,
23− presignedHeadObjectUrl,
24− PostPolicyCondition (..),
25− ppCondBucket,
26− ppCondContentLengthRange,
27− ppCondContentType,
28− ppCondKey,
29− ppCondKeyStartsWith,
30− ppCondSuccessActionStatus,
31− PostPolicy (..),
32− PostPolicyError (..),
33− newPostPolicy,
34− showPostPolicy,
35− presignedPostPolicy,
36− )
37−where
38−
39−import Data.Aeson ((.=))
40−import qualified Data.Aeson as Json
41−import Data.ByteString.Builder (byteString, toLazyByteString)
42−import qualified Data.HashMap.Strict as H
43−import qualified Data.Text as T
44−import qualified Data.Time as Time
45−import Lib.Prelude
46−import qualified Network.HTTP.Client as NClient
47−import qualified Network.HTTP.Types as HT
48−import Network.Minio.API (buildRequest)
49−import Network.Minio.Credentials
50−import Network.Minio.Data
51−import Network.Minio.Data.Time
52−import Network.Minio.Errors
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 -}
18+ ( UrlExpiry
19+ , makePresignedUrl
20+ , presignedPutObjectUrl
21+ , presignedGetObjectUrl
22+ , presignedHeadObjectUrl
23+
24+ , PostPolicyCondition(..)
25+ , ppCondBucket
26+ , ppCondContentLengthRange
27+ , ppCondContentType
28+ , ppCondKey
29+ , ppCondKeyStartsWith
30+ , ppCondSuccessActionStatus
31+
32+ , PostPolicy(..)
33+ , PostPolicyError(..)
34+ , newPostPolicy
35+ , showPostPolicy
36+ , presignedPostPolicy
37+ ) where
38+
39+import Data.Aeson ((.=))
40+import qualified Data.Aeson as Json
41+import Data.ByteString.Builder (byteString, toLazyByteString)
42+import qualified Data.Map.Strict as Map
43+import qualified Data.Text as T
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
50+
51+import Network.Minio.Data
52+import Network.Minio.Data.Time
53+import Network.Minio.Errors
54+import Network.Minio.Sign.V4
6155
6256-- | Generate a presigned URL. This function allows for advanced usage
6357-- - for simple cases prefer the `presigned*Url` functions.
@@ -67,36 +61,42 @@ import qualified Data.Aeson.Key as A
6761--
6862-- All extra query parameters or headers are signed, and therefore are
6963-- required to be sent when the generated URL is actually used.
70−makePresignedUrl ::
71− UrlExpiry ->
72− HT.Method ->
73− Maybe Bucket ->
74− Maybe Object ->
75− Maybe Region ->
76− HT.Query ->
77− HT.RequestHeaders ->
78− Minio ByteString
64+makePresignedUrl :: UrlExpiry -> HT.Method -> Maybe Bucket -> Maybe Object
65+ -> Maybe Region -> HT.Query -> HT.RequestHeaders
66+ -> Minio ByteString
7967makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
80− when (expiry > 7 * 24 * 3600 || expiry < 0) $
81− throwIO $
82− MErrVInvalidUrlExpiry expiry
83−
84− let s3ri =
85− defaultS3ReqInfo
86− { riPresignExpirySecs = Just expiry,
87− riMethod = method,
88− riBucket = bucket,
89− riObject = object,
90− riRegion = region,
91− riQueryParams = extraQuery,
92− riHeaders = extraHeaders
93− }
94−
95− req <- buildRequest s3ri
96− let uri = NClient.getUri req
97− uriString = uriToString identity uri ""
98−
99− return $ encodeUtf8 uriString
68+ when (expiry > 7*24*3600 || expiry < 0) $
69+ throwIO $ MErrVInvalidUrlExpiry expiry
70+
71+ ci <- asks mcConnInfo
72+
73+ let
74+ hostHeader = (hHost, getHostAddr ci)
75+ req = NC.defaultRequest {
76+ NC.method = method
77+ , NC.secure = connectIsSecure ci
78+ , NC.host = encodeUtf8 $ connectHost ci
79+ , NC.port = connectPort ci
80+ , NC.path = getS3Path bucket object
81+ , NC.requestHeaders = hostHeader : extraHeaders
82+ , NC.queryString = HT.renderQuery True extraQuery
83+ }
84+ ts <- liftIO Time.getCurrentTime
85+
86+ let sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
87+ ts region (Just expiry) Nothing
88+
89+ signPairs = signV4 sp req
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
100100
101101-- | Generate a URL with authentication signature to PUT (upload) an
102102-- object. Any extra headers if passed, are signed, and so they are
@@ -105,22 +105,12 @@ makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
105105--
106106-- For a list of possible headers to pass, please refer to the PUT
107107-- object REST API AWS S3 documentation.
108−presignedPutObjectUrl ::
109− Bucket ->
110− Object ->
111− UrlExpiry ->
112− HT.RequestHeaders ->
113− Minio ByteString
108+presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders
109+ -> Minio ByteString
114110presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
115111 region <- asks (Just . connectRegion . mcConnInfo)
116− makePresignedUrl
117− expirySeconds
118− HT.methodPut
119− (Just bucket)
120− (Just object)
121− region
122− []
123− extraHeaders
112+ makePresignedUrl expirySeconds HT.methodPut
113+ (Just bucket) (Just object) region [] extraHeaders
124114
125115-- | Generate a URL with authentication signature to GET (download) an
126116-- object. All extra query parameters and headers passed here will be
@@ -131,23 +121,12 @@ presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
131121--
132122-- For a list of possible request parameters and headers, please refer
133123-- to the GET object REST API AWS S3 documentation.
134−presignedGetObjectUrl ::
135− Bucket ->
136− Object ->
137− UrlExpiry ->
138− HT.Query ->
139− HT.RequestHeaders ->
140− Minio ByteString
124+presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query
125+ -> HT.RequestHeaders -> Minio ByteString
141126presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
142127 region <- asks (Just . connectRegion . mcConnInfo)
143− makePresignedUrl
144− expirySeconds
145− HT.methodGet
146− (Just bucket)
147− (Just object)
148− region
149− extraQuery
150− extraHeaders
128+ makePresignedUrl expirySeconds HT.methodGet
129+ (Just bucket) (Just object) region extraQuery extraHeaders
151130
152131-- | Generate a URL with authentication signature to make a HEAD
153132-- request on an object. This is used to fetch metadata about an
@@ -156,74 +135,50 @@ presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
156135--
157136-- For a list of possible headers to pass, please refer to the HEAD
158137-- object REST API AWS S3 documentation.
159−presignedHeadObjectUrl ::
160− Bucket ->
161− Object ->
162− UrlExpiry ->
163− HT.RequestHeaders ->
164− Minio ByteString
138+presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry
139+ -> HT.RequestHeaders -> Minio ByteString
165140presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
166141 region <- asks (Just . connectRegion . mcConnInfo)
167− makePresignedUrl
168− expirySeconds
169− HT.methodHead
170− (Just bucket)
171− (Just object)
172− region
173− []
174− extraHeaders
142+ makePresignedUrl expirySeconds HT.methodHead
143+ (Just bucket) (Just object) region [] extraHeaders
175144
176145-- | Represents individual conditions in a Post Policy document.
177−data PostPolicyCondition
178− = PPCStartsWith Text Text
179− | PPCEquals Text Text
180− | PPCRange Text Int64 Int64
181− deriving stock (Show, Eq)
146+data PostPolicyCondition = PPCStartsWith Text Text
147+ | PPCEquals Text Text
148+ | PPCRange Text Int64 Int64
149+ deriving (Show, Eq)
182150
183−{- ORMOLU_DISABLE -}
184151instance Json.ToJSON PostPolicyCondition where
185152 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
189153 toJSON (PPCEquals k v) = Json.object [k .= v]
190−#endif
191154 toJSON (PPCRange k minVal maxVal) =
192155 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
193156
194157 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
198158 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
199−#endif
200159 toEncoding (PPCRange k minVal maxVal) =
201160 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202−{- ORMOLU_ENABLE -}
203161
204162-- | A PostPolicy is required to perform uploads via browser forms.
205−data PostPolicy = PostPolicy
206− { expiration :: UTCTime,
207− conditions :: [PostPolicyCondition]
208− }
209− deriving stock (Show, Eq)
163+data PostPolicy = PostPolicy {
164+ expiration :: UTCTime
165+ , conditions :: [PostPolicyCondition]
166+ } deriving (Show, Eq)
210167
211168instance Json.ToJSON PostPolicy where
212169 toJSON (PostPolicy e c) =
213− Json.object
214− [ "expiration" .= iso8601TimeFormat e,
215− "conditions" .= c
216− ]
170+ Json.object $ [ "expiration" .= iso8601TimeFormat e
171+ , "conditions" .= c
172+ ]
217173 toEncoding (PostPolicy e c) =
218174 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
219175
220176-- | Possible validation errors when creating a PostPolicy.
221−data PostPolicyError
222− = PPEKeyNotSpecified
223− | PPEBucketNotSpecified
224− | PPEConditionKeyEmpty
225− | PPERangeInvalid
226− deriving stock (Show, Eq)
177+data PostPolicyError = PPEKeyNotSpecified
178+ | PPEBucketNotSpecified
179+ | PPEConditionKeyEmpty
180+ | PPERangeInvalid
181+ deriving (Eq, Show)
227182
228183-- | Set the bucket name that the upload should use.
229184ppCondBucket :: Bucket -> PostPolicyCondition
@@ -231,10 +186,8 @@ ppCondBucket = PPCEquals "bucket"
231186
232187-- | Set the content length range constraint with minimum and maximum
233188-- byte count values.
234−ppCondContentLengthRange ::
235− Int64 ->
236− Int64 ->
237− PostPolicyCondition
189+ppCondContentLengthRange :: Int64 -> Int64
190+ -> PostPolicyCondition
238191ppCondContentLengthRange = PPCRange "content-length-range"
239192
240193-- | Set the content-type header for the upload.
@@ -257,99 +210,83 @@ ppCondSuccessActionStatus n =
257210
258211-- | This function creates a PostPolicy after validating its
259212-- arguments.
260−newPostPolicy ::
261− UTCTime ->
262− [PostPolicyCondition] ->
263− Either PostPolicyError PostPolicy
213+newPostPolicy :: UTCTime -> [PostPolicyCondition]
214+ -> Either PostPolicyError PostPolicy
264215newPostPolicy expirationTime conds
265216 -- object name condition must be present
266217 | not $ any (keyEquals "key") conds =
267218 Left PPEKeyNotSpecified
219+
268220 -- bucket name condition must be present
269221 | not $ any (keyEquals "bucket") conds =
270222 Left PPEBucketNotSpecified
223+
271224 -- a condition with an empty key is invalid
272225 | any (keyEquals "") conds || any isEmptyRangeKey conds =
273226 Left PPEConditionKeyEmpty
227+
274228 -- invalid range check
275229 | any isInvalidRange conds =
276230 Left PPERangeInvalid
231+
277232 -- all good!
278233 | otherwise =
279234 return $ PostPolicy expirationTime conds
235+
280236 where
281237 keyEquals k' (PPCStartsWith k _) = k == k'
282− keyEquals k' (PPCEquals k _) = k == k'
283− keyEquals _ _ = False
238+ keyEquals k' (PPCEquals k _) = k == k'
239+ keyEquals _ _ = False
240+
284241 isEmptyRangeKey (PPCRange k _ _) = k == ""
285− isEmptyRangeKey _ = False
242+ isEmptyRangeKey _ = False
243+
286244 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
287− isInvalidRange _ = False
245+ isInvalidRange _ = False
288246
289247-- | Convert Post Policy to a string (e.g. for printing).
290248showPostPolicy :: PostPolicy -> ByteString
291−showPostPolicy = toStrictBS . Json.encode
249+showPostPolicy = toS . Json.encode
292250
293251-- | Generate a presigned URL and POST policy to upload files via a
294252-- browser. On success, this function returns a URL and POST
295253-- form-data.
296−presignedPostPolicy ::
297− PostPolicy ->
298− Minio (ByteString, H.HashMap Text ByteString)
254+presignedPostPolicy :: PostPolicy
255+ -> Minio (ByteString, Map.Map Text ByteString)
299256presignedPostPolicy p = do
300257 ci <- asks mcConnInfo
301− signTime <- liftIO Time.getCurrentTime
302− mgr <- asks mcConnManager
303− cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
304−
305− let extraConditions signParams =
306− [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
307− PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
308− PPCEquals
309− "x-amz-credential"
310− ( T.intercalate
311− "/"
312− [ coerce $ cvAccessKey cv,
313− decodeUtf8 $ credentialScope signParams
314− ]
315− )
316− ]
317− ppWithCreds signParams =
318− p
319− { conditions = conditions p ++ extraConditions signParams
320− }
321− sp =
322− SignParams
323− (coerce $ cvAccessKey cv)
324− (coerce $ cvSecretKey cv)
325− (coerce $ cvSessionToken cv)
326− ServiceS3
327− signTime
328− (Just $ connectRegion ci)
329− Nothing
330− Nothing
331− signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
332− -- compute form-data
333− mkPair (PPCStartsWith k v) = Just (k, v)
334− mkPair (PPCEquals k v) = Just (k, v)
335− mkPair _ = Nothing
336− formFromPolicy =
337− H.map encodeUtf8 $
338− H.fromList $
339− mapMaybe
340− mkPair
341− (conditions $ ppWithCreds sp)
342− formData = formFromPolicy `H.union` signData
343− -- compute POST upload URL
344− bucket = H.lookupDefault "" "bucket" formData
345− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
346− url =
347− toStrictBS $
348− toLazyByteString $
349− scheme
350− <> byteString (getHostAddr ci)
351− <> byteString "/"
352− <> byteString bucket
353− <> byteString "/"
258+ signTime <- liftIO $ Time.getCurrentTime
259+
260+ let
261+ extraConditions =
262+ [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime)
263+ , PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256"
264+ , PPCEquals "x-amz-credential"
265+ (T.intercalate "/" [connectAccessKey ci,
266+ decodeUtf8 $ mkScope signTime region])
267+ ]
268+ ppWithCreds = p {
269+ conditions = conditions p ++ extraConditions
270+ }
271+ sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
272+ signTime (Just $ connectRegion ci) Nothing Nothing
273+ signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
274+
275+
276+ -- compute form-data
277+ mkPair (PPCStartsWith k v) = Just (k, v)
278+ mkPair (PPCEquals k v) = Just (k, v)
279+ mkPair _ = Nothing
280+ formFromPolicy = Map.map toS $ Map.fromList $ catMaybes $
281+ mkPair <$> conditions ppWithCreds
282+ formData = formFromPolicy `Map.union` signData
283+
284+ -- compute POST upload URL
285+ bucket = Map.findWithDefault "" "bucket" formData
286+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
287+ region = connectRegion ci
288+
289+ url = toS $ toLazyByteString $ scheme <> byteString (getHostAddr ci) <>
290+ byteString "/" <> byteString (toS bucket) <> byteString "/"
354291
355292 return (url, formData)
src/Network/Minio/PutObject.hs+69−85
@@ -15,24 +15,29 @@
1515--
1616
1717module Network.Minio.PutObject
18− ( putObjectInternal,
19− ObjectData (..),
20− selectPartSizes,
21− )
22−where
23−
24−import Conduit (takeC)
25−import qualified Conduit as C
26−import qualified Data.ByteString.Lazy as LBS
27−import qualified Data.Conduit.Binary as CB
18+ (
19+ putObjectInternal
20+ , ObjectData(..)
21+ , selectPartSizes
22+ ) where
23+
24+
25+import Conduit (takeC)
26+import qualified Conduit as C
27+import qualified Data.ByteString.Lazy as LBS
28+import qualified Data.Conduit.Binary as CB
2829import qualified Data.Conduit.Combinators as CC
29−import qualified Data.Conduit.List as CL
30−import qualified Data.List as List
31−import Lib.Prelude
32−import Network.Minio.Data
33−import Network.Minio.Errors
34−import Network.Minio.S3API
35−import Network.Minio.Utils
30+import qualified Data.Conduit.List as CL
31+import qualified Data.List as List
32+
33+
34+import Lib.Prelude
35+
36+import Network.Minio.Data
37+import Network.Minio.Errors
38+import Network.Minio.S3API
39+import Network.Minio.Utils
40+
3641
3742-- | A data-type to represent the source data for an object. A
3843-- file-path or a producer-conduit may be provided.
@@ -45,45 +50,37 @@ import Network.Minio.Utils
4550-- the input - if it is not provided, upload will continue until the
4651-- stream ends or the object reaches `maxObjectSize` size.
4752data ObjectData m
48− = -- | Takes filepath and optional
49− -- size.
50− ODFile FilePath (Maybe Int64)
51− | -- | Pass
52− -- size
53− -- (bytes)
54− -- if
55− -- known.
56− ODStream (C.ConduitM () ByteString m ()) (Maybe Int64)
53+ = ODFile FilePath (Maybe Int64) -- ^ Takes filepath and optional
54+ -- size.
55+ | ODStream (C.ConduitM () ByteString m ()) (Maybe Int64) -- ^ Pass
56+ -- size
57+ -- (bytes)
58+ -- if
59+ -- known.
5760
5861-- | Put an object from ObjectData. This high-level API handles
5962-- objects of all sizes, and even if the object size is unknown.
60−putObjectInternal ::
61− Bucket ->
62− Object ->
63− PutObjectOptions ->
64− ObjectData Minio ->
65− Minio ETag
63+putObjectInternal :: Bucket -> Object -> PutObjectOptions
64+ -> ObjectData Minio -> Minio ETag
6665putObjectInternal b o opts (ODStream src sizeMay) = do
6766 case sizeMay of
6867 -- unable to get size, so assume non-seekable file
6968 Nothing -> sequentialMultipartUpload b o opts Nothing src
69+
7070 -- got file size, so check for single/multipart upload
7171 Just size ->
72− if
73− | size <= 64 * oneMiB -> do
74− bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
75− putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
76− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
77− | otherwise -> sequentialMultipartUpload b o opts (Just size) src
72+ if | size <= 64 * oneMiB -> do
73+ bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
74+ putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
75+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
76+ | otherwise -> sequentialMultipartUpload b o opts (Just size) src
77+
7878putObjectInternal b o opts (ODFile fp sizeMay) = do
7979 hResE <- withNewHandle fp $ \h ->
80− liftA2 (,) (isHandleSeekable h) (getFileSize h)
80+ liftM2 (,) (isHandleSeekable h) (getFileSize h)
8181
82− (isSeekable, handleSizeMay) <-
83− either
84− (const $ return (False, Nothing))
85− return
86− hResE
82+ (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return
83+ hResE
8784
8885 -- prefer given size to queried size.
8986 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
@@ -91,25 +88,18 @@ putObjectInternal b o opts (ODFile fp sizeMay) = do
9188 case finalSizeMay of
9289 -- unable to get size, so assume non-seekable file
9390 Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
91+
9492 -- got file size, so check for single/multipart upload
9593 Just size ->
96− if
97− | size <= 64 * oneMiB ->
98− either throwIO return
99− =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
100− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
101− | isSeekable -> parallelMultipartUpload b o opts fp size
102− | otherwise ->
103− sequentialMultipartUpload b o opts (Just size) $
104− CB.sourceFile fp
105−
106−parallelMultipartUpload ::
107− Bucket ->
108− Object ->
109− PutObjectOptions ->
110− FilePath ->
111− Int64 ->
112− Minio ETag
94+ if | size <= 64 * oneMiB -> either throwIO return =<<
95+ withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
96+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
97+ | isSeekable -> parallelMultipartUpload b o opts fp size
98+ | otherwise -> sequentialMultipartUpload b o opts (Just size) $
99+ CB.sourceFile fp
100+
101+parallelMultipartUpload :: Bucket -> Object -> PutObjectOptions
102+ -> FilePath -> Int64 -> Minio ETag
113103parallelMultipartUpload b o opts filePath size = do
114104 -- get a new upload id.
115105 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -119,17 +109,15 @@ parallelMultipartUpload b o opts filePath size = do
119109 let threads = fromMaybe 10 $ pooNumThreads opts
120110
121111 -- perform upload with 'threads' threads
122− uploadedPartsE <-
123− limitedMapConcurrently
124− (fromIntegral threads)
125− (uploadPart uploadId)
126− partSizeInfo
112+ uploadedPartsE <- limitedMapConcurrently (fromIntegral threads)
113+ (uploadPart uploadId) partSizeInfo
127114
128115 -- if there were any errors, rethrow exception.
129116 mapM_ throwIO $ lefts uploadedPartsE
130117
131118 -- if we get here, all parts were successfully uploaded.
132119 completeMultipartUpload b o uploadId $ rights uploadedPartsE
120+
133121 where
134122 uploadPart uploadId (partNum, offset, sz) =
135123 withNewHandle filePath $ \h -> do
@@ -137,13 +125,10 @@ parallelMultipartUpload b o opts filePath size = do
137125 putObjectPart b o uploadId partNum [] payload
138126
139127-- | Upload multipart object from conduit source sequentially
140−sequentialMultipartUpload ::
141− Bucket ->
142− Object ->
143− PutObjectOptions ->
144− Maybe Int64 ->
145− C.ConduitM () ByteString Minio () ->
146− Minio ETag
128+sequentialMultipartUpload :: Bucket -> Object -> PutObjectOptions
129+ -> Maybe Int64
130+ -> C.ConduitM () ByteString Minio ()
131+ -> Minio ETag
147132sequentialMultipartUpload b o opts sizeMay src = do
148133 -- get a new upload id.
149134 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -151,23 +136,22 @@ sequentialMultipartUpload b o opts sizeMay src = do
151136 -- upload parts in loop
152137 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
153138 (pnums, _, sizes) = List.unzip3 partSizes
154− uploadedParts <-
155− C.runConduit $
156− src
157− C..| chunkBSConduit (map fromIntegral sizes)
158− C..| CL.map PayloadBS
159− C..| uploadPart' uploadId pnums
160− C..| CC.sinkList
139+ uploadedParts <- C.runConduit
140+ $ src
141+ C..| chunkBSConduit (map fromIntegral sizes)
142+ C..| CL.map PayloadBS
143+ C..| uploadPart' uploadId pnums
144+ C..| CC.sinkList
161145
162146 -- complete multipart upload
163147 completeMultipartUpload b o uploadId uploadedParts
148+
164149 where
165150 uploadPart' _ [] = return ()
166− uploadPart' uid (pn : pns) = do
151+ uploadPart' uid (pn:pns) = do
167152 payloadMay <- C.await
168153 case payloadMay of
169154 Nothing -> return ()
170− Just payload -> do
171− pinfo <- lift $ putObjectPart b o uid pn [] payload
172− C.yield pinfo
173− uploadPart' uid pns
155+ Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload
156+ C.yield pinfo
157+ uploadPart' uid pns
src/Network/Minio/S3API.hs+314−486
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -14,185 +14,133 @@
1414-- limitations under the License.
1515--
1616
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.
2517module Network.Minio.S3API
26− ( Region,
27− getLocation,
28−
29− -- * Listing buckets
30−
31− --------------------
32− getService,
33−
34− -- * Listing objects
35−
36− --------------------
37− ListObjectsResult (..),
38− ListObjectsV1Result (..),
39− listObjects',
40− listObjectsV1',
41−
42− -- * Retrieving buckets
43− headBucket,
44−
45− -- * Retrieving objects
46−
47− -----------------------
48− getObject',
49− headObject,
50−
51− -- * Creating buckets and objects
52−
53− ---------------------------------
54− putBucket,
55− ETag,
56− maxSinglePutObjectSizeBytes,
57− putObjectSingle',
58− putObjectSingle,
59− copyObjectSingle,
60−
61− -- * Multipart Upload APIs
62−
63− --------------------------
64− UploadId,
65− PartTuple,
66− Payload (..),
67− PartNumber,
68− newMultipartUpload,
69− putObjectPart,
70− copyObjectPart,
71− completeMultipartUpload,
72− abortMultipartUpload,
73− ListUploadsResult (..),
74− listIncompleteUploads',
75− ListPartsResult (..),
76− listIncompleteParts',
77−
78− -- * Deletion APIs
79−
80− --------------------------
81− deleteBucket,
82− deleteObject,
83−
84− -- * Presigned Operations
85−
86− -----------------------------
87− module Network.Minio.PresignedOperations,
88−
89− -- ** Bucket Policies
90− getBucketPolicy,
91− setBucketPolicy,
92−
93− -- * Bucket Notifications
94−
95− -------------------------
96− Notification (..),
97− NotificationConfig (..),
98− Arn,
99− Event (..),
100− Filter (..),
101− FilterKey (..),
102− FilterRules (..),
103− FilterRule (..),
104− getBucketNotification,
105− putBucketNotification,
106− removeAllBucketNotification,
107− )
108−where
109−
110−import qualified Data.ByteString as BS
111−import qualified Data.Text as T
112−import Lib.Prelude
113−import qualified Network.HTTP.Conduit as NC
114−import qualified Network.HTTP.Types as HT
115−import Network.HTTP.Types.Status (status404)
116−import Network.Minio.API
117−import Network.Minio.APICommon
118−import Network.Minio.Data
119−import Network.Minio.Errors
120−import Network.Minio.PresignedOperations
121−import Network.Minio.Utils
122−import Network.Minio.XmlGenerator
123−import Network.Minio.XmlParser
124−import UnliftIO (Handler (Handler))
18+ (
19+ Region
20+ , getLocation
21+
22+ -- * Listing buckets
23+ --------------------
24+ , getService
25+
26+ -- * Listing objects
27+ --------------------
28+ , ListObjectsResult(..)
29+ , ListObjectsV1Result(..)
30+ , listObjects'
31+ , listObjectsV1'
32+
33+ -- * Retrieving buckets
34+ , headBucket
35+
36+ -- * Retrieving objects
37+ -----------------------
38+ , getObject'
39+ , headObject
40+
41+ -- * Creating buckets and objects
42+ ---------------------------------
43+ , putBucket
44+ , ETag
45+ , maxSinglePutObjectSizeBytes
46+ , putObjectSingle'
47+ , putObjectSingle
48+ , copyObjectSingle
49+
50+ -- * Multipart Upload APIs
51+ --------------------------
52+ , UploadId
53+ , PartTuple
54+ , Payload(..)
55+ , PartNumber
56+ , newMultipartUpload
57+ , putObjectPart
58+ , copyObjectPart
59+ , completeMultipartUpload
60+ , abortMultipartUpload
61+ , ListUploadsResult(..)
62+ , listIncompleteUploads'
63+ , ListPartsResult(..)
64+ , listIncompleteParts'
65+
66+ -- * Deletion APIs
67+ --------------------------
68+ , deleteBucket
69+ , deleteObject
70+
71+ -- * Presigned Operations
72+ -----------------------------
73+ , module Network.Minio.PresignedOperations
74+
75+ -- ** Bucket Policies
76+ , getBucketPolicy
77+ , setBucketPolicy
78+
79+ -- * Bucket Notifications
80+ -------------------------
81+ , Notification(..)
82+ , NotificationConfig(..)
83+ , Arn
84+ , Event(..)
85+ , Filter(..)
86+ , FilterKey(..)
87+ , FilterRules(..)
88+ , FilterRule(..)
89+ , getBucketNotification
90+ , putBucketNotification
91+ , removeAllBucketNotification
92+ ) where
93+
94+import qualified Conduit as C
95+import qualified Data.ByteString as BS
96+import qualified Data.Text as T
97+import qualified Network.HTTP.Conduit as NC
98+import qualified Network.HTTP.Types as HT
99+import Network.HTTP.Types.Status (status404)
100+import UnliftIO (Handler (Handler))
101+
102+import Lib.Prelude
103+
104+import Network.Minio.API
105+import Network.Minio.APICommon
106+import Network.Minio.Data
107+import Network.Minio.Errors
108+import Network.Minio.PresignedOperations
109+import Network.Minio.Utils
110+import Network.Minio.XmlGenerator
111+import Network.Minio.XmlParser
125112
126113-- | Fetch all buckets from the service.
127114getService :: Minio [BucketInfo]
128115getService = do
129− resp <-
130− executeRequest $
131− defaultS3ReqInfo
132− { riNeedsLocation = False
133− }
116+ resp <- executeRequest $ defaultS3ReqInfo {
117+ riNeedsLocation = False
118+ }
134119 parseListBuckets $ NC.responseBody resp
135120
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−
150−-- | GET an object from the service and return parsed ObjectInfo and a
151−-- conduit source for the object content
152−getObject' ::
153− Bucket ->
154− Object ->
155− HT.Query ->
156− [HT.Header] ->
157− Minio GetObjectResponse
121+-- | GET an object from the service and return the response headers
122+-- and a conduit source for the object content
123+getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
124+ -> Minio ([HT.Header], C.ConduitM () ByteString Minio ())
158125getObject' bucket object queryParams headers = do
159126 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,
169− gorObjectStream = NC.responseBody resp
170− }
127+ return (NC.responseHeaders resp, NC.responseBody resp)
171128 where
172− reqInfo =
173− defaultS3ReqInfo
174− { riBucket = Just bucket,
175− riObject = Just object,
176− riQueryParams = queryParams,
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")]
183− }
129+ reqInfo = defaultS3ReqInfo { riBucket = Just bucket
130+ , riObject = Just object
131+ , riQueryParams = queryParams
132+ , riHeaders = headers
133+ }
184134
185135-- | Creates a bucket via a PUT bucket call.
186136putBucket :: Bucket -> Region -> Minio ()
187137putBucket bucket location = do
188138 ns <- asks getSvcNamespace
189− void $
190− executeRequest $
191− defaultS3ReqInfo
192− { riMethod = HT.methodPut,
193− riBucket = Just bucket,
194− riPayload = PayloadBS $ mkCreateBucketConfig ns location,
195− riNeedsLocation = False
139+ void $ executeRequest $
140+ defaultS3ReqInfo { riMethod = HT.methodPut
141+ , riBucket = Just bucket
142+ , riPayload = PayloadBS $ mkCreateBucketConfig ns location
143+ , riNeedsLocation = False
196144 }
197145
198146-- | Single PUT object size.
@@ -208,432 +156,321 @@ putObjectSingle' bucket object headers bs = do
208156 let size = fromIntegral (BS.length bs)
209157 -- check length is within single PUT object size.
210158 when (size > maxSinglePutObjectSizeBytes) $
211− throwIO $
212− MErrVSinglePUTSizeExceeded size
159+ throwIO $ MErrVSinglePUTSizeExceeded size
213160
214161 let payload = mkStreamingPayload $ PayloadBS bs
215− resp <-
216− executeRequest $
217− defaultS3ReqInfo
218− { riMethod = HT.methodPut,
219− riBucket = Just bucket,
220− riObject = Just object,
221− riHeaders = headers,
222− riPayload = payload
223− }
162+ resp <- executeRequest $
163+ defaultS3ReqInfo { riMethod = HT.methodPut
164+ , riBucket = Just bucket
165+ , riObject = Just object
166+ , riHeaders = headers
167+ , riPayload = payload
168+ }
224169
225170 let rheaders = NC.responseHeaders resp
226171 etag = getETagHeader rheaders
227172 maybe
228173 (throwIO MErrVETagHeaderNotFound)
229− return
230− etag
174+ return etag
231175
232176-- | PUT an object into the service. This function performs a single
233177-- PUT object call, and so can only transfer objects upto 5GiB.
234−putObjectSingle ::
235− Bucket ->
236− Object ->
237− [HT.Header] ->
238− Handle ->
239− Int64 ->
240− Int64 ->
241− Minio ETag
178+putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
179+ -> Int64 -> Minio ETag
242180putObjectSingle bucket object headers h offset size = do
243181 -- check length is within single PUT object size.
244182 when (size > maxSinglePutObjectSizeBytes) $
245− throwIO $
246− MErrVSinglePUTSizeExceeded size
183+ throwIO $ MErrVSinglePUTSizeExceeded size
247184
248185 -- content-length header is automatically set by library.
249186 let payload = mkStreamingPayload $ PayloadH h offset size
250− resp <-
251− executeRequest $
252− defaultS3ReqInfo
253− { riMethod = HT.methodPut,
254− riBucket = Just bucket,
255− riObject = Just object,
256− riHeaders = headers,
257− riPayload = payload
258− }
187+ resp <- executeRequest $
188+ defaultS3ReqInfo { riMethod = HT.methodPut
189+ , riBucket = Just bucket
190+ , riObject = Just object
191+ , riHeaders = headers
192+ , riPayload = payload
193+ }
259194
260195 let rheaders = NC.responseHeaders resp
261196 etag = getETagHeader rheaders
262197 maybe
263198 (throwIO MErrVETagHeaderNotFound)
264− return
265− etag
199+ return etag
266200
267201-- | List objects in a bucket matching prefix up to delimiter,
268202-- starting from nextMarker.
269−listObjectsV1' ::
270− Bucket ->
271− Maybe Text ->
272− Maybe Text ->
273− Maybe Text ->
274− Maybe Int ->
275− Minio ListObjectsV1Result
203+listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
204+ -> Minio ListObjectsV1Result
276205listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
277− resp <-
278− executeRequest $
279− defaultS3ReqInfo
280− { riMethod = HT.methodGet,
281− riBucket = Just bucket,
282− riQueryParams = mkOptionalParams params
283− }
206+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
207+ , riBucket = Just bucket
208+ , riQueryParams = mkOptionalParams params
209+ }
284210 parseListObjectsV1Response $ NC.responseBody resp
285211 where
286− params =
287− [ ("marker", nextMarker),
288− ("prefix", prefix),
289− ("delimiter", delimiter),
290− ("max-keys", show <$> maxKeys)
212+ params = [
213+ ("marker", nextMarker)
214+ , ("prefix", prefix)
215+ , ("delimiter", delimiter)
216+ , ("max-keys", show <$> maxKeys)
291217 ]
292218
293219-- | List objects in a bucket matching prefix up to delimiter,
294220-- starting from nextToken.
295−listObjects' ::
296− Bucket ->
297− Maybe Text ->
298− Maybe Text ->
299− Maybe Text ->
300− Maybe Int ->
301− Minio ListObjectsResult
221+listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
222+ -> Minio ListObjectsResult
302223listObjects' bucket prefix nextToken delimiter maxKeys = do
303− resp <-
304− executeRequest $
305− defaultS3ReqInfo
306− { riMethod = HT.methodGet,
307− riBucket = Just bucket,
308− riQueryParams = mkOptionalParams params
309− }
224+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
225+ , riBucket = Just bucket
226+ , riQueryParams = mkOptionalParams params
227+ }
310228 parseListObjectsResponse $ NC.responseBody resp
311229 where
312− params =
313− [ ("list-type", Just "2"),
314− ("continuation_token", nextToken),
315− ("prefix", prefix),
316− ("delimiter", delimiter),
317− ("max-keys", show <$> maxKeys)
230+ params = [
231+ ("list-type", Just "2")
232+ , ("continuation_token", nextToken)
233+ , ("prefix", prefix)
234+ , ("delimiter", delimiter)
235+ , ("max-keys", show <$> maxKeys)
318236 ]
319237
320238-- | DELETE a bucket from the service.
321239deleteBucket :: Bucket -> Minio ()
322−deleteBucket bucket =
323− void $
324− executeRequest $
325− defaultS3ReqInfo
326− { riMethod = HT.methodDelete,
327− riBucket = Just bucket
240+deleteBucket bucket = void $
241+ executeRequest $
242+ defaultS3ReqInfo { riMethod = HT.methodDelete
243+ , riBucket = Just bucket
328244 }
329245
330246-- | DELETE an object from the service.
331247deleteObject :: Bucket -> Object -> Minio ()
332−deleteObject bucket object =
333− void $
334− executeRequest $
335− defaultS3ReqInfo
336− { riMethod = HT.methodDelete,
337− riBucket = Just bucket,
338− riObject = Just object
248+deleteObject bucket object = void $
249+ executeRequest $
250+ defaultS3ReqInfo { riMethod = HT.methodDelete
251+ , riBucket = Just bucket
252+ , riObject = Just object
339253 }
340254
341255-- | Create a new multipart upload.
342256newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
343257newMultipartUpload bucket object headers = do
344− resp <-
345− executeRequest $
346− defaultS3ReqInfo
347− { riMethod = HT.methodPost,
348− riBucket = Just bucket,
349− riObject = Just object,
350− riQueryParams = [("uploads", Nothing)],
351− riHeaders = headers
352− }
258+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPost
259+ , riBucket = Just bucket
260+ , riObject = Just object
261+ , riQueryParams = [("uploads", Nothing)]
262+ , riHeaders = headers
263+ }
353264 parseNewMultipartUpload $ NC.responseBody resp
354265
355266-- | PUT a part of an object as part of a multipart upload.
356−putObjectPart ::
357− Bucket ->
358− Object ->
359− UploadId ->
360− PartNumber ->
361− [HT.Header] ->
362− Payload ->
363− Minio PartTuple
267+putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
268+ -> Payload -> Minio PartTuple
364269putObjectPart bucket object uploadId partNumber headers payload = do
365270 -- transform payload to conduit to enable streaming signature
366271 let payload' = mkStreamingPayload payload
367− resp <-
368− executeRequest $
369− defaultS3ReqInfo
370− { riMethod = HT.methodPut,
371− riBucket = Just bucket,
372− riObject = Just object,
373− riQueryParams = mkOptionalParams params,
374− riHeaders = headers,
375− riPayload = payload'
376− }
272+ resp <- executeRequest $
273+ defaultS3ReqInfo { riMethod = HT.methodPut
274+ , riBucket = Just bucket
275+ , riObject = Just object
276+ , riQueryParams = mkOptionalParams params
277+ , riHeaders = headers
278+ , riPayload = payload'
279+ }
377280 let rheaders = NC.responseHeaders resp
378281 etag = getETagHeader rheaders
379282 maybe
380283 (throwIO MErrVETagHeaderNotFound)
381− (return . (partNumber,))
382− etag
284+ (return . (partNumber, )) etag
383285 where
384− params =
385− [ ("uploadId", Just uploadId),
386− ("partNumber", Just $ show partNumber)
286+ params = [
287+ ("uploadId", Just uploadId)
288+ , ("partNumber", Just $ show partNumber)
387289 ]
388290
389291srcInfoToHeaders :: SourceInfo -> [HT.Header]
390−srcInfoToHeaders srcInfo =
391− ( "x-amz-copy-source",
392− encodeUtf8 $
393− T.concat
394− [ "/",
395− srcBucket srcInfo,
396− "/",
397− srcObject srcInfo
398− ]
399− )
400− : rangeHdr
401− ++ zip names values
292+srcInfoToHeaders srcInfo = ("x-amz-copy-source",
293+ toS $ T.concat ["/", srcBucket srcInfo,
294+ "/", srcObject srcInfo]
295+ ) : rangeHdr ++ zip names values
402296 where
403− names =
404− [ "x-amz-copy-source-if-match",
405− "x-amz-copy-source-if-none-match",
406− "x-amz-copy-source-if-unmodified-since",
407− "x-amz-copy-source-if-modified-since"
408− ]
409− values =
410− mapMaybe
411− (fmap encodeUtf8 . (srcInfo &))
412− [ srcIfMatch,
413− srcIfNoneMatch,
414− fmap formatRFC1123 . srcIfUnmodifiedSince,
415− fmap formatRFC1123 . srcIfModifiedSince
416− ]
417− rangeHdr =
418− maybe [] ((\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) . toByteRange) (srcRange srcInfo)
297+ names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",
298+ "x-amz-copy-source-if-unmodified-since",
299+ "x-amz-copy-source-if-modified-since"]
300+ values = mapMaybe (fmap encodeUtf8 . (srcInfo &))
301+ [srcIfMatch, srcIfNoneMatch,
302+ fmap formatRFC1123 . srcIfUnmodifiedSince,
303+ fmap formatRFC1123 . srcIfModifiedSince]
304+ rangeHdr = maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])])
305+ $ toByteRange <$> srcRange srcInfo
419306 toByteRange :: (Int64, Int64) -> HT.ByteRange
420307 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
421308
422309-- | Performs server-side copy of an object or part of an object as an
423310-- upload part of an ongoing multi-part upload.
424−copyObjectPart ::
425− DestinationInfo ->
426− SourceInfo ->
427− UploadId ->
428− PartNumber ->
429− [HT.Header] ->
430− Minio (ETag, UTCTime)
311+copyObjectPart :: DestinationInfo -> SourceInfo -> UploadId
312+ -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
431313copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
432− resp <-
433− executeRequest $
434− defaultS3ReqInfo
435− { riMethod = HT.methodPut,
436− riBucket = Just $ dstBucket dstInfo,
437− riObject = Just $ dstObject dstInfo,
438− riQueryParams = mkOptionalParams params,
439− riHeaders = headers ++ srcInfoToHeaders srcInfo
440− }
314+ resp <- executeRequest $
315+ defaultS3ReqInfo { riMethod = HT.methodPut
316+ , riBucket = Just $ dstBucket dstInfo
317+ , riObject = Just $ dstObject dstInfo
318+ , riQueryParams = mkOptionalParams params
319+ , riHeaders = headers ++ srcInfoToHeaders srcInfo
320+ }
441321
442322 parseCopyObjectResponse $ NC.responseBody resp
443323 where
444− params =
445− [ ("uploadId", Just uploadId),
446− ("partNumber", Just $ show partNumber)
324+ params = [
325+ ("uploadId", Just uploadId)
326+ , ("partNumber", Just $ show partNumber)
447327 ]
448328
449329-- | Performs server-side copy of an object that is upto 5GiB in
450330-- size. If the object is greater than 5GiB, this function throws the
451331-- error returned by the server.
452−copyObjectSingle ::
453− Bucket ->
454− Object ->
455− SourceInfo ->
456− [HT.Header] ->
457− Minio (ETag, UTCTime)
332+copyObjectSingle :: Bucket -> Object -> SourceInfo -> [HT.Header]
333+ -> Minio (ETag, UTCTime)
458334copyObjectSingle bucket object srcInfo headers = do
459335 -- validate that srcRange is Nothing for this API.
460336 when (isJust $ srcRange srcInfo) $
461337 throwIO MErrVCopyObjSingleNoRangeAccepted
462− resp <-
463− executeRequest $
464− defaultS3ReqInfo
465− { riMethod = HT.methodPut,
466− riBucket = Just bucket,
467− riObject = Just object,
468− riHeaders = headers ++ srcInfoToHeaders srcInfo
469− }
338+ resp <- executeRequest $
339+ defaultS3ReqInfo { riMethod = HT.methodPut
340+ , riBucket = Just bucket
341+ , riObject = Just object
342+ , riHeaders = headers ++ srcInfoToHeaders srcInfo
343+ }
470344 parseCopyObjectResponse $ NC.responseBody resp
471345
472346-- | Complete a multipart upload.
473−completeMultipartUpload ::
474− Bucket ->
475− Object ->
476− UploadId ->
477− [PartTuple] ->
478− Minio ETag
347+completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
348+ -> Minio ETag
479349completeMultipartUpload bucket object uploadId partTuple = do
480− resp <-
481− executeRequest $
482− defaultS3ReqInfo
483− { riMethod = HT.methodPost,
484− riBucket = Just bucket,
485− riObject = Just object,
486− riQueryParams = mkOptionalParams params,
487− riPayload =
488− PayloadBS $
489− mkCompleteMultipartUploadRequest partTuple
490− }
350+ resp <- executeRequest $
351+ defaultS3ReqInfo { riMethod = HT.methodPost
352+ , riBucket = Just bucket
353+ , riObject = Just object
354+ , riQueryParams = mkOptionalParams params
355+ , riPayload = PayloadBS $
356+ mkCompleteMultipartUploadRequest partTuple
357+ }
491358 parseCompleteMultipartUploadResponse $ NC.responseBody resp
492359 where
493360 params = [("uploadId", Just uploadId)]
494361
495362-- | Abort a multipart upload.
496363abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
497−abortMultipartUpload bucket object uploadId =
498− void $
499− executeRequest $
500− defaultS3ReqInfo
501− { riMethod = HT.methodDelete,
502− riBucket = Just bucket,
503− riObject = Just object,
504− riQueryParams = mkOptionalParams params
505− }
364+abortMultipartUpload bucket object uploadId = void $
365+ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
366+ , riBucket = Just bucket
367+ , riObject = Just object
368+ , riQueryParams = mkOptionalParams params
369+ }
506370 where
507371 params = [("uploadId", Just uploadId)]
508372
509373-- | List incomplete multipart uploads.
510−listIncompleteUploads' ::
511− Bucket ->
512− Maybe Text ->
513− Maybe Text ->
514− Maybe Text ->
515− Maybe Text ->
516− Maybe Int ->
517− Minio ListUploadsResult
374+listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
375+ -> Maybe Text -> Maybe Int -> Minio ListUploadsResult
518376listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
519− resp <-
520− executeRequest $
521− defaultS3ReqInfo
522− { riMethod = HT.methodGet,
523− riBucket = Just bucket,
524− riQueryParams = params
525− }
377+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
378+ , riBucket = Just bucket
379+ , riQueryParams = params
380+ }
526381 parseListUploadsResponse $ NC.responseBody resp
527382 where
528383 -- build query params
529− params =
530− ("uploads", Nothing)
531− : mkOptionalParams
532− [ ("prefix", prefix),
533− ("delimiter", delimiter),
534− ("key-marker", keyMarker),
535− ("upload-id-marker", uploadIdMarker),
536− ("max-uploads", show <$> maxKeys)
537− ]
384+ params = ("uploads", Nothing) : mkOptionalParams
385+ [ ("prefix", prefix)
386+ , ("delimiter", delimiter)
387+ , ("key-marker", keyMarker)
388+ , ("upload-id-marker", uploadIdMarker)
389+ , ("max-uploads", show <$> maxKeys)
390+ ]
391+
538392
539393-- | List parts of an ongoing multipart upload.
540−listIncompleteParts' ::
541− Bucket ->
542− Object ->
543− UploadId ->
544− Maybe Text ->
545− Maybe Text ->
546− Minio ListPartsResult
394+listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
395+ -> Maybe Text -> Minio ListPartsResult
547396listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
548− resp <-
549− executeRequest $
550− defaultS3ReqInfo
551− { riMethod = HT.methodGet,
552− riBucket = Just bucket,
553− riObject = Just object,
554− riQueryParams = mkOptionalParams params
555− }
397+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
398+ , riBucket = Just bucket
399+ , riObject = Just object
400+ , riQueryParams = mkOptionalParams params
401+ }
556402 parseListPartsResponse $ NC.responseBody resp
557403 where
558404 -- build optional query params
559− params =
560− [ ("uploadId", Just uploadId),
561− ("part-number-marker", partNumMarker),
562− ("max-parts", maxParts)
405+ params = [
406+ ("uploadId", Just uploadId)
407+ , ("part-number-marker", partNumMarker)
408+ , ("max-parts", maxParts)
563409 ]
564410
565411-- | Get metadata of an object.
566412headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
567413headObject bucket object reqHeaders = do
568− resp <-
569− executeRequest $
570− defaultS3ReqInfo
571− { riMethod = HT.methodHead,
572− riBucket = Just bucket,
573− riObject = Just object,
574− riHeaders =
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")]
580− }
414+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
415+ , riBucket = Just bucket
416+ , riObject = Just object
417+ , riHeaders = reqHeaders
418+ }
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+
581427 maybe (throwIO MErrVInvalidObjectInfoResponse) return $
582− parseGetObjectHeaders object $
583− NC.responseHeaders resp
428+ ObjectInfo <$> Just object <*> modTime <*> etag <*> size <*> Just metadata
429+
584430
585431-- | Query the object store if a given bucket exists.
586432headBucket :: Bucket -> Minio Bool
587−headBucket bucket =
588− headBucketEx
589− `catches` [ Handler handleNoSuchBucket,
590− Handler handleStatus404
591− ]
433+headBucket bucket = headBucketEx `catches`
434+ [ Handler handleNoSuchBucket
435+ , Handler handleStatus404
436+ ]
437+
592438 where
593439 handleNoSuchBucket :: ServiceErr -> Minio Bool
594− handleNoSuchBucket e
595− | e == NoSuchBucket = return False
596− | otherwise = throwIO e
440+ handleNoSuchBucket e | e == NoSuchBucket = return False
441+ | otherwise = throwIO e
442+
597443 handleStatus404 :: NC.HttpException -> Minio Bool
598444 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
599445 if NC.responseStatus res == status404
600− then return False
601− else throwIO e
446+ then return False
447+ else throwIO e
602448 handleStatus404 e = throwIO e
449+
603450 headBucketEx = do
604− resp <-
605− executeRequest $
606− defaultS3ReqInfo
607− { riMethod = HT.methodHead,
608− riBucket = Just bucket
609− }
451+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
452+ , riBucket = Just bucket
453+ }
610454 return $ NC.responseStatus resp == HT.ok200
611455
612456-- | Set the notification configuration on a bucket.
613457putBucketNotification :: Bucket -> Notification -> Minio ()
614458putBucketNotification bucket ncfg = do
615459 ns <- asks getSvcNamespace
616− void $
617− executeRequest $
618− defaultS3ReqInfo
619− { riMethod = HT.methodPut,
620− riBucket = Just bucket,
621− riQueryParams = [("notification", Nothing)],
622− riPayload =
623− PayloadBS $
624− mkPutNotificationRequest ns ncfg
625− }
460+ void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
461+ , riBucket = Just bucket
462+ , riQueryParams = [("notification", Nothing)]
463+ , riPayload = PayloadBS $
464+ mkPutNotificationRequest ns ncfg
465+ }
626466
627467-- | Retrieve the notification configuration on a bucket.
628468getBucketNotification :: Bucket -> Minio Notification
629469getBucketNotification bucket = do
630− resp <-
631− executeRequest $
632− defaultS3ReqInfo
633− { riMethod = HT.methodGet,
634− riBucket = Just bucket,
635− riQueryParams = [("notification", Nothing)]
636− }
470+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
471+ , riBucket = Just bucket
472+ , riQueryParams = [("notification", Nothing)]
473+ }
637474 parseNotification $ NC.responseBody resp
638475
639476-- | Remove all notifications configured on a bucket.
@@ -643,14 +480,11 @@ removeAllBucketNotification = flip putBucketNotification defaultNotification
643480-- | Fetch the policy if any on a bucket.
644481getBucketPolicy :: Bucket -> Minio Text
645482getBucketPolicy bucket = do
646− resp <-
647− executeRequest $
648− defaultS3ReqInfo
649− { riMethod = HT.methodGet,
650− riBucket = Just bucket,
651− riQueryParams = [("policy", Nothing)]
652− }
653− return $ decodeUtf8Lenient $ toStrictBS $ NC.responseBody resp
483+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
484+ , riBucket = Just bucket
485+ , riQueryParams = [("policy", Nothing)]
486+ }
487+ return $ toS $ NC.responseBody resp
654488
655489-- | Set a new policy on a bucket.
656490-- As a special condition if the policy is empty
@@ -662,24 +496,18 @@ setBucketPolicy bucket policy = do
662496 else putBucketPolicy bucket policy
663497
664498-- | Save a new policy on a bucket.
665−putBucketPolicy :: Bucket -> Text -> Minio ()
499+putBucketPolicy :: Bucket -> Text -> Minio()
666500putBucketPolicy bucket policy = do
667− void $
668− executeRequest $
669− defaultS3ReqInfo
670− { riMethod = HT.methodPut,
671− riBucket = Just bucket,
672− riQueryParams = [("policy", Nothing)],
673− riPayload = PayloadBS $ encodeUtf8 policy
674− }
501+ void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
502+ , riBucket = Just bucket
503+ , riQueryParams = [("policy", Nothing)]
504+ , riPayload = PayloadBS $ encodeUtf8 policy
505+ }
675506
676507-- | Delete any policy set on a bucket.
677−deleteBucketPolicy :: Bucket -> Minio ()
508+deleteBucketPolicy :: Bucket -> Minio()
678509deleteBucketPolicy bucket = do
679− void $
680− executeRequest $
681− defaultS3ReqInfo
682− { riMethod = HT.methodDelete,
683− riBucket = Just bucket,
684− riQueryParams = [("policy", Nothing)]
685− }
510+ void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
511+ , riBucket = Just bucket
512+ , riQueryParams = [("policy", Nothing)]
513+ }
src/Network/Minio/SelectAPI.hs+241−233
@@ -15,103 +15,116 @@
1515--
1616
1717module Network.Minio.SelectAPI
18− ( -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
19− -- format objects in AWS S3 and in MinIO using SQL Select
20− -- statements. This allows significant reduction of data transfer
21− -- from object storage for computation-intensive tasks, as relevant
22− -- data is filtered close to the storage.
23− selectObjectContent,
24− SelectRequest,
25− selectRequest,
26−
27− -- *** Input Serialization
28− InputSerialization,
29− defaultCsvInput,
30− linesJsonInput,
31− documentJsonInput,
32− defaultParquetInput,
33− setInputCSVProps,
34− CompressionType (..),
35− setInputCompressionType,
36−
37− -- *** CSV Format details
38−
39− -- | CSV format options such as delimiters and quote characters are
40− -- specified using using the functions below. Options are combined
41− -- monoidally.
42− CSVProp,
43− recordDelimiter,
44− fieldDelimiter,
45− quoteCharacter,
46− quoteEscapeCharacter,
47− commentCharacter,
48− allowQuotedRecordDelimiter,
49− FileHeaderInfo (..),
50− fileHeaderInfo,
51− QuoteFields (..),
52− quoteFields,
53−
54− -- *** Output Serialization
55− OutputSerialization,
56− defaultCsvOutput,
57− defaultJsonOutput,
58− outputCSVFromProps,
59− outputJSONFromRecordDelimiter,
60−
61− -- *** Progress messages
62− setRequestProgressEnabled,
63−
64− -- *** Interpreting Select output
65−
66− -- | The conduit returned by `selectObjectContent` returns values of
67− -- the `EventMessage` data type. This returns the query output
68− -- messages formatted according to the chosen output serialization,
69− -- interleaved with progress messages (if enabled by
70− -- `setRequestProgressEnabled`), and at the end a statistics
71− -- message.
72− --
73− -- If the application is interested in only the payload, then
74− -- `getPayloadBytes` can be used. For example to simply print the
75− -- payload to stdout:
76− --
77− -- > resultConduit <- selectObjectContent bucket object mySelectRequest
78− -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
79− --
80− -- Note that runConduit, the connect operator (.|) and stdoutC are
81− -- all from the "conduit" package.
82− getPayloadBytes,
83− EventMessage (..),
84− Progress (..),
85− Stats,
86− )
87−where
88−
89−import Conduit ((.|))
90−import qualified Conduit as C
91−import qualified Data.Binary as Bin
92−import qualified Data.ByteString as B
93−import qualified Data.ByteString.Lazy as LB
94−import Data.Digest.CRC32 (crc32, crc32Update)
95−import Lib.Prelude
96−import qualified Network.HTTP.Conduit as NC
97−import qualified Network.HTTP.Types as HT
98−import Network.Minio.API
99−import Network.Minio.Data
100−import Network.Minio.Errors
101−import Network.Minio.Utils
102−import Network.Minio.XmlGenerator
103−import Network.Minio.XmlParser
104−import UnliftIO (MonadUnliftIO)
105−
106−data EventStreamException
107− = ESEPreludeCRCFailed
108− | ESEMessageCRCFailed
109− | ESEUnexpectedEndOfStream
110− | ESEDecodeFail [Char]
111− | ESEInvalidHeaderType
112− | ESEInvalidHeaderValueType
113− | ESEInvalidMessageType
114− deriving stock (Eq, Show)
18+ (
19+
20+ -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
21+ -- format objects in AWS S3 and in MinIO using SQL Select
22+ -- statements. This allows significant reduction of data transfer
23+ -- from object storage for computation-intensive tasks, as relevant
24+ -- data is filtered close to the storage.
25+
26+ selectObjectContent
27+
28+ , SelectRequest
29+ , selectRequest
30+
31+ -- *** Input Serialization
32+ -------------------------
33+
34+ , InputSerialization
35+ , defaultCsvInput
36+ , linesJsonInput
37+ , documentJsonInput
38+ , defaultParquetInput
39+ , setInputCSVProps
40+
41+ , CompressionType(..)
42+ , setInputCompressionType
43+
44+ -- *** CSV Format details
45+ ------------------------
46+ -- | CSV format options such as delimiters and quote characters are
47+ -- specified using using the functions below. Options are combined
48+ -- monoidally.
49+
50+ , CSVProp
51+ , recordDelimiter
52+ , fieldDelimiter
53+ , quoteCharacter
54+ , quoteEscapeCharacter
55+ , commentCharacter
56+ , allowQuotedRecordDelimiter
57+ , FileHeaderInfo(..)
58+ , fileHeaderInfo
59+ , QuoteFields(..)
60+ , quoteFields
61+
62+ -- *** Output Serialization
63+ -------------------------
64+
65+ , OutputSerialization
66+ , defaultCsvOutput
67+ , defaultJsonOutput
68+ , outputCSVFromProps
69+ , outputJSONFromRecordDelimiter
70+
71+ -- *** Progress messages
72+ ------------------------
73+
74+ , setRequestProgressEnabled
75+
76+ -- *** Interpreting Select output
77+ --------------------------------------------
78+ -- | The conduit returned by `selectObjectContent` returns values of
79+ -- the `EventMessage` data type. This returns the query output
80+ -- messages formatted according to the chosen output serialization,
81+ -- interleaved with progress messages (if enabled by
82+ -- `setRequestProgressEnabled`), and at the end a statistics
83+ -- message.
84+ --
85+ -- If the application is interested in only the payload, then
86+ -- `getPayloadBytes` can be used. For example to simply print the
87+ -- payload to stdout:
88+ --
89+ -- > resultConduit <- selectObjectContent bucket object mySelectRequest
90+ -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
91+ --
92+ -- Note that runConduit, the connect operator (.|) and stdoutC are
93+ -- all from the "conduit" package.
94+
95+ , getPayloadBytes
96+ , EventMessage(..)
97+ , Progress(..)
98+ , Stats
99+ ) where
100+
101+import Conduit ((.|))
102+import qualified Conduit as C
103+import qualified Data.Binary as Bin
104+import qualified Data.ByteString as B
105+import qualified Data.ByteString.Lazy as LB
106+import Data.Digest.CRC32 (crc32, crc32Update)
107+import qualified Network.HTTP.Conduit as NC
108+import qualified Network.HTTP.Types as HT
109+import UnliftIO (MonadUnliftIO)
110+
111+import Lib.Prelude
112+
113+import Network.Minio.API
114+import Network.Minio.Data
115+import Network.Minio.Errors
116+import Network.Minio.Utils
117+import Network.Minio.XmlGenerator
118+import Network.Minio.XmlParser
119+
120+data EventStreamException = ESEPreludeCRCFailed
121+ | ESEMessageCRCFailed
122+ | ESEUnexpectedEndOfStream
123+ | ESEDecodeFail [Char]
124+ | ESEInvalidHeaderType
125+ | ESEInvalidHeaderValueType
126+ | ESEInvalidMessageType
127+ deriving (Eq, Show)
115128
116129instance Exception EventStreamException
117130
@@ -119,176 +132,171 @@ instance Exception EventStreamException
119132chunkSize :: Int
120133chunkSize = 32 * 1024
121134
122−parseBinary :: (Bin.Binary a) => ByteString -> IO a
135+parseBinary :: Bin.Binary a => ByteString -> IO a
123136parseBinary b = do
124− case Bin.decodeOrFail $ LB.fromStrict b of
125− Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
126− Right (_, _, r) -> return r
137+ case Bin.decodeOrFail $ LB.fromStrict b of
138+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
139+ Right (_, _, r) -> return r
127140
128141bytesToHeaderName :: Text -> IO MsgHeaderName
129142bytesToHeaderName t = case t of
130− ":message-type" -> return MessageType
131− ":event-type" -> return EventType
132− ":content-type" -> return ContentType
133− ":error-code" -> return ErrorCode
143+ ":message-type" -> return MessageType
144+ ":event-type" -> return EventType
145+ ":content-type" -> return ContentType
146+ ":error-code" -> return ErrorCode
134147 ":error-message" -> return ErrorMessage
135− _ -> throwIO ESEInvalidHeaderType
148+ _ -> throwIO ESEInvalidHeaderType
136149
137−parseHeaders ::
138− (MonadUnliftIO m) =>
139− Word32 ->
140− C.ConduitM ByteString a m [MessageHeader]
150+parseHeaders :: MonadUnliftIO m
151+ => Word32 -> C.ConduitM ByteString a m [MessageHeader]
141152parseHeaders 0 = return []
142153parseHeaders hdrLen = do
143− bs1 <- readNBytes 1
144− n :: Word8 <- liftIO $ parseBinary bs1
154+ bs1 <- readNBytes 1
155+ n :: Word8 <- liftIO $ parseBinary bs1
145156
146− headerKeyBytes <- readNBytes $ fromIntegral n
147− let headerKey = decodeUtf8Lenient headerKeyBytes
148− headerName <- liftIO $ bytesToHeaderName headerKey
157+ headerKeyBytes <- readNBytes $ fromIntegral n
158+ let headerKey = decodeUtf8Lenient headerKeyBytes
159+ headerName <- liftIO $ bytesToHeaderName headerKey
149160
150− bs2 <- readNBytes 1
151− headerValueType :: Word8 <- liftIO $ parseBinary bs2
152− when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
161+ bs2 <- readNBytes 1
162+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
163+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
153164
154− bs3 <- readNBytes 2
155− vLen :: Word16 <- liftIO $ parseBinary bs3
156− headerValueBytes <- readNBytes $ fromIntegral vLen
157− let headerValue = decodeUtf8Lenient headerValueBytes
158− m = (headerName, headerValue)
159− k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
165+ bs3 <- readNBytes 2
166+ vLen :: Word16 <- liftIO $ parseBinary bs3
167+ headerValueBytes <- readNBytes $ fromIntegral vLen
168+ let headerValue = decodeUtf8Lenient headerValueBytes
169+ m = (headerName, headerValue)
170+ k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
160171
161− ms <- parseHeaders (hdrLen - k)
162− return (m : ms)
172+ ms <- parseHeaders (hdrLen - k)
173+ return (m:ms)
163174
164175-- readNBytes returns N bytes read from the string and throws an
165176-- exception if N bytes are not present on the stream.
166−readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
177+readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
167178readNBytes n = do
168− b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
169− if B.length b /= n
170− then throwIO ESEUnexpectedEndOfStream
171− else return b
172−
173−crcCheck ::
174− (MonadUnliftIO m) =>
175− C.ConduitM ByteString ByteString m ()
179+ b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
180+ if B.length b /= n
181+ then throwIO ESEUnexpectedEndOfStream
182+ else return b
183+
184+crcCheck :: MonadUnliftIO m
185+ => C.ConduitM ByteString ByteString m ()
176186crcCheck = do
177− b <- readNBytes 12
178− n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
179− preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
180− when (crc32 (B.take 8 b) /= preludeCRC) $
181− throwIO ESEPreludeCRCFailed
187+ b <- readNBytes 12
188+ n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
189+ preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
190+ when (crc32 (B.take 8 b) /= preludeCRC) $
191+ throwIO ESEPreludeCRCFailed
182192
183− -- we do not yield the checksum
184− C.yield $ B.take 8 b
193+ -- we do not yield the checksum
194+ C.yield $ B.take 8 b
185195
186− -- 12 bytes have been read off the current message. Now read the
187− -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
188− let startCrc = crc32 b
189− finalCrc <- accumulateYield (fromIntegral n - 16) startCrc
196+ -- 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.
198+ let startCrc = crc32 b
199+ finalCrc <- accumulateYield (fromIntegral n-16) startCrc
190200
191− bs <- readNBytes 4
192− expectedCrc :: Word32 <- liftIO $ parseBinary bs
201+ bs <- readNBytes 4
202+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
193203
194− when (finalCrc /= expectedCrc) $
195− throwIO ESEMessageCRCFailed
204+ when (finalCrc /= expectedCrc) $
205+ throwIO ESEMessageCRCFailed
196206
197− -- we unconditionally recurse - downstream figures out when to
198− -- quit reading the stream
199− crcCheck
207+ -- we unconditionally recurse - downstream figures out when to
208+ -- quit reading the stream
209+ crcCheck
200210 where
201211 accumulateYield n checkSum = do
202− let toRead = min n chunkSize
203− b <- readNBytes toRead
204− let c' = crc32Update checkSum b
205− n' = n - B.length b
206− C.yield b
207− if n' > 0
208− then accumulateYield n' c'
209− else return c'
210−
211−handleMessage :: (MonadUnliftIO m) => C.ConduitT ByteString EventMessage m ()
212+ let toRead = min n chunkSize
213+ b <- readNBytes toRead
214+ let c' = crc32Update checkSum b
215+ n' = n - B.length b
216+ C.yield b
217+ if n' > 0
218+ then accumulateYield n' c'
219+ else return c'
220+
221+handleMessage :: MonadUnliftIO m => C.ConduitT ByteString EventMessage m ()
212222handleMessage = do
213− b1 <- readNBytes 4
214− msgLen :: Word32 <- liftIO $ parseBinary b1
215−
216− b2 <- readNBytes 4
217− hdrLen :: Word32 <- liftIO $ parseBinary b2
218−
219− hs <- parseHeaders hdrLen
220−
221− let payloadLen = msgLen - hdrLen - 16
222− getHdrVal h = fmap snd . find ((h ==) . fst)
223− eventHdrValue = getHdrVal EventType hs
224− msgHdrValue = getHdrVal MessageType hs
225− errCode = getHdrVal ErrorCode hs
226− errMsg = getHdrVal ErrorMessage hs
227−
228− case msgHdrValue of
229− Just "event" -> do
230− case eventHdrValue of
231− Just "Records" -> passThrough $ fromIntegral payloadLen
232− Just "Cont" -> return ()
233− Just "Progress" -> do
234− bs <- readNBytes $ fromIntegral payloadLen
235− progress <- parseSelectProgress bs
236− C.yield $ ProgressEventMessage progress
237− Just "Stats" -> do
238− bs <- readNBytes $ fromIntegral payloadLen
239− stats <- parseSelectProgress bs
240− C.yield $ StatsEventMessage stats
241− Just "End" -> return ()
242− _ -> throwIO ESEInvalidMessageType
243− when (eventHdrValue /= Just "End") handleMessage
244− Just "error" -> do
245− let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
246− maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
247− _ -> throwIO ESEInvalidMessageType
223+ b1 <- readNBytes 4
224+ msgLen :: Word32 <- liftIO $ parseBinary b1
225+
226+ b2 <- readNBytes 4
227+ hdrLen :: Word32 <- liftIO $ parseBinary b2
228+
229+ hs <- parseHeaders hdrLen
230+
231+ let payloadLen = msgLen - hdrLen - 16
232+ getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)
233+ eventHdrValue = getHdrVal EventType hs
234+ msgHdrValue = getHdrVal MessageType hs
235+ errCode = getHdrVal ErrorCode hs
236+ errMsg = getHdrVal ErrorMessage hs
237+
238+ case msgHdrValue of
239+ Just "event" -> do
240+ case eventHdrValue of
241+ Just "Records" -> passThrough $ fromIntegral payloadLen
242+ Just "Cont" -> return ()
243+ Just "Progress" -> do
244+ bs <- readNBytes $ fromIntegral payloadLen
245+ progress <- parseSelectProgress bs
246+ C.yield $ ProgressEventMessage progress
247+ Just "Stats" -> do
248+ bs <- readNBytes $ fromIntegral payloadLen
249+ stats <- parseSelectProgress bs
250+ C.yield $ StatsEventMessage stats
251+ Just "End" -> return ()
252+ _ -> throwIO ESEInvalidMessageType
253+ when (eventHdrValue /= Just "End") handleMessage
254+
255+ Just "error" -> do
256+ let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
257+ maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
258+
259+ _ -> throwIO ESEInvalidMessageType
260+
248261 where
249262 passThrough 0 = return ()
250263 passThrough n = do
251− let c = min n chunkSize
252− b <- readNBytes c
253− C.yield $ RecordPayloadEventMessage b
254− passThrough $ n - B.length b
255−
256−selectProtoConduit ::
257− (MonadUnliftIO m) =>
258− C.ConduitT ByteString EventMessage m ()
264+ let c = min n chunkSize
265+ b <- readNBytes c
266+ C.yield $ RecordPayloadEventMessage b
267+ passThrough $ n - B.length b
268+
269+
270+selectProtoConduit :: MonadUnliftIO m
271+ => C.ConduitT ByteString EventMessage m ()
259272selectProtoConduit = crcCheck .| handleMessage
260273
261274-- | selectObjectContent calls the SelectRequest on the given
262275-- object. It returns a Conduit of event messages that can be consumed
263276-- by the client.
264−selectObjectContent ::
265− Bucket ->
266− Object ->
267− SelectRequest ->
268− Minio (C.ConduitT () EventMessage Minio ())
277+selectObjectContent :: Bucket -> Object -> SelectRequest
278+ -> Minio (C.ConduitT () EventMessage Minio ())
269279selectObjectContent b o r = do
270− let reqInfo =
271− defaultS3ReqInfo
272− { riMethod = HT.methodPost,
273− riBucket = Just b,
274− riObject = Just o,
275− riPayload = PayloadBS $ mkSelectRequest r,
276− riNeedsLocation = False,
277− riQueryParams = [("select", Nothing), ("select-type", Just "2")]
278− }
279− -- print $ mkSelectRequest r
280− resp <- mkStreamRequest reqInfo
281− return $ NC.responseBody resp .| selectProtoConduit
280+ let reqInfo = defaultS3ReqInfo { riMethod = HT.methodPost
281+ , riBucket = Just b
282+ , riObject = Just o
283+ , riPayload = PayloadBS $ mkSelectRequest r
284+ , riNeedsLocation = False
285+ , riQueryParams = [("select", Nothing), ("select-type", Just "2")]
286+ }
287+ --print $ mkSelectRequest r
288+ resp <- mkStreamRequest reqInfo
289+ return $ NC.responseBody resp .| selectProtoConduit
282290
283291-- | A helper conduit that returns only the record payload bytes.
284−getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
292+getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
285293getPayloadBytes = do
286− evM <- C.await
287− case evM of
288− Just v -> do
289− case v of
290− RecordPayloadEventMessage b -> C.yield b
291− RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
292− _ -> return ()
293− getPayloadBytes
294− Nothing -> return ()
294+ evM <- C.await
295+ case evM of
296+ Just v -> do
297+ case v of
298+ RecordPayloadEventMessage b -> C.yield b
299+ RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
300+ _ -> return ()
301+ getPayloadBytes
302+ Nothing -> return ()
src/Network/Minio/Sign/V4.hs+329−432
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,94 +13,88 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE BangPatterns #-}
17−
18−module Network.Minio.Sign.V4
19− ( SignParams (..),
20− signV4QueryParams,
21− signV4,
22− signV4PostPolicy,
23− signV4Stream,
24− Service (..),
25− credentialScope,
26− )
27−where
28−
29−import qualified Conduit as C
30−import qualified Data.ByteArray as BA
31−import qualified Data.ByteString as B
32−import qualified Data.ByteString.Base64 as Base64
33−import qualified Data.ByteString.Char8 as B8
34−import qualified Data.ByteString.Lazy as LB
35−import Data.CaseInsensitive (mk)
36−import qualified Data.CaseInsensitive as CI
37−import qualified Data.HashMap.Strict as Map
38−import qualified Data.HashSet as Set
39−import Data.List (partition)
40−import qualified Data.List.NonEmpty as NE
41−import qualified Data.Time as Time
42−import Lib.Prelude
43−import qualified Network.HTTP.Conduit as NC
44−import Network.HTTP.Types (Header, SimpleQuery, hContentEncoding, parseQuery)
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
51−import Text.Printf (printf)
16+
17+module Network.Minio.Sign.V4 where
18+
19+import qualified Conduit as C
20+import qualified Data.ByteString as B
21+import qualified Data.ByteString.Base64 as Base64
22+import qualified Data.ByteString.Char8 as B8
23+import Data.CaseInsensitive (mk)
24+import qualified Data.CaseInsensitive as CI
25+import qualified Data.Map.Strict as Map
26+import qualified Data.Set as Set
27+import qualified Data.Time as Time
28+import qualified Network.HTTP.Conduit as NC
29+import Network.HTTP.Types (Header, parseQuery)
30+import qualified Network.HTTP.Types as H
31+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
5239
5340-- these headers are not included in the string to sign when signing a
5441-- request
55−ignoredHeaders :: Set.HashSet ByteString
56−ignoredHeaders =
57− Set.fromList $
58− map
59− CI.foldedCase
60− [ H.hAuthorization,
61− H.hContentType,
62− H.hUserAgent
63− ]
64−
65−data Service = ServiceS3 | ServiceSTS
66− deriving stock (Eq, Show)
67−
68−toByteString :: Service -> ByteString
69−toByteString ServiceS3 = "s3"
70−toByteString ServiceSTS = "sts"
71−
72−data SignParams = SignParams
73− { spAccessKey :: Text,
74− spSecretKey :: BA.ScrubbedBytes,
75− spSessionToken :: Maybe BA.ScrubbedBytes,
76− spService :: Service,
77− spTimeStamp :: UTCTime,
78− spRegion :: Maybe Text,
79− spExpirySecs :: Maybe UrlExpiry,
80− spPayloadHash :: Maybe ByteString
81− }
82− deriving stock (Show)
42+ignoredHeaders :: Set ByteString
43+ignoredHeaders = Set.fromList $ map CI.foldedCase
44+ [ H.hAuthorization
45+ , H.hContentType
46+ , H.hUserAgent
47+ ]
48+
49+data SignV4Data = SignV4Data {
50+ sv4SignTime :: UTCTime
51+ , sv4Scope :: ByteString
52+ , sv4CanonicalRequest :: ByteString
53+ , sv4HeadersToSign :: [(ByteString, ByteString)]
54+ , sv4Output :: [(ByteString, ByteString)]
55+ , sv4StringToSign :: ByteString
56+ , sv4SigningKey :: ByteString
57+ } deriving (Show)
58+
59+data SignParams = SignParams {
60+ spAccessKey :: Text
61+ , spSecretKey :: Text
62+ , spTimeStamp :: UTCTime
63+ , spRegion :: Maybe Text
64+ , spExpirySecs :: Maybe Int
65+ , spPayloadHash :: Maybe ByteString
66+ } deriving (Show)
67+
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 ""
8384
8485mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
8586mkAuthHeader accessKey scope signedHeaderKeys sign =
86− let authValue =
87− B.concat
88− [ "AWS4-HMAC-SHA256 Credential=",
89− encodeUtf8 accessKey,
90− "/",
91− scope,
92− ", SignedHeaders=",
93− signedHeaderKeys,
94− ", Signature=",
95− sign
96− ]
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"
87+ let authValue = B.concat
88+ [ "AWS4-HMAC-SHA256 Credential="
89+ , toS accessKey
90+ , "/"
91+ , scope
92+ , ", SignedHeaders="
93+ , signedHeaderKeys
94+ , ", Signature="
95+ , sign
96+ ]
97+ in (H.hAuthorization, authValue)
10498
10599-- | Given SignParams and request details, including request method,
106100-- request path, headers, query params and payload hash, generates an
@@ -114,212 +108,124 @@ amzSecurityToken = "X-Amz-Security-Token"
114108-- is being created. The expiry is interpreted as an integer number of
115109-- seconds. The output will be the list of query-parameters to add to
116110-- 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.
161−signV4 :: SignParams -> NC.Request -> [Header]
111+signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
162112signV4 !sp !req =
163− let scope = credentialScope sp
164−
165− -- extra headers to be added for signing purposes.
166− extraHeaders =
167− ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp)
168− : ( -- payload hash is only used for S3 (not STS)
169− [ ( "x-amz-content-sha256",
170− fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
171− )
172− | spService sp == ServiceS3
173− ]
174− )
175− ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
176−
177− -- 1. compute canonical request
178− reqHeaders = NC.requestHeaders req ++ extraHeaders
179− (canonicalRequest, signedHeaderKeys) =
180− getCanonicalRequestAndSignedHeaders
181− NotStreaming
182− sp
183− req
184− reqHeaders
185−
186− -- 2. compute string to sign
187− stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
188− -- 3.1 compute signing key
189− signingKey = getSigningKey sp
190− -- 3.2 compute signature
191− signature = computeSignature stringToSign signingKey
192− -- 4. compute auth header
193− authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
194− in authHeader : extraHeaders
195−
196−credentialScope :: SignParams -> ByteString
197−credentialScope sp =
198− let region = fromMaybe "" $ spRegion sp
199− in B.intercalate
200− "/"
201− [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" $ spTimeStamp sp,
202− encodeUtf8 region,
203− toByteString $ spService sp,
204− "aws4_request"
205− ]
113+ let
114+ region = fromMaybe "" $ spRegion sp
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
122+ datePair = ("X-Amz-Date", awsTimeFormatBS ts)
123+ computedHeaders = NC.requestHeaders req ++
124+ if isJust $ expiry
125+ then []
126+ else [(\(x, y) -> (mk x, y)) datePair]
127+ headersToSign = getHeadersToSign computedHeaders
128+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
129+
130+ -- query-parameters to be added before signing for presigned URLs
131+ -- (i.e. when `isJust expiry`)
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
144+ canonicalRequest = mkCanonicalRequest False sp
145+ (NC.setQueryString finalQP req)
146+ headersToSign
147+
148+ -- 2. compute string to sign
149+ stringToSign = mkStringToSign ts scope canonicalRequest
150+
151+ -- 3.1 compute signing key
152+ signingKey = mkSigningKey ts region secretKey
153+
154+ -- 3.2 compute signature
155+ signature = computeSignature stringToSign signingKey
156+
157+ -- 4. compute auth header
158+ authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
159+
160+ -- finally compute output pairs
161+ sha256Hdr = ("x-amz-content-sha256",
162+ fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp)
163+ output = if isJust expiry
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 "/"
173+ [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts
174+ , toS region
175+ , "s3"
176+ , "aws4_request"
177+ ]
206178
207−-- Folds header name, trims whitespace in header values, skips ignored headers
208−-- and sorts headers.
209179getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
210180getHeadersToSign !h =
211− filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
212− map (bimap CI.foldedCase stripBS) h
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 ->
274− [(ByteString, ByteString)] ->
275− ByteString
181+ filter (flip Set.notMember ignoredHeaders . fst) $
182+ map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
183+
184+mkCanonicalRequest :: Bool -> SignParams -> NC.Request -> [(ByteString, ByteString)]
185+ -> ByteString
276186mkCanonicalRequest !isStreaming !sp !req !headersForSign =
277− let httpMethod = NC.method req
278− canonicalUri = uriEncode False $ NC.path req
279− canonicalQueryString =
280− B.intercalate "&" $
281− map (\(x, y) -> B.concat [x, "=", y]) $
282− sortBy (\a b -> compare (fst a) (fst b)) $
283− map
284− ( bimap (uriEncode True) (maybe "" (uriEncode True))
285− )
286− (parseQuery $ NC.queryString req)
287− sortedHeaders = sort headersForSign
288− canonicalHeaders =
289− B.concat $
290− map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
291− signedHeaders = B.intercalate ";" $ map fst sortedHeaders
292− payloadHashStr =
293− if isStreaming
294− then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
295− else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
296− in B.intercalate
297− "\n"
298− [ httpMethod,
299− canonicalUri,
300− canonicalQueryString,
301− canonicalHeaders,
302− signedHeaders,
303− payloadHashStr
304− ]
187+ let
188+ canonicalQueryString = B.intercalate "&" $
189+ map (\(x, y) -> B.concat [x, "=", y]) $
190+ sort $ map (\(x, y) ->
191+ (uriEncode True x, maybe "" (uriEncode True) y)) $
192+ (parseQuery $ NC.queryString req)
305193
306−mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
307−mkStringToSign ts !scope !canonicalRequest =
308− B.intercalate
309− "\n"
310− [ "AWS4-HMAC-SHA256",
311− awsTimeFormatBS ts,
312− scope,
313− hashSHA256 canonicalRequest
194+ sortedHeaders = sort headersForSign
195+
196+ canonicalHeaders = B.concat $
197+ map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
198+
199+ signedHeaders = B.intercalate ";" $ map fst sortedHeaders
200+
201+ payloadHashStr =
202+ if isStreaming
203+ then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
204+ else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
205+ in
206+ B.intercalate "\n"
207+ [ NC.method req
208+ , uriEncode False $ NC.path req
209+ , canonicalQueryString
210+ , canonicalHeaders
211+ , signedHeaders
212+ , payloadHashStr
314213 ]
315214
316−getSigningKey :: SignParams -> ByteString
317−getSigningKey sp =
318− hmacSHA256RawBS "aws4_request"
319− . hmacSHA256RawBS (toByteString $ spService sp)
320− . hmacSHA256RawBS (encodeUtf8 $ fromMaybe "" $ spRegion sp)
321− . hmacSHA256RawBS (awsDateFormatBS $ spTimeStamp sp)
322− $ B.concat ["AWS4", BA.convert $ spSecretKey sp]
215+mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
216+mkStringToSign ts !scope !canonicalRequest = B.intercalate "\n"
217+ [ "AWS4-HMAC-SHA256"
218+ , awsTimeFormatBS ts
219+ , scope
220+ , hashSHA256 canonicalRequest
221+ ]
222+
223+mkSigningKey :: UTCTime -> Text -> ByteString -> ByteString
224+mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"
225+ . hmacSHA256RawBS "s3"
226+ . hmacSHA256RawBS (toS region)
227+ . hmacSHA256RawBS (awsDateFormatBS ts)
228+ $ B.concat ["AWS4", secretKey]
323229
324230computeSignature :: ByteString -> ByteString -> ByteString
325231computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -327,168 +233,159 @@ computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
327233-- | Takes a validated Post Policy JSON bytestring, the signing time,
328234-- and ConnInfo and returns form-data for the POST upload containing
329235-- just the signature and the encoded post-policy.
330−signV4PostPolicy ::
331− ByteString ->
332− SignParams ->
333− Map.HashMap Text ByteString
236+signV4PostPolicy :: ByteString -> SignParams
237+ -> Map.Map Text ByteString
334238signV4PostPolicy !postPolicyJSON !sp =
335− let stringToSign = Base64.encode postPolicyJSON
336− signingKey = getSigningKey sp
337− signature = computeSignature stringToSign signingKey
338− in Map.fromList $
339− [ ("x-amz-signature", signature),
340− ("policy", stringToSign)
341− ]
342− ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
239+ let
240+ stringToSign = Base64.encode postPolicyJSON
241+ region = fromMaybe "" $ spRegion sp
242+ signingKey = mkSigningKey (spTimeStamp sp) region $ toS $ spSecretKey sp
243+ signature = computeSignature stringToSign signingKey
244+ in
245+ Map.fromList [ ("x-amz-signature", signature)
246+ , ("policy", stringToSign)
247+ ]
343248
344249chunkSizeConstant :: Int
345250chunkSizeConstant = 64 * 1024
346251
347252-- base16Len computes the number of bytes required to represent @n (> 0)@ in
348253-- hexadecimal.
349−base16Len :: (Integral a) => a -> Int
350−base16Len n
351− | n == 0 = 0
352− | otherwise = 1 + base16Len (n `div` 16)
254+base16Len :: Integral a => a -> Int
255+base16Len n | n == 0 = 0
256+ | otherwise = 1 + base16Len (n `div` 16)
353257
354258signedStreamLength :: Int64 -> Int64
355259signedStreamLength dataLen =
356− let chunkSzInt = fromIntegral chunkSizeConstant
357− (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
358− -- Structure of a chunk:
359− -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
360− encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
361− fullChunkSize = encodedChunkLen chunkSzInt
362− lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
363− finalChunkSize = 1 + 17 + 64 + 2 + 2
364− in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
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−
377−signV4Stream ::
378− Int64 ->
379− SignParams ->
380− NC.Request ->
381− (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
260+ let
261+ chunkSzInt = fromIntegral chunkSizeConstant
262+ (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
263+
264+
265+ -- Structure of a chunk:
266+ -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
267+ encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
268+ fullChunkSize = encodedChunkLen chunkSzInt
269+ lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
270+ finalChunkSize = 1 + 17 + 64 + 2 + 2
271+ in
272+ numChunks * fullChunkSize + lastChunkSize + finalChunkSize
273+
274+signV4Stream :: Int64 -> SignParams -> NC.Request
275+ -> (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
276+ -- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
382277signV4Stream !payloadLength !sp !req =
383− let ts = spTimeStamp sp
384−
385− -- compute the updated list of headers to be added for signing purposes.
386− signedContentLength = signedStreamLength payloadLength
387− extraHeaders =
388− [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
389− ("x-amz-decoded-content-length", showBS payloadLength),
390− ("content-length", showBS signedContentLength),
391− ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
278+ let
279+ ts = spTimeStamp sp
280+
281+ addContentEncoding hs =
282+ let ceMay = headMay $ filter (\(x, _) -> x == "content-encoding") hs
283+ in case ceMay of
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)
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)
298+ , ("content-length", show signedContentLength )
299+ , ("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
392346 ]
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− ]
435− -- Read n byte from upstream and return a strict bytestring.
436− mustTakeN n = do
437− bs <- LB.toStrict <$> (C.takeCE n C..| C.sinkLazy)
347+
348+ -- Read n byte from upstream and return a strict bytestring.
349+ mustTakeN n = do
350+ bs <- toS <$> (C.takeCE n C..| C.sinkLazy)
438351 when (B.length bs /= n) $
439− throwIO MErrVStreamingBodyUnexpectedEOF
352+ throwIO MErrVStreamingBodyUnexpectedEOF
440353 return bs
441− signerConduit n lps prevSign =
442− -- First case encodes a full chunk of length
443− -- 'chunkSizeConstant'.
444− if
445− | n > 0 -> do
446− bs <- mustTakeN chunkSizeConstant
447− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
448− nextSign = computeSignature strToSign signingKey
449− chunkBS =
450− toHexStr chunkSizeConstant
451− <> ";chunk-signature="
452− <> nextSign
453− <> "\r\n"
454− <> bs
455− <> "\r\n"
456− C.yield chunkBS
457− signerConduit (n - 1) lps nextSign
458−
459− -- Second case encodes the last chunk which is smaller than
460− -- 'chunkSizeConstant'
461− | lps > 0 -> do
462− bs <- mustTakeN $ fromIntegral lps
463− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
464− nextSign = computeSignature strToSign signingKey
465− chunkBS =
466− toHexStr lps
467− <> ";chunk-signature="
468− <> nextSign
469− <> "\r\n"
470− <> bs
471− <> "\r\n"
472− C.yield chunkBS
473− signerConduit 0 0 nextSign
474−
475− -- Last case encodes the final signature chunk that has no
476− -- data.
477− | otherwise -> do
478− let strToSign = chunkStrToSign prevSign (hashSHA256 "")
479− nextSign = computeSignature strToSign signingKey
480− lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
481− C.yield lastChunkBS
482− in \src ->
483− req
484− { NC.requestHeaders = finalReqHeaders,
485− NC.requestBody =
486− NC.requestBodySource signedContentLength $
487− src C..| signerConduit numParts lastPSize seedSignature
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'
354+
355+ signerConduit n lps prevSign =
356+ -- First case encodes a full chunk of length
357+ -- 'chunkSizeConstant'.
358+ if | n > 0 -> do
359+ bs <- mustTakeN chunkSizeConstant
360+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
361+ nextSign = computeSignature strToSign signingKey
362+ chunkBS = toHexStr chunkSizeConstant
363+ <> ";chunk-signature="
364+ <> nextSign <> "\r\n" <> bs <> "\r\n"
365+ C.yield chunkBS
366+ signerConduit (n-1) lps nextSign
367+
368+ -- Second case encodes the last chunk which is smaller than
369+ -- 'chunkSizeConstant'
370+ | lps > 0 -> do
371+ bs <- mustTakeN $ fromIntegral lps
372+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
373+ nextSign = computeSignature strToSign signingKey
374+ chunkBS = toHexStr lps <> ";chunk-signature="
375+ <> nextSign <> "\r\n" <> bs <> "\r\n"
376+ C.yield chunkBS
377+ signerConduit 0 0 nextSign
378+
379+ -- Last case encodes the final signature chunk that has no
380+ -- data.
381+ | otherwise -> do
382+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")
383+ nextSign = computeSignature strToSign signingKey
384+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
385+ C.yield lastChunkBS
386+ in
387+ \src -> req { NC.requestHeaders = finalReqHeaders
388+ , NC.requestBody =
389+ NC.requestBodySource signedContentLength $
390+ src C..| signerConduit numParts lastPSize seedSignature
391+ }
src/Network/Minio/Utils.hs+123−150
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -16,77 +16,71 @@
1616
1717module Network.Minio.Utils where
1818
19−import qualified Conduit as C
20−import Control.Monad.IO.Unlift (MonadUnliftIO)
21−import qualified Control.Monad.Trans.Resource as R
22−import qualified Data.ByteString as B
23−import qualified Data.ByteString.Lazy as LB
24−import Data.CaseInsensitive (mk, original)
25−import qualified Data.Conduit.Binary as CB
26−import qualified Data.HashMap.Strict as H
27−import qualified Data.Text as T
28−import Data.Text.Read (decimal)
29−import Data.Time
30− ( defaultTimeLocale,
31− parseTimeM,
32− rfc822DateFormat,
33− )
34−import Lib.Prelude
35−import Network.HTTP.Conduit (Response)
36−import qualified Network.HTTP.Conduit as NC
37−import qualified Network.HTTP.Types as HT
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)
42−import qualified System.IO as IO
43−import qualified UnliftIO as U
44−import qualified UnliftIO.Async as A
45−
46−allocateReadFile ::
47− (MonadUnliftIO m, R.MonadResource m) =>
48− FilePath ->
49− m (R.ReleaseKey, Handle)
19+import qualified Conduit as C
20+import Control.Monad.IO.Unlift (MonadUnliftIO)
21+import qualified Control.Monad.Trans.Resource as R
22+import qualified Data.ByteString as B
23+import qualified Data.ByteString.Lazy as LB
24+import Data.CaseInsensitive (mk, original)
25+import qualified Data.Conduit.Binary as CB
26+import qualified Data.List as List
27+import qualified Data.Map as Map
28+import qualified Data.Text as T
29+import Data.Text.Encoding.Error (lenientDecode)
30+import Data.Text.Read (decimal)
31+import Data.Time (defaultTimeLocale, parseTimeM,
32+ rfc822DateFormat)
33+import Network.HTTP.Conduit (Response)
34+import qualified Network.HTTP.Conduit as NC
35+import qualified Network.HTTP.Types as HT
36+import qualified Network.HTTP.Types.Header as Hdr
37+import qualified System.IO as IO
38+import qualified UnliftIO as U
39+import qualified UnliftIO.Async as A
40+import qualified UnliftIO.MVar as UM
41+
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)
50+ => FilePath -> m (R.ReleaseKey, Handle)
5051allocateReadFile fp = do
5152 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
52− either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
53+ either (\(e :: IOException) -> throwIO e) (return . (rk,)) hdlE
5354 where
5455 openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
5556 cleanup = either (const $ return ()) IO.hClose
5657
5758-- | Queries the file size from the handle. Catches any file operation
5859-- exceptions and returns Nothing instead.
59−getFileSize ::
60− (MonadUnliftIO m) =>
61− Handle ->
62− m (Maybe Int64)
60+getFileSize :: (MonadUnliftIO m, R.MonadResource m)
61+ => Handle -> m (Maybe Int64)
6362getFileSize h = do
6463 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
6564 case resE of
66− Left (_ :: U.IOException) -> return Nothing
67− Right s -> return $ Just s
65+ Left (_ :: IOException) -> return Nothing
66+ Right s -> return $ Just s
6867
6968-- | Queries if handle is seekable. Catches any file operation
7069-- exceptions and return False instead.
71−isHandleSeekable ::
72− (R.MonadResource m) =>
73− Handle ->
74− m Bool
70+isHandleSeekable :: (R.MonadResource m, MonadUnliftIO m)
71+ => Handle -> m Bool
7572isHandleSeekable h = do
7673 resE <- liftIO $ try $ IO.hIsSeekable h
7774 case resE of
78− Left (_ :: U.IOException) -> return False
79− Right v -> return v
75+ Left (_ :: IOException) -> return False
76+ Right v -> return v
8077
8178-- | Helper function that opens a handle to the filepath and performs
8279-- the given action on it. Exceptions of type MError are caught and
8380-- returned - both during file handle allocation and when the action
8481-- is run.
85−withNewHandle ::
86− (MonadUnliftIO m, R.MonadResource m) =>
87− FilePath ->
88− (Handle -> m a) ->
89− m (Either U.IOException a)
82+withNewHandle :: (MonadUnliftIO m, R.MonadResource m)
83+ => FilePath -> (Handle -> m a) -> m (Either IOException a)
9084withNewHandle fp fileAction = do
9185 -- opening a handle can throw MError exception.
9286 handleE <- try $ allocateReadFile fp
@@ -100,61 +94,19 @@ withNewHandle fp fileAction = do
10094 return resE
10195
10296mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
103−mkHeaderFromPairs = map (first mk)
97+mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
10498
10599lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
106−lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
100+lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
107101
108102getETagHeader :: [HT.Header] -> Maybe Text
109103getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
110104
111105getMetadata :: [HT.Header] -> [(Text, Text)]
112−getMetadata =
113− map (\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y))
114−
115−-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
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_
106+getMetadata = map ((\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y)))
107+
108+getMetadataMap :: [HT.Header] -> Map Text Text
109+getMetadataMap hs = Map.fromList (getMetadata hs)
158110
159111getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
160112getLastModifiedHeader hs = do
@@ -164,21 +116,19 @@ getLastModifiedHeader hs = do
164116getContentLength :: [HT.Header] -> Maybe Int64
165117getContentLength hs = do
166118 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
167− fst <$> either (const Nothing) Just (decimal nbs)
119+ fst <$> hush (decimal nbs)
120+
168121
169122decodeUtf8Lenient :: ByteString -> Text
170123decodeUtf8Lenient = decodeUtf8With lenientDecode
171124
172125isSuccessStatus :: HT.Status -> Bool
173−isSuccessStatus sts =
174− let s = HT.statusCode sts
175− in (s >= 200 && s < 300)
176−
177−httpLbs ::
178− (MonadIO m) =>
179− NC.Request ->
180− NC.Manager ->
181− m (NC.Response LByteString)
126+isSuccessStatus sts = let s = HT.statusCode sts
127+ in (s >= 200 && s < 300)
128+
129+httpLbs :: MonadIO m
130+ => NC.Request -> NC.Manager
131+ -> m (NC.Response LByteString)
182132httpLbs req mgr = do
183133 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
184134 resp <- either throwIO return respE
@@ -190,26 +140,21 @@ httpLbs req mgr = do
190140 Just "application/json" -> do
191141 sErr <- parseErrResponseJSON $ NC.responseBody resp
192142 throwIO sErr
193− _ ->
194− throwIO $
195− NC.HttpExceptionRequest req $
196− NC.StatusCodeException (void resp) (showBS resp)
143+
144+ _ -> throwIO $ NC.HttpExceptionRequest req $
145+ NC.StatusCodeException (void resp) (show resp)
197146
198147 return resp
199148 where
200− tryHttpEx ::
201− IO (NC.Response LByteString) ->
202− IO (Either NC.HttpException (NC.Response LByteString))
149+ tryHttpEx :: IO (NC.Response LByteString)
150+ -> IO (Either NC.HttpException (NC.Response LByteString))
203151 tryHttpEx = try
204− contentTypeMay resp =
205− lookupHeader Hdr.hContentType $
206− NC.responseHeaders resp
207−
208−http ::
209− (MonadUnliftIO m, R.MonadResource m) =>
210− NC.Request ->
211− NC.Manager ->
212− m (Response (C.ConduitT () ByteString m ()))
152+ contentTypeMay resp = lookupHeader Hdr.hContentType $
153+ NC.responseHeaders resp
154+
155+http :: (MonadUnliftIO m, R.MonadResource m)
156+ => NC.Request -> NC.Manager
157+ -> m (Response (C.ConduitT () ByteString m ()))
213158http req mgr = do
214159 respE <- tryHttpEx $ NC.http req mgr
215160 resp <- either throwIO return respE
@@ -219,31 +164,25 @@ http req mgr = do
219164 respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
220165 sErr <- parseErrResponse respBody
221166 throwIO sErr
167+
222168 _ -> do
223169 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
224− throwIO $
225− NC.HttpExceptionRequest req $
226− NC.StatusCodeException (void resp) content
170+ throwIO $ NC.HttpExceptionRequest req $
171+ NC.StatusCodeException (void resp) content
172+
227173
228174 return resp
229175 where
230− tryHttpEx ::
231− (MonadUnliftIO m) =>
232− m a ->
233− m (Either NC.HttpException a)
176+ tryHttpEx :: (MonadUnliftIO m) => m a
177+ -> m (Either NC.HttpException a)
234178 tryHttpEx = try
235− contentTypeMay resp =
236− lookupHeader Hdr.hContentType $
237− NC.responseHeaders resp
179+ contentTypeMay resp = lookupHeader Hdr.hContentType $
180+ NC.responseHeaders resp
238181
239182-- Similar to mapConcurrently but limits the number of threads that
240183-- can run using a quantity semaphore.
241−limitedMapConcurrently ::
242− (MonadUnliftIO m) =>
243− Int ->
244− (t -> m a) ->
245− [t] ->
246− m [a]
184+limitedMapConcurrently :: MonadUnliftIO m
185+ => Int -> (t -> m a) -> [t] -> m [a]
247186limitedMapConcurrently 0 _ _ = return []
248187limitedMapConcurrently count act args = do
249188 t' <- U.newTVarIO count
@@ -252,15 +191,17 @@ limitedMapConcurrently count act args = do
252191 where
253192 wThread t arg =
254193 U.bracket_ (waitSem t) (signalSem t) $ act arg
194+
255195 -- quantity semaphore implementation using TVar
256196 waitSem t = U.atomically $ do
257197 v <- U.readTVar t
258198 if v > 0
259− then U.writeTVar t (v - 1)
260− else U.retrySTM
199+ then U.writeTVar t (v-1)
200+ else U.retrySTM
201+
261202 signalSem t = U.atomically $ do
262203 v <- U.readTVar t
263− U.writeTVar t (v + 1)
204+ U.writeTVar t (v+1)
264205
265206-- helper function to 'drop' empty optional parameter.
266207mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -269,7 +210,7 @@ mkQuery k mv = (k,) <$> mv
269210-- helper function to build query parameters that are optional.
270211-- don't use it with mandatory query params with empty value.
271212mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
272−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
213+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
273214
274215-- | Conduit that rechunks bytestrings into the given chunk
275216-- lengths. Stops after given chunk lengths are yielded. Stops if
@@ -277,9 +218,41 @@ mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
277218-- received. Does not throw any errors.
278219chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
279220chunkBSConduit [] = return ()
280−chunkBSConduit (s : ss) = do
281− bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
282− if
283− | B.length bs == s -> C.yield bs >> chunkBSConduit ss
284− | B.length bs > 0 -> C.yield bs
285− | otherwise -> return ()
221+chunkBSConduit (s:ss) = do
222+ bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
223+ if | B.length bs == s -> C.yield bs >> chunkBSConduit ss
224+ | 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
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.hsdeleted−65
@@ -1,65 +0,0 @@
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+99−165
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,112 +15,89 @@
1515--
1616
1717module Network.Minio.XmlGenerator
18− ( mkCreateBucketConfig,
19− mkCompleteMultipartUploadRequest,
20− mkPutNotificationRequest,
21− mkSelectRequest,
22− )
23−where
18+ ( mkCreateBucketConfig
19+ , mkCompleteMultipartUploadRequest
20+ , mkPutNotificationRequest
21+ , mkSelectRequest
22+ ) where
23+
2424
2525import qualified Data.ByteString.Lazy as LBS
26−import qualified Data.Text as T
27−import Network.Minio.Data
28−import Network.Minio.XmlCommon
29−import Text.XML
26+import qualified Data.HashMap.Strict as H
27+import qualified Data.Text as T
28+import Text.XML
29+
30+import Lib.Prelude
31+
32+import Network.Minio.Data
33+
3034
3135-- | Create a bucketConfig request body XML
3236mkCreateBucketConfig :: Text -> Region -> ByteString
3337mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
3438 where
35− s3Element n = Element (s3Name ns n) mempty
36− root =
37− s3Element
38− "CreateBucketConfiguration"
39− [ NodeElement $
40− s3Element
41− "LocationConstraint"
42− [NodeContent location]
39+ s3Element n = Element (s3Name ns n) mempty
40+ root = s3Element "CreateBucketConfiguration"
41+ [ NodeElement $ s3Element "LocationConstraint"
42+ [ NodeContent location]
4343 ]
44− bucketConfig = Document (Prologue [] Nothing []) root []
44+ bucketConfig = Document (Prologue [] Nothing []) root []
4545
4646-- | Create a completeMultipartUpload request body XML
4747mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
4848mkCompleteMultipartUploadRequest partInfo =
4949 LBS.toStrict $ renderLBS def cmur
5050 where
51− root =
52− Element "CompleteMultipartUpload" mempty $
53− map (NodeElement . mkPart) partInfo
54− mkPart (n, etag) =
55− Element
56− "Part"
57− mempty
58− [ NodeElement $
59− Element
60− "PartNumber"
61− mempty
62− [NodeContent $ T.pack $ show n],
63− NodeElement $
64− Element
65− "ETag"
66− mempty
67− [NodeContent etag]
68− ]
51+ root = Element "CompleteMultipartUpload" mempty $
52+ map (NodeElement . mkPart) partInfo
53+ mkPart (n, etag) = Element "Part" mempty
54+ [ NodeElement $ Element "PartNumber" mempty
55+ [NodeContent $ T.pack $ show n]
56+ , NodeElement $ Element "ETag" mempty
57+ [NodeContent etag]
58+ ]
6959 cmur = Document (Prologue [] Nothing []) root []
7060
7161-- Simplified XML representation without element attributes.
72−data XNode
73− = XNode Text [XNode]
74− | XLeaf Text Text
75− deriving stock (Eq, Show)
62+data XNode = XNode Text [XNode]
63+ | XLeaf Text Text
64+ deriving (Eq, Show)
7665
7766toXML :: Text -> XNode -> ByteString
78−toXML ns node =
79− LBS.toStrict $
80− renderLBS def $
81− Document (Prologue [] Nothing []) (xmlNode node) []
67+toXML ns node = LBS.toStrict $ renderLBS def $
68+ Document (Prologue [] Nothing []) (xmlNode node) []
8269 where
8370 xmlNode :: XNode -> Element
84− xmlNode (XNode name nodes) =
85− Element (s3Name ns name) mempty $
86− map (NodeElement . xmlNode) nodes
87− xmlNode (XLeaf name content) =
88− Element
89− (s3Name ns name)
90− mempty
91− [NodeContent content]
71+ xmlNode (XNode name nodes) = Element (s3Name ns name) mempty $
72+ map (NodeElement . xmlNode) nodes
73+ xmlNode (XLeaf name content) = Element (s3Name ns name) mempty
74+ [NodeContent content]
9275
9376class ToXNode a where
9477 toXNode :: a -> XNode
9578
9679instance ToXNode Event where
97− toXNode = XLeaf "Event" . toText
80+ toXNode = XLeaf "Event" . show
9881
9982instance ToXNode Notification where
100− toXNode (Notification qc tc lc) =
101− XNode "NotificationConfiguration" $
102− map (toXNodesWithArnName "QueueConfiguration" "Queue") qc
103− ++ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc
104− ++ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
83+ toXNode (Notification qc tc lc) = XNode "NotificationConfiguration" $
84+ map (toXNodesWithArnName "QueueConfiguration" "Queue") qc ++
85+ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc ++
86+ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
10587
10688toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
107−toXNodesWithArnName eltName arnName (NotificationConfig itemId arn events fRule) =
108− XNode eltName $
109− [XLeaf "Id" itemId, XLeaf arnName arn]
110− ++ map toXNode events
111− ++ [toXNode fRule]
89+toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
90+ XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events ++
91+ [toXNode fRule]
11292
11393instance ToXNode Filter where
11494 toXNode (Filter (FilterKey (FilterRules rules))) =
11595 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
11696
11797getFRXNode :: FilterRule -> XNode
118−getFRXNode (FilterRule n v) =
119− XNode
120− "FilterRule"
121− [ XLeaf "Name" n,
122− XLeaf "Value" v
123− ]
98+getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
99+ , XLeaf "Value" v
100+ ]
124101
125102mkPutNotificationRequest :: Text -> Notification -> ByteString
126103mkPutNotificationRequest ns = toXML ns . toXNode
@@ -129,103 +106,60 @@ mkSelectRequest :: SelectRequest -> ByteString
129106mkSelectRequest r = LBS.toStrict $ renderLBS def sr
130107 where
131108 sr = Document (Prologue [] Nothing []) root []
132− root =
133− Element "SelectRequest" mempty $
134− [ NodeElement
135− ( Element
136− "Expression"
137− mempty
138− [NodeContent $ srExpression r]
139− ),
140− NodeElement
141− ( Element
142− "ExpressionType"
143− mempty
144− [NodeContent $ show $ srExpressionType r]
145− ),
146− NodeElement
147− ( Element "InputSerialization" mempty $
148− inputSerializationNodes $
149− srInputSerialization r
150− ),
151− NodeElement
152− ( Element "OutputSerialization" mempty $
153− outputSerializationNodes $
154− srOutputSerialization r
155− )
156− ]
157− ++ maybe [] reqProgElem (srRequestProgressEnabled r)
158− reqProgElem enabled =
159− [ NodeElement
160− ( Element
161− "RequestProgress"
162− mempty
163− [ NodeElement
164− ( Element
165− "Enabled"
166− mempty
167− [ NodeContent
168− (if enabled then "TRUE" else "FALSE")
169− ]
170− )
171− ]
172− )
173− ]
174− inputSerializationNodes is =
175− comprTypeNode (isCompressionType is)
176− ++ [NodeElement $ formatNode (isFormatInfo is)]
177− comprTypeNode (Just c) =
178− [ NodeElement $
179− Element
180− "CompressionType"
181− mempty
182− [ NodeContent $ case c of
183− CompressionTypeNone -> "NONE"
184− CompressionTypeGzip -> "GZIP"
185− CompressionTypeBzip2 -> "BZIP2"
186− ]
187− ]
109+ root = Element "SelectRequest" mempty $
110+ [ NodeElement (Element "Expression" mempty
111+ [NodeContent $ srExpression r])
112+ , NodeElement (Element "ExpressionType" mempty
113+ [NodeContent $ show $ srExpressionType r])
114+ , NodeElement (Element "InputSerialization" mempty $
115+ inputSerializationNodes $ srInputSerialization r)
116+ , NodeElement (Element "OutputSerialization" mempty $
117+ outputSerializationNodes $ srOutputSerialization r)
118+ ] ++ maybe [] reqProgElem (srRequestProgressEnabled r)
119+ reqProgElem enabled = [NodeElement
120+ (Element "RequestProgress" mempty
121+ [NodeElement
122+ (Element "Enabled" mempty
123+ [NodeContent
124+ (if enabled then "TRUE" else "FALSE")]
125+ )
126+ ]
127+ )
128+ ]
129+ inputSerializationNodes is = comprTypeNode (isCompressionType is) ++
130+ [NodeElement $ formatNode (isFormatInfo is)]
131+ comprTypeNode (Just c) = [NodeElement $ Element "CompressionType" mempty
132+ [NodeContent $ case c of
133+ CompressionTypeNone -> "NONE"
134+ CompressionTypeGzip -> "GZIP"
135+ CompressionTypeBzip2 -> "BZIP2"
136+ ]
137+ ]
188138 comprTypeNode Nothing = []
139+
189140 kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
190− formatNode (InputFormatCSV c) =
191− Element
192− "CSV"
193− mempty
194− (map (NodeElement . kvElement) (csvPropsList c))
141+ formatNode (InputFormatCSV (CSVProp h)) =
142+ Element "CSV" mempty
143+ (map NodeElement $ map kvElement $ H.toList h)
195144 formatNode (InputFormatJSON p) =
196− Element
197− "JSON"
198− mempty
199− [ NodeElement
200− ( Element
201− "Type"
202− mempty
203− [ NodeContent $ case jsonipType p of
204− JSONTypeDocument -> "DOCUMENT"
205− JSONTypeLines -> "LINES"
206− ]
207− )
208− ]
145+ Element "JSON" mempty
146+ [NodeElement
147+ (Element "Type" mempty
148+ [NodeContent $ case jsonipType p of
149+ JSONTypeDocument -> "DOCUMENT"
150+ JSONTypeLines -> "LINES"
151+ ]
152+ )
153+ ]
209154 formatNode InputFormatParquet = Element "Parquet" mempty []
155+
210156 outputSerializationNodes (OutputSerializationJSON j) =
211− [ NodeElement
212− ( Element "JSON" mempty $
213− rdElem $
214− jsonopRecordDelimiter j
215− )
216− ]
217− outputSerializationNodes (OutputSerializationCSV c) =
218− [ NodeElement $
219− Element
220− "CSV"
221− mempty
222− (map (NodeElement . kvElement) (csvPropsList c))
223− ]
157+ [NodeElement (Element "JSON" mempty $
158+ rdElem $ jsonopRecordDelimiter j)]
159+ outputSerializationNodes (OutputSerializationCSV (CSVProp h)) =
160+ [NodeElement $ Element "CSV" mempty
161+ (map NodeElement $ map kvElement $ H.toList h)]
162+
224163 rdElem Nothing = []
225− rdElem (Just t) =
226− [ NodeElement $
227− Element
228− "RecordDelimiter"
229− mempty
230− [NodeContent t]
231− ]
164+ rdElem (Just t) = [NodeElement $ Element "RecordDelimiter" mempty
165+ [NodeContent t]]
src/Network/Minio/XmlParser.hs+152−107
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,38 +15,75 @@
1515--
1616
1717module Network.Minio.XmlParser
18− ( parseListBuckets,
19− parseLocation,
20− parseNewMultipartUpload,
21− parseCompleteMultipartUploadResponse,
22− parseCopyObjectResponse,
23− parseListObjectsResponse,
24− parseListObjectsV1Response,
25− parseListUploadsResponse,
26− parseListPartsResponse,
27− parseErrResponse,
28− parseNotification,
29− parseSelectProgress,
30− )
31−where
18+ ( parseListBuckets
19+ , parseLocation
20+ , parseNewMultipartUpload
21+ , parseCompleteMultipartUploadResponse
22+ , parseCopyObjectResponse
23+ , parseListObjectsResponse
24+ , parseListObjectsV1Response
25+ , parseListUploadsResponse
26+ , parseListPartsResponse
27+ , parseErrResponse
28+ , parseNotification
29+ , parseSelectProgress
30+ ) where
3231
3332import qualified Data.ByteString.Lazy as LB
34−import qualified Data.HashMap.Strict as H
35−import Data.List (zip4, zip6)
36−import qualified Data.Text as T
37−import Data.Time
38−import Network.Minio.Data
39−import Network.Minio.XmlCommon
40−import Text.XML.Cursor hiding (bool)
33+import Data.List (zip3, zip4, zip5)
34+import qualified Data.Map as Map
35+import qualified Data.Text as T
36+import Data.Text.Read (decimal)
37+import Data.Time
38+import Text.XML
39+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
4177
4278-- | Parse the response XML of a list buckets call.
4379parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
4480parseListBuckets xmldata = do
4581 r <- parseRoot xmldata
4682 ns <- asks getSvcNamespace
47− let s3Elem' = s3Elem ns
48− names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
49− timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
83+ let
84+ s3Elem' = s3Elem ns
85+ names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
86+ timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
5087
5188 times <- mapM parseS3XMLTime timeStrings
5289 return $ zipWith BucketInfo names times
@@ -79,38 +116,40 @@ parseCopyObjectResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =
79116parseCopyObjectResponse xmldata = do
80117 r <- parseRoot xmldata
81118 ns <- asks getSvcNamespace
82− let s3Elem' = s3Elem ns
83− mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
119+ let
120+ s3Elem' = s3Elem ns
121+ mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
84122
85123 mtime <- parseS3XMLTime mtimeStr
86124 return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
87125
88126-- | Parse the response XML of a list objects v1 call.
89−parseListObjectsV1Response ::
90− (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
91− LByteString ->
92− m ListObjectsV1Result
127+parseListObjectsV1Response :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
128+ => LByteString -> m ListObjectsV1Result
93129parseListObjectsV1Response xmldata = do
94130 r <- parseRoot xmldata
95131 ns <- asks getSvcNamespace
96− let s3Elem' = s3Elem ns
97− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
98− nextMarker = listToMaybe $ r $/ s3Elem' "NextMarker" &/ content
99− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
100− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
101− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
102− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
103− -- if response xml contains empty etag response fill them with as
104− -- many empty Text for the zip4 below to work as intended.
105− etags = etagsList ++ repeat ""
106− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
132+ let
133+ s3Elem' = s3Elem ns
134+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
135+
136+ nextMarker = headMay $ r $/ s3Elem' "NextMarker" &/ content
137+
138+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
139+
140+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
141+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
142+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
143+ -- if response xml contains empty etag response fill them with as
144+ -- many empty Text for the zip4 below to work as intended.
145+ etags = etagsList ++ repeat ""
146+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
107147
108148 modTimes <- mapM parseS3XMLTime modTimeStr
109149 sizes <- parseDecimals sizeStr
110150
111− let objects =
112− map (uncurry6 ObjectInfo) $
113− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
151+ let
152+ objects = map (uncurry5 ObjectInfo) $ zip5 keys modTimes etags sizes (repeat Map.empty)
114153
115154 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
116155
@@ -119,24 +158,27 @@ parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
119158parseListObjectsResponse xmldata = do
120159 r <- parseRoot xmldata
121160 ns <- asks getSvcNamespace
122− let s3Elem' = s3Elem ns
123− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
124− nextToken = listToMaybe $ r $/ s3Elem' "NextContinuationToken" &/ content
125− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
126− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
127− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
128− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
129− -- if response xml contains empty etag response fill them with as
130− -- many empty Text for the zip4 below to work as intended.
131− etags = etagsList ++ repeat ""
132− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
161+ let
162+ s3Elem' = s3Elem ns
163+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
164+
165+ nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ content
166+
167+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
168+
169+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
170+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
171+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
172+ -- if response xml contains empty etag response fill them with as
173+ -- many empty Text for the zip4 below to work as intended.
174+ etags = etagsList ++ repeat ""
175+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
133176
134177 modTimes <- mapM parseS3XMLTime modTimeStr
135178 sizes <- parseDecimals sizeStr
136179
137− let objects =
138− map (uncurry6 ObjectInfo) $
139− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
180+ let
181+ objects = map (uncurry5 ObjectInfo) $ zip5 keys modTimes etags sizes (repeat Map.empty)
140182
141183 return $ ListObjectsResult hasMore nextToken objects prefixes
142184
@@ -145,18 +187,20 @@ parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
145187parseListUploadsResponse xmldata = do
146188 r <- parseRoot xmldata
147189 ns <- asks getSvcNamespace
148− let s3Elem' = s3Elem ns
149− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
150− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
151− nextKey = listToMaybe $ r $/ s3Elem' "NextKeyMarker" &/ content
152− nextUpload = listToMaybe $ r $/ s3Elem' "NextUploadIdMarker" &/ content
153− uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
154− uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
155− uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
190+ let
191+ s3Elem' = s3Elem ns
192+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
193+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
194+ nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content
195+ nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content
196+ uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
197+ uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
198+ uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
156199
157200 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
158201
159− let uploads = zip3 uploadKeys uploadIds uploadInitTimes
202+ let
203+ uploads = zip3 uploadKeys uploadIds uploadInitTimes
160204
161205 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
162206
@@ -164,25 +208,34 @@ parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
164208parseListPartsResponse xmldata = do
165209 r <- parseRoot xmldata
166210 ns <- asks getSvcNamespace
167− let s3Elem' = s3Elem ns
168− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
169− nextPartNumStr = listToMaybe $ r $/ s3Elem' "NextPartNumberMarker" &/ content
170− partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
171− partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
172− partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
173− partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
211+ let
212+ s3Elem' = s3Elem ns
213+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
214+ nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content
215+ partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
216+ partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
217+ partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
218+ partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
174219
175220 partModTimes <- mapM parseS3XMLTime partModTimeStr
176221 partSizes <- parseDecimals partSizeStr
177222 partNumbers <- parseDecimals partNumberStr
178223 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
179224
180− let partInfos =
181− map (uncurry4 ObjectPartInfo) $
182− zip4 partNumbers partETags partSizes partModTimes
225+ let
226+ partInfos = map (uncurry4 ObjectPartInfo) $
227+ zip4 partNumbers partETags partSizes partModTimes
183228
184229 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
185230
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+
186239parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
187240parseNotification xmldata = do
188241 r <- parseRoot xmldata
@@ -191,40 +244,32 @@ parseNotification xmldata = do
191244 qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
192245 tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
193246 lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
194− Notification
195− <$> mapM (parseNode ns "Queue") qcfg
196− <*> mapM (parseNode ns "Topic") tcfg
197− <*> mapM (parseNode ns "CloudFunction") lcfg
247+ Notification <$> (mapM (parseNode ns "Queue") qcfg)
248+ <*> (mapM (parseNode ns "Topic") tcfg)
249+ <*> (mapM (parseNode ns "CloudFunction") lcfg)
198250 where
251+
199252 getFilterRule ns c =
200253 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
201254 value = T.concat $ c $/ s3Elem ns "Value" &/ content
202− in FilterRule name value
255+ in FilterRule name value
256+
203257 parseNode ns arnName nodeData = do
204258 let c = fromNode nodeData
205− itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
259+ id = T.concat $ c $/ s3Elem ns "Id" &/ content
206260 arn = T.concat $ c $/ s3Elem ns arnName &/ content
207− events = mapMaybe textToEvent (c $/ s3Elem ns "Event" &/ content)
208− rules =
209− c
210− $/ s3Elem ns "Filter"
211− &/ s3Elem ns "S3Key"
212− &/ s3Elem ns "FilterRule"
213− &| getFilterRule ns
214− return $
215− NotificationConfig
216− itemId
217− arn
218− events
219− (Filter $ FilterKey $ FilterRules rules)
220−
221−parseSelectProgress :: (MonadIO m) => ByteString -> m Progress
261+ events = catMaybes $ map textToEvent $ c $/ s3Elem ns "Event" &/ content
262+ rules = c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key" &/
263+ s3Elem ns "FilterRule" &| getFilterRule ns
264+ return $ NotificationConfig id arn events
265+ (Filter $ FilterKey $ FilterRules rules)
266+
267+parseSelectProgress :: MonadIO m => ByteString -> m Progress
222268parseSelectProgress xmldata = do
223− r <- parseRoot $ LB.fromStrict xmldata
224− let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
225− bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content
226− bReturned = T.concat $ r $/ element "BytesReturned" &/ content
227− Progress
228− <$> parseDecimal bScanned
229− <*> parseDecimal bProcessed
230− <*> parseDecimal bReturned
269+ r <- parseRoot $ LB.fromStrict xmldata
270+ let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
271+ bProcessed = T.concat $ r $/element "BytesProcessed" &/ content
272+ bReturned = T.concat $ r $/element "BytesReturned" &/ content
273+ Progress <$> parseDecimal bScanned
274+ <*> parseDecimal bProcessed
275+ <*> parseDecimal bReturned
stack.yaml+4−4
@@ -15,7 +15,7 @@
1515# resolver:
1616# name: custom-snapshot
1717# location: "./custom-snapshot.yaml"
18−resolver: lts-22.19
18+resolver: lts-13.1
1919
2020# User packages to be built.
2121# Various formats can be used as shown in the example below.
@@ -36,17 +36,17 @@ resolver: lts-22.19
3636# non-dependency (i.e. a user package), and its test suites and benchmarks
3737# will not be run. This is useful for tweaking upstream packages.
3838packages:
39− - "."
39+- '.'
4040# Dependency packages to be pulled from upstream that are not in the resolver
4141# (e.g., acme-missiles-0.3)
42−extra-deps:
43− - crypton-connection-0.3.2
42+extra-deps: []
4443
4544# Override default flag values for local packages and extra-deps
4645flags: {}
4746
4847# Extra package databases containing global packages
4948extra-package-dbs: []
49+
5050# Control whether we use the GHC we find on the path
5151# system-ghc: true
5252#
stack.yaml.lockdeleted−19
@@ -1,19 +0,0 @@
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+461−807
@@ -1,5 +1,6 @@
1+{-# LANGUAGE OverloadedStrings #-}
12--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
34--
45-- Licensed under the Apache License, Version 2.0 (the "License");
56-- you may not use this file except in compliance with the License.
@@ -14,34 +15,36 @@
1415-- limitations under the License.
1516--
1617
17−import Conduit (replicateC)
18−import qualified Conduit as C
19−import qualified Control.Monad.Trans.Resource as R
20−import qualified Data.ByteString as BS
21−import Data.Conduit (yield)
22−import qualified Data.Conduit.Binary as CB
23−import Data.Conduit.Combinators (sinkList)
24−import qualified Data.HashMap.Strict as H
25−import qualified Data.Text as T
26−import Data.Time (fromGregorian)
27−import qualified Data.Time as Time
28−import Lib.Prelude
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)
24+import qualified Control.Monad.Trans.Resource as R
25+import qualified Data.ByteString as BS
26+import Data.Conduit (yield)
27+import qualified Data.Conduit as C
28+import qualified Data.Conduit.Binary as CB
29+import Data.Conduit.Combinators (sinkList)
30+import qualified Data.Map.Strict as Map
31+import qualified Data.Text as T
32+import Data.Time (fromGregorian)
33+import qualified Data.Time as Time
2934import qualified Network.HTTP.Client.MultipartFormData as Form
30−import qualified Network.HTTP.Conduit as NC
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
38−import System.Directory (getTemporaryDirectory)
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
44−import qualified UnliftIO.IO as UIO
35+import qualified Network.HTTP.Conduit as NC
36+import qualified Network.HTTP.Types as HT
37+import System.Directory (getTemporaryDirectory)
38+import System.Environment (lookupEnv)
39+import qualified System.IO as SIO
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
4548
4649main :: IO ()
4750main = defaultMain tests
@@ -49,107 +52,41 @@ main = defaultMain tests
4952tests :: TestTree
5053tests = testGroup "Tests" [liveServerUnitTests]
5154
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−
9755-- conduit that generates random binary stream of given length
98−randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
99−randomDataSrc = genBS
56+randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
57+randomDataSrc s' = genBS s'
10058 where
101− concatIt bs n =
102− BS.concat $
103− replicate (fromIntegral q) bs
104− ++ [BS.take (fromIntegral r) bs]
105− where
106− (q, r) = n `divMod` fromIntegral (BS.length bs)
59+ concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++
60+ [BS.take (fromIntegral r) bs]
61+ where (q, r) = n `divMod` fromIntegral (BS.length bs)
62+
10763 genBS s = do
10864 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
10965 let byteArr64 = BS.pack w8s
11066 if s < oneMiB
11167 then yield $ concatIt byteArr64 s
112− else do
113− yield $ concatIt byteArr64 oneMiB
114− genBS (s - oneMiB)
68+ else do yield $ concatIt byteArr64 oneMiB
69+ genBS (s - oneMiB)
11570
116−mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
71+mkRandFile :: R.MonadResource m => Int64 -> m FilePath
11772mkRandFile size = do
118− dir <- liftIO getTemporaryDirectory
73+ dir <- liftIO $ getTemporaryDirectory
11974 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
12075
12176funTestBucketPrefix :: Text
12277funTestBucketPrefix = "miniohstest-"
12378
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−
143−funTestWithBucket ::
144− TestName ->
145− (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
146− TestTree
79+funTestWithBucket :: TestName
80+ -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree
14781funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
14882 -- generate a random name for the bucket
14983 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
15084 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
15185 liftStep = liftIO . step
152− connInfo <- loadTestServerConnInfo
86+ connInfo <- ( bool minioPlayCI
87+ ( setCreds (Credentials "minio" "minio123") "http://localhost:9000" )
88+ . isJust
89+ ) <$> lookupEnv "MINIO_LOCAL"
15390 ret <- runMinio connInfo $ do
15491 liftStep $ "Creating bucket for test - " ++ t
15592 foundBucket <- bucketExists b
@@ -159,40 +96,24 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
15996 deleteBucket b
16097 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
16198
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−
17399liveServerUnitTests :: TestTree
174−liveServerUnitTests =
175− testGroup
176− "Unit tests against a live server"
177− [ basicTests,
178− listingTest,
179− highLevelListingTest,
180− lowLevelMultipartTest,
181− putObjectSizeTest,
182− putObjectNoSizeTest,
183− multipartTest,
184− putObjectContentTypeTest,
185− putObjectContentLanguageTest,
186− putObjectStorageClassTest,
187− putObjectUserMetadataTest,
188− getObjectTest,
189− copyObjectTests,
190− presignedUrlFunTest,
191− presignedPostPolicyFunTest,
192− bucketPolicyFunTest,
193− getNPutSSECTest,
194− assumeRoleRequestTest
195− ]
100+liveServerUnitTests = testGroup "Unit tests against a live server"
101+ [ basicTests
102+ , listingTest
103+ , highLevelListingTest
104+ , lowLevelMultipartTest
105+ , putObjectSizeTest
106+ , putObjectNoSizeTest
107+ , multipartTest
108+ , putObjectContentTypeTest
109+ , putObjectContentLanguageTest
110+ , putObjectStorageClassTest
111+ , copyObjectTests
112+ , presignedUrlFunTest
113+ , presignedPostPolicyFunTest
114+ , bucketPolicyFunTest
115+ , getNPutSSECTest
116+ ]
196117
197118basicTests :: TestTree
198119basicTests = funTestWithBucket "Basic tests" $
@@ -201,109 +122,83 @@ basicTests = funTestWithBucket "Basic tests" $
201122 buckets <- getService
202123 unless (length (filter (== bucket) $ map biName buckets) == 1) $
203124 liftIO $
204− assertFailure
205− ( "The bucket "
206− ++ show bucket
207− ++ " was expected to exist."
208− )
125+ assertFailure ("The bucket " ++ show bucket ++
126+ " was expected to exist.")
209127
210128 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
211129 mbE <- try $ makeBucket bucket Nothing
212130 case mbE of
213131 Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
214− _ -> return ()
132+ _ -> return ()
215133
216134 step "makeBucket with an invalid bucket name and check for appropriate exception."
217135 invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
218136 case invalidMBE of
219137 Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
220− _ -> return ()
138+ _ -> return ()
221139
222140 step "getLocation works"
223141 region <- getLocation bucket
224142 liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
225143
226144 step "singlepart putObject works"
227− testFilepath <- mkRandFile 200
228− fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
145+ fPutObject bucket "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
229146
230147 step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
231− fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
148+ fpE <- try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
232149 case fpE of
233150 Left exn -> liftIO $ exn @?= NoSuchBucket
234− _ -> return ()
151+ _ -> return ()
235152
236153 outFile <- mkRandFile 0
237154 step "simple fGetObject works"
238− fGetObject bucket "test-file" outFile defaultGetObjectOptions
155+ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions
239156
240157 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
241158 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
248− defaultGetObjectOptions
249− { gooIfUnmodifiedSince = Just unmodifiedTime
250− }
159+ resE <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
160+ gooIfUnmodifiedSince = (Just unmodifiedTime)
161+ }
251162 case resE of
252163 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
253− _ -> return ()
164+ _ -> return ()
254165
255166 step "fGetObject an object with no matching etag, check for exception"
256− resE1 <-
257− try $
258− fGetObject
259− bucket
260− "test-file"
261− outFile
262− defaultGetObjectOptions
263− { gooIfMatch = Just "invalid-etag"
264− }
167+ resE1 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
168+ gooIfMatch = (Just "invalid-etag")
169+ }
265170 case resE1 of
266171 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
267− _ -> return ()
172+ _ -> return ()
268173
269174 step "fGetObject an object with no valid range, check for exception"
270− resE2 <-
271− try $
272− fGetObject
273− bucket
274− "test-file"
275− outFile
276− defaultGetObjectOptions
277− { gooRange = Just $ HT.ByteRangeFromTo 100 300
278− }
175+ resE2 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
176+ gooRange = (Just $ HT.ByteRangeFromTo 100 200)
177+ }
279178 case resE2 of
280− Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
281− _ -> return ()
179+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
180+ _ -> return ()
282181
283182 step "fGetObject on object with a valid range"
284− fGetObject
285− bucket
286− "test-file"
287− outFile
288− defaultGetObjectOptions
289− { gooRange = Just $ HT.ByteRangeFrom 1
290− }
183+ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
184+ gooRange = (Just $ HT.ByteRangeFrom 1)
185+ }
291186
292187 step "fGetObject a non-existent object and check for NoSuchKey exception"
293188 resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
294189 case resE3 of
295190 Left exn -> liftIO $ exn @?= NoSuchKey
296− _ -> return ()
191+ _ -> return ()
297192
298193 step "create new multipart upload works"
299194 uid <- newMultipartUpload bucket "newmpupload" []
300− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
195+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
301196
302197 step "abort a new multipart upload works"
303198 abortMultipartUpload bucket "newmpupload" uid
304199
305200 step "delete object works"
306− deleteObject bucket "test-file"
201+ deleteObject bucket "lsb-release"
307202
308203 step "statObject test"
309204 let object = "sample"
@@ -313,310 +208,257 @@ basicTests = funTestWithBucket "Basic tests" $
313208
314209 step "get metadata of the object"
315210 res <- statObject bucket object defaultGetObjectOptions
316− liftIO $ oiSize res @?= 0
211+ liftIO $ (oiSize res) @?= 0
317212
318213 step "delete object"
319214 deleteObject bucket object
320215
321216lowLevelMultipartTest :: TestTree
322217lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
323− \step bucket -> do
324− -- low-level multipart operation tests.
325− let object = "newmpupload"
326− mb15 = 15 * 1024 * 1024
218+ \step bucket -> do
219+ -- low-level multipart operation tests.
220+ let object = "newmpupload"
221+ mb15 = 15 * 1024 * 1024
327222
328− step "Prepare for low-level multipart tests."
329− step "create new multipart upload"
330− uid <- newMultipartUpload bucket object []
331− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
223+ step "Prepare for low-level multipart tests."
224+ step "create new multipart upload"
225+ uid <- newMultipartUpload bucket object []
226+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
332227
333− randFile <- mkRandFile mb15
228+ randFile <- mkRandFile mb15
334229
335− step "put object parts 1 of 1"
336− partInfo <-
337− UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
338− putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
230+ step "put object parts 1 of 1"
231+ h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
232+ partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
339233
340− step "complete multipart"
341− void $ completeMultipartUpload bucket object uid [partInfo]
234+ step "complete multipart"
235+ void $ completeMultipartUpload bucket object uid [partInfo]
342236
343− destFile <- mkRandFile 0
344− step "Retrieve the created object and check size"
345− fGetObject bucket object destFile defaultGetObjectOptions
346− gotSize <- withNewHandle destFile getFileSize
347− liftIO $
348− gotSize
349− == Right (Just mb15)
350− @? "Wrong file size of put file after getting"
237+ destFile <- mkRandFile 0
238+ step "Retrieve the created object and check size"
239+ fGetObject bucket object destFile defaultGetObjectOptions
240+ gotSize <- withNewHandle destFile getFileSize
241+ liftIO $ gotSize == Right (Just mb15) @?
242+ "Wrong file size of put file after getting"
351243
352− step "Cleanup actions"
353− removeObject bucket object
244+ step "Cleanup actions"
245+ removeObject bucket object
354246
355247putObjectSizeTest :: TestTree
356248putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
357249 \step bucket -> do
358− -- putObject test (conduit source, size specified)
359− let obj = "msingle"
360− mb1 = 1 * 1024 * 1024
250+ -- putObject test (conduit source, size specified)
251+ let obj = "msingle"
252+ mb1 = 1 * 1024 * 1024
361253
362− step "Prepare for putObject with from source with size."
363− rFile <- mkRandFile mb1
254+ step "Prepare for putObject with from source with size."
255+ rFile <- mkRandFile mb1
364256
365− step "Upload single file."
366− putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
257+ step "Upload single file."
258+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
367259
368− step "Retrieve and verify file size"
369− destFile <- mkRandFile 0
370− fGetObject bucket obj destFile defaultGetObjectOptions
371− gotSize <- withNewHandle destFile getFileSize
372− liftIO $
373− gotSize
374− == Right (Just mb1)
375− @? "Wrong file size of put file after getting"
260+ step "Retrieve and verify file size"
261+ destFile <- mkRandFile 0
262+ fGetObject bucket obj destFile defaultGetObjectOptions
263+ gotSize <- withNewHandle destFile getFileSize
264+ liftIO $ gotSize == Right (Just mb1) @?
265+ "Wrong file size of put file after getting"
376266
377− step "Cleanup actions"
378− deleteObject bucket obj
267+ step "Cleanup actions"
268+ deleteObject bucket obj
379269
380270putObjectNoSizeTest :: TestTree
381271putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
382272 \step bucket -> do
383− -- putObject test (conduit source, no size specified)
384− let obj = "mpart"
385− mb70 = 70 * 1024 * 1024
273+ -- putObject test (conduit source, no size specified)
274+ let obj = "mpart"
275+ mb70 = 70 * 1024 * 1024
386276
387− step "Prepare for putObject with from source without providing size."
388− rFile <- mkRandFile mb70
277+ step "Prepare for putObject with from source without providing size."
278+ rFile <- mkRandFile mb70
389279
390− step "Upload multipart file."
391− putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
280+ step "Upload multipart file."
281+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
392282
393− step "Retrieve and verify file size"
394− destFile <- mkRandFile 0
395− fGetObject bucket obj destFile defaultGetObjectOptions
396− gotSize <- withNewHandle destFile getFileSize
397− liftIO $
398− gotSize
399− == Right (Just mb70)
400− @? "Wrong file size of put file after getting"
283+ step "Retrieve and verify file size"
284+ destFile <- mkRandFile 0
285+ fGetObject bucket obj destFile defaultGetObjectOptions
286+ gotSize <- withNewHandle destFile getFileSize
287+ liftIO $ gotSize == Right (Just mb70) @?
288+ "Wrong file size of put file after getting"
401289
402− step "Cleanup actions"
403− deleteObject bucket obj
290+ step "Cleanup actions"
291+ deleteObject bucket obj
404292
405293highLevelListingTest :: TestTree
406294highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
407295 \step bucket -> do
408− step "High-level listObjects Test"
409− step "put 3 objects"
410− let extractObjectsFromList =
411− mapM
412− ( \case
413− ListItemObject o -> Just $ oiObject o
414− _ -> Nothing
415− )
416− extractObjectsAndDirsFromList =
417− map
418− ( \case
419− ListItemObject o -> oiObject o
420− ListItemPrefix d -> d
421− )
422− expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
423− expectedNonRecList = ["o4", "dir/"]
424−
425− testFilepath <- mkRandFile 200
426− forM_ expectedObjects $
427− \obj -> fPutObject bucket obj testFilepath defaultPutObjectOptions
428−
429− step "High-level listing of objects"
430− items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
431− liftIO $
432− assertEqual "Objects/Dirs match failed!" expectedNonRecList $
296+ step "High-level listObjects Test"
297+ step "put 3 objects"
298+ let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
299+ extractObjectsFromList os =
300+ mapM (\t -> case t of
301+ ListItemObject o -> Just $ oiObject o
302+ _ -> Nothing) os
303+ expectedNonRecList = ["o4", "dir/"]
304+ extractObjectsAndDirsFromList os =
305+ map (\t -> case t of
306+ ListItemObject o -> oiObject o
307+ ListItemPrefix d -> d) os
308+
309+ forM_ expectedObjects $
310+ \obj -> fPutObject bucket obj "/etc/lsb-release" defaultPutObjectOptions
311+
312+ step "High-level listing of objects"
313+ items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
314+ liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
433315 extractObjectsAndDirsFromList items
434316
435− step "High-level recursive listing of objects"
436− objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
317+ step "High-level recursive listing of objects"
318+ objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
437319
438− liftIO
439− $ assertEqual
440− "Objects match failed!"
441− (Just $ sort expectedObjects)
442− $ extractObjectsFromList objects
320+ liftIO $ assertEqual "Objects match failed!"
321+ (Just $ sort expectedObjects) $
322+ extractObjectsFromList objects
443323
444− step "High-level listing of objects (version 1)"
445− itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
446− liftIO $
447− assertEqual "Objects/Dirs match failed!" expectedNonRecList $
324+ step "High-level listing of objects (version 1)"
325+ itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
326+ liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
448327 extractObjectsAndDirsFromList itemsV1
449328
450− step "High-level recursive listing of objects (version 1)"
451− objectsV1 <-
452− C.runConduit $
453− listObjectsV1 bucket Nothing True
454− C..| sinkList
455−
456− liftIO
457− $ assertEqual
458− "Objects match failed!"
459− (Just $ sort expectedObjects)
460− $ extractObjectsFromList objectsV1
461−
462− let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
463− expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
464− step "High-level listing with prefix"
465− prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
466− liftIO
467− $ assertEqual
468− "Objects/Dirs under prefix match failed!"
469− expectedPrefListing
470− $ extractObjectsAndDirsFromList prefItems
471−
472− step "High-level listing with prefix recursive"
473− prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
474− liftIO
475− $ assertEqual
476− "Objects/Dirs under prefix match recursive failed!"
477− expectedPrefListingRec
478− $ extractObjectsFromList prefItemsRec
479−
480− step "High-level listing with prefix (version 1)"
481− prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
482− liftIO
483− $ assertEqual
484− "Objects/Dirs under prefix match failed!"
485− expectedPrefListing
486− $ extractObjectsAndDirsFromList prefItemsV1
487−
488− step "High-level listing with prefix recursive (version 1)"
489− prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
490− liftIO
491− $ assertEqual
492− "Objects/Dirs under prefix match recursive failed!"
493− expectedPrefListingRec
494− $ extractObjectsFromList prefItemsRecV1
495−
496− step "Cleanup actions"
497− forM_ expectedObjects $
498− \obj -> removeObject bucket obj
499−
500− step "High-level listIncompleteUploads Test"
501− let object = "newmpupload"
502− step "create 10 multipart uploads"
503− forM_ [1 .. 10 :: Int] $ \_ -> do
329+ step "High-level recursive listing of objects (version 1)"
330+ objectsV1 <- C.runConduit $ listObjectsV1 bucket Nothing True C..|
331+ sinkList
332+
333+ liftIO $ assertEqual "Objects match failed!"
334+ (Just $ sort expectedObjects) $
335+ extractObjectsFromList objectsV1
336+
337+ let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
338+ expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
339+ step "High-level listing with prefix"
340+ prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
341+ liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
342+ expectedPrefListing $ extractObjectsAndDirsFromList prefItems
343+
344+ step "High-level listing with prefix recursive"
345+ prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
346+ liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
347+ expectedPrefListingRec $ extractObjectsFromList prefItemsRec
348+
349+ step "High-level listing with prefix (version 1)"
350+ prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
351+ liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
352+ expectedPrefListing $ extractObjectsAndDirsFromList prefItemsV1
353+
354+ step "High-level listing with prefix recursive (version 1)"
355+ prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
356+ liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
357+ expectedPrefListingRec $ extractObjectsFromList prefItemsRecV1
358+
359+ step "Cleanup actions"
360+ forM_ expectedObjects $
361+ \obj -> removeObject bucket obj
362+
363+ step "High-level listIncompleteUploads Test"
364+ let object = "newmpupload"
365+ step "create 10 multipart uploads"
366+ 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"
504384 uid <- newMultipartUpload bucket object []
505385 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
506386
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−
525− step "put object parts 1..10"
526− inputFile <- mkRandFile mb5
527− UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
528− forM_ [1 .. 10] $ \pnum ->
387+ step "put object parts 1..10"
388+ inputFile <- mkRandFile mb5
389+ h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
390+ forM_ [1..10] $ \pnum ->
529391 putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
530392
531− step "fetch list parts"
532− incompleteParts <-
533− C.runConduit $
534− listIncompleteParts bucket object uid
535− C..| sinkList
536− liftIO $ length incompleteParts @?= 10
393+ step "fetch list parts"
394+ incompleteParts <- C.runConduit $ listIncompleteParts bucket object uid
395+ C..| sinkList
396+ liftIO $ length incompleteParts @?= 10
537397
538− step "cleanup"
539− abortMultipartUpload bucket object uid
398+ step "cleanup"
399+ abortMultipartUpload bucket object uid
540400
541401listingTest :: TestTree
542402listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
543− step "listObjects' test"
544− step "put 10 objects"
545− let objects = (\s -> T.concat ["test-file-", T.pack (show s)]) <$> [1 .. 10 :: Int]
546−
547− testFilepath <- mkRandFile 200
548− forM_ [1 .. 10 :: Int] $ \s ->
549− fPutObject bucket (T.concat ["test-file-", T.pack (show s)]) testFilepath defaultPutObjectOptions
550−
551− step "Simple list"
552− res <- listObjects' bucket Nothing Nothing Nothing Nothing
553− let expectedObjects = sort objects
554− liftIO $
555− assertEqual
556− "Objects match failed!"
557− expectedObjects
558− (map oiObject $ lorObjects res)
559−
560− step "Simple list version 1"
561− resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
562− let expected =
563− sort $
564− map
565− ( T.concat
566− . ("test-file-" :)
567− . (: [])
568− . T.pack
569− . show
570− )
571− [1 .. 10 :: Int]
572− liftIO $
573− assertEqual
574− "Objects match failed!"
575− expected
576− (map oiObject $ lorObjects' resV1)
577−
578− step "Cleanup actions"
579− forM_ objects $ \obj -> deleteObject bucket obj
580−
581− step "listIncompleteUploads' test"
582− step "create 10 multipart uploads"
583− let object = "newmpupload"
584− forM_ [1 .. 10 :: Int] $ \_ -> do
585− uid <- newMultipartUpload bucket object []
586− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
403+ step "listObjects' test"
404+ step "put 10 objects"
405+ let objects = (\s ->T.concat ["lsb-release", T.pack (show s)]) <$> [1..10::Int]
406+
407+ forM_ [1..10::Int] $ \s ->
408+ fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release" defaultPutObjectOptions
409+
410+ step "Simple list"
411+ res <- listObjects' bucket Nothing Nothing Nothing Nothing
412+ let expectedObjects = sort objects
413+ liftIO $ assertEqual "Objects match failed!" expectedObjects
414+ (map oiObject $ lorObjects res)
415+
416+ step "Simple list version 1"
417+ resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
418+ let expected = sort $ map (T.concat .
419+ ("lsb-release":) .
420+ (\x -> [x]) .
421+ T.pack .
422+ show) [1..10::Int]
423+ liftIO $ assertEqual "Objects match failed!" expected
424+ (map oiObject $ lorObjects' resV1)
425+
426+ step "Cleanup actions"
427+ forM_ objects $ \obj -> deleteObject bucket obj
428+
429+ step "listIncompleteUploads' test"
430+ step "create 10 multipart uploads"
431+ let object = "newmpupload"
432+ forM_ [1..10::Int] $ \_ -> do
433+ 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.")
587451
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−
616− step "fetch list parts"
617− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
618− liftIO $ length (lprParts listPartsResult) @?= 10
619− abortMultipartUpload bucket object uid
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+
458+ step "fetch list parts"
459+ listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
460+ liftIO $ (length $ lprParts listPartsResult) @?= 10
461+ abortMultipartUpload bucket object uid
620462
621463presignedUrlFunTest :: TestTree
622464presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -628,47 +470,29 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
628470 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
629471
630472 step "PUT object presigned URL - makePresignedUrl"
631− putUrl <-
632− makePresignedUrl
633− 3600
634− HT.methodPut
635− (Just bucket)
636− (Just obj)
637− (Just "us-east-1")
638− []
639− []
640−
641− -- print putUrl
473+ putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
474+ (Just obj) (Just "us-east-1") [] []
475+
642476 let size1 = 1000 :: Int64
643477 inputFile <- mkRandFile size1
644478
645479 -- attempt to upload using the presigned URL
646480 putResp <- putR size1 inputFile mgr putUrl
647− liftIO $
648− (NC.responseStatus putResp == HT.status200)
649− @? "presigned PUT failed"
481+ liftIO $ (NC.responseStatus putResp == HT.status200) @?
482+ "presigned PUT failed"
650483
651484 step "GET object presigned URL - makePresignedUrl"
652− getUrl <-
653− makePresignedUrl
654− 3600
655− HT.methodGet
656− (Just bucket)
657− (Just obj)
658− (Just "us-east-1")
659− []
660− []
485+ getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
486+ (Just obj) (Just "us-east-1") [] []
661487
662488 getResp <- getR mgr getUrl
663− liftIO $
664− (NC.responseStatus getResp == HT.status200)
665− @? "presigned GET failed"
489+ liftIO $ (NC.responseStatus getResp == HT.status200) @?
490+ "presigned GET failed"
666491
667492 -- read content from file to compare with response above
668493 bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
669− liftIO $
670− (bs == NC.responseBody getResp)
671− @? "presigned put and get got mismatched data"
494+ liftIO $ (bs == NC.responseBody getResp) @?
495+ "presigned put and get got mismatched data"
672496
673497 step "PUT object presigned - presignedPutObjectURL"
674498 putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
@@ -677,71 +501,66 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
677501 testFile <- mkRandFile size2
678502
679503 putResp2 <- putR size2 testFile mgr putUrl2
680− liftIO $
681− (NC.responseStatus putResp2 == HT.status200)
682− @? "presigned PUT failed (presignedPutObjectUrl)"
504+ liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
505+ "presigned PUT failed (presignedPutObjectUrl)"
683506
684507 step "HEAD object presigned URL - presignedHeadObjectUrl"
685508 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
686509
687− headResp <- do
688− let req = NC.parseRequest_ $ decodeUtf8 headUrl
689− NC.httpLbs (req {NC.method = HT.methodHead}) mgr
690− liftIO $
691− (NC.responseStatus headResp == HT.status200)
692− @? "presigned HEAD failed (presignedHeadObjectUrl)"
510+ headResp <- do let req = NC.parseRequest_ $ toS headUrl
511+ NC.httpLbs (req {NC.method = HT.methodHead}) mgr
512+ liftIO $ (NC.responseStatus headResp == HT.status200) @?
513+ "presigned HEAD failed (presignedHeadObjectUrl)"
693514
694515 -- check that header info is accurate
695− let h = H.fromList $ NC.responseHeaders headResp
696− cLen = H.lookupDefault "0" HT.hContentLength h
697− liftIO $ (cLen == showBS size2) @? "Head req returned bad content length"
516+ let h = Map.fromList $ NC.responseHeaders headResp
517+ cLen = Map.findWithDefault "0" HT.hContentLength h
518+ liftIO $ (cLen == show size2) @? "Head req returned bad content length"
698519
699520 step "GET object presigned URL - presignedGetObjectUrl"
700521 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
701522
702523 getResp2 <- getR mgr getUrl2
703− liftIO $
704− (NC.responseStatus getResp2 == HT.status200)
705− @? "presigned GET failed (presignedGetObjectUrl)"
524+ liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
525+ "presigned GET failed (presignedGetObjectUrl)"
706526
707527 -- read content from file to compare with response above
708528 bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
709− liftIO $
710− (bs2 == NC.responseBody getResp2)
711− @? "presigned put and get got mismatched data (presigned*Url)"
529+ liftIO $ (bs2 == NC.responseBody getResp2) @?
530+ "presigned put and get got mismatched data (presigned*Url)"
531+
712532
713533 mapM_ (removeObject bucket) [obj, obj2]
714534 where
715535 putR size filePath mgr url = do
716− let req = NC.parseRequest_ $ decodeUtf8 url
717− let req' =
718− req
719− { NC.method = HT.methodPut,
720− NC.requestBody =
721− NC.requestBodySource size $
722− CB.sourceFile filePath
723− }
536+ let req = NC.parseRequest_ $ toS url
537+ let req' = req { NC.method = HT.methodPut
538+ , NC.requestBody = NC.requestBodySource size $
539+ CB.sourceFile filePath}
724540 NC.httpLbs req' mgr
541+
725542 getR mgr url = do
726− let req = NC.parseRequest_ $ decodeUtf8 url
543+ let req = NC.parseRequest_ $ toS url
727544 NC.httpLbs req mgr
728545
729546presignedPostPolicyFunTest :: TestTree
730−presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
547+presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
731548 \step bucket -> do
549+
732550 step "presignedPostPolicy basic test"
733− now <- liftIO Time.getCurrentTime
551+ now <- liftIO $ Time.getCurrentTime
734552
735553 let key = "presignedPostPolicyTest/myfile"
736− policyConds =
737− [ ppCondBucket bucket,
738− ppCondKey key,
739− ppCondContentLengthRange 1 1000,
740− ppCondContentType "application/octet-stream",
741− ppCondSuccessActionStatus 200
742− ]
554+ policyConds = [ ppCondBucket bucket
555+ , ppCondKey key
556+ , ppCondContentLengthRange 1 1000
557+ , ppCondContentType "application/octet-stream"
558+ , ppCondSuccessActionStatus 200
559+ ]
560+
743561 expirationTime = Time.addUTCTime 3600 now
744562 postPolicyE = newPostPolicy expirationTime policyConds
563+
745564 size = 1000 :: Int64
746565
747566 inputFile <- mkRandFile size
@@ -752,43 +571,43 @@ presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests
752571 (url, formData) <- presignedPostPolicy postPolicy
753572 -- liftIO (print url) >> liftIO (print formData)
754573 result <- liftIO $ postForm url formData inputFile
755− liftIO $
756− (NC.responseStatus result == HT.status200)
757− @? "presigned POST failed"
574+ liftIO $ (NC.responseStatus result == HT.status200) @?
575+ "presigned POST failed"
758576
759577 mapM_ (removeObject bucket) [key]
760− where
761− postForm url formData inputFile = do
762− req <- NC.parseRequest $ decodeUtf8 url
763− let parts =
764− map (uncurry Form.partBS) $
765− H.toList formData
766− parts' = parts ++ [Form.partFile "file" inputFile]
767− req' <- Form.formDataBody parts' req
768− mgr <- NC.newManager NC.tlsManagerSettings
769− NC.httpLbs req' mgr
578+ where
579+
580+ postForm url formData inputFile = do
581+ req <- NC.parseRequest $ toS url
582+ let parts = map (\(x, y) -> Form.partBS x y) $
583+ Map.toList formData
584+ parts' = parts ++ [Form.partFile "file" inputFile]
585+ req' <- Form.formDataBody parts' req
586+ mgr <- NC.newManager NC.tlsManagerSettings
587+ NC.httpLbs req' mgr
770588
771589bucketPolicyFunTest :: TestTree
772590bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
773591 \step bucket -> do
592+
774593 step "bucketPolicy basic test - no policy exception"
775594 resE <- try $ getBucketPolicy bucket
776595 case resE of
777596 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
778− _ -> return ()
597+ _ -> return ()
779598
780599 resE' <- try $ setBucketPolicy bucket T.empty
781600 case resE' of
782601 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
783− _ -> return ()
602+ _ -> return ()
784603
785604 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/*\"]}]}"
786605
787606 step "try a malformed policy, expect error"
788607 resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
789608 case resE'' of
790− Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
791− _ -> return ()
609+ Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "Policy has invalid resource."
610+ _ -> return ()
792611
793612 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 <> "/*\"]}]}"
794613
@@ -803,22 +622,13 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
803622 step "verify bucket policy: (2) get `myobject` anonymously"
804623 connInfo <- asks mcConnInfo
805624 let proto = bool "http://" "https://" $ connectIsSecure connInfo
806− url =
807− BS.concat
808− [ proto,
809− getHostAddr connInfo,
810− "/",
811− encodeUtf8 bucket,
812− "/",
813− encodeUtf8 obj
814− ]
815− respE <-
816− liftIO $
817− fmap (Right . toStrictBS) (NC.simpleHttp $ decodeUtf8 url)
818− `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
625+ url = BS.concat [proto, getHostAddr connInfo, "/", toS bucket,
626+ "/", toS obj]
627+ respE <- liftIO $ (fmap (Right . toS) $ NC.simpleHttp $ toS url) `catch`
628+ (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
819629 case respE of
820− Left err -> liftIO $ assertFailure $ show err
821− Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
630+ Left err -> liftIO $ assertFailure $ show err
631+ Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")
822632
823633 deleteObject bucket obj
824634
@@ -828,28 +638,25 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
828638multipartTest :: TestTree
829639multipartTest = funTestWithBucket "Multipart Tests" $
830640 \step bucket -> do
831− -- Commenting out test since it's platform specific.
832− -- FIXME: Need to find a platform agnostic way to test this.
833− -- step "Prepare for putObjectInternal with non-seekable file, with size."
834− -- step "Upload multipart file."
835− -- let mb80 = 80 * 1024 * 1024
836− -- obj = "mpart"
641+ step "Prepare for putObjectInternal with non-seekable file, with size."
642+ step "Upload multipart file."
643+ let mb80 = 80 * 1024 * 1024
644+ obj = "mpart"
837645
838− -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
646+ void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
839647
840− -- step "Retrieve and verify file size"
841− -- destFile <- mkRandFile 0
842− -- fGetObject bucket obj destFile defaultGetObjectOptions
843− -- gotSize <- withNewHandle destFile getFileSize
844− -- liftIO $
845− -- gotSize == Right (Just mb80)
846− -- @? "Wrong file size of put file after getting"
648+ step "Retrieve and verify file size"
649+ destFile <- mkRandFile 0
650+ fGetObject bucket obj destFile defaultGetObjectOptions
651+ gotSize <- withNewHandle destFile getFileSize
652+ liftIO $ gotSize == Right (Just mb80) @?
653+ "Wrong file size of put file after getting"
847654
848− -- step "Cleanup actions"
849− -- removeObject bucket obj
655+ step "Cleanup actions"
656+ removeObject bucket obj
850657
851− -- step "cleanup"
852− -- removeObject bucket "big"
658+ step "cleanup"
659+ removeObject bucket "big"
853660
854661 step "Prepare for removeIncompleteUpload"
855662 -- low-level multipart operation tests.
@@ -863,17 +670,15 @@ multipartTest = funTestWithBucket "Multipart Tests" $
863670 randFile <- mkRandFile kb5
864671
865672 step "upload 2 parts"
866− forM_ [1, 2] $ \partNum -> do
867− UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
868− void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
673+ forM_ [1,2] $ \partNum -> do
674+ h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
675+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
869676
870677 step "remove ongoing upload"
871678 removeIncompleteUpload bucket object
872− uploads <-
873− C.runConduit $
874− listIncompleteUploads bucket (Just object) False
875− C..| sinkList
876− liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
679+ uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
680+ C..| sinkList
681+ liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
877682
878683putObjectContentTypeTest :: TestTree
879684putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
@@ -884,39 +689,28 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
884689
885690 step "create server object with content-type"
886691 inputFile <- mkRandFile size1
887− fPutObject
888− bucket
889− object
890− inputFile
891− defaultPutObjectOptions
892− { pooContentType = Just "application/javascript"
893− }
692+ fPutObject bucket object inputFile defaultPutObjectOptions {
693+ pooContentType = Just "application/javascript"
694+ }
894695
895696 -- retrieve obj info to check
896697 oi <- headObject bucket object []
897698 let m = oiMetadata oi
898699
899700 step "Validate content-type"
900− liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
701+ liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (Map.lookup "Content-Type" m)
901702
902703 step "upload object with content-encoding set to identity"
903− fPutObject
904− bucket
905− object
906− inputFile
907− defaultPutObjectOptions
908− { pooContentEncoding = Just "identity"
909− }
704+ fPutObject bucket object inputFile defaultPutObjectOptions {
705+ pooContentEncoding = Just "identity"
706+ }
910707
911708 oiCE <- headObject bucket object []
912709 let m' = oiMetadata oiCE
913710
914711 step "Validate content-encoding"
915− liftIO $
916− assertEqual
917− "Content-Encoding did not match"
918− (Just "identity")
919− (H.lookup "Content-Encoding" m')
712+ liftIO $ assertEqual "Content-Encoding did not match" (Just "identity")
713+ (Map.lookup "Content-Encoding" m')
920714
921715 step "Cleanup actions"
922716
@@ -931,103 +725,19 @@ putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage test
931725
932726 step "create server object with content-language"
933727 inputFile <- mkRandFile size1
934− fPutObject
935− bucket
936− object
937− inputFile
938− defaultPutObjectOptions
939− { pooContentLanguage = Just "en-US"
940− }
728+ fPutObject bucket object inputFile defaultPutObjectOptions {
729+ pooContentLanguage = Just "en-US"
730+ }
941731
942732 -- retrieve obj info to check
943733 oi <- headObject bucket object []
944734 let m = oiMetadata oi
945735
946736 step "Validate content-language"
947− liftIO $
948− assertEqual
949− "content-language did not match"
950− (Just "en-US")
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−
737+ liftIO $ assertEqual "content-language did not match" (Just "en-US")
738+ (Map.lookup "Content-Language" m)
990739 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!"
1029740
1030− step "Cleanup actions"
1031741 removeObject bucket object
1032742
1033743putObjectStorageClassTest :: TestTree
@@ -1045,21 +755,13 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
1045755 inputFile' <- mkRandFile size1
1046756 inputFile'' <- mkRandFile size0
1047757
1048− fPutObject
1049− bucket
1050− object
1051− inputFile
1052− defaultPutObjectOptions
1053− { pooStorageClass = Just "STANDARD"
1054− }
1055−
1056− fPutObject
1057− bucket
1058− object'
1059− inputFile'
1060− defaultPutObjectOptions
1061− { pooStorageClass = Just "REDUCED_REDUNDANCY"
1062− }
758+ fPutObject bucket object inputFile defaultPutObjectOptions {
759+ pooStorageClass = Just "STANDARD"
760+ }
761+
762+ fPutObject bucket object' inputFile' defaultPutObjectOptions {
763+ pooStorageClass = Just "REDUCED_REDUNDANCY"
764+ }
1063765
1064766 removeObject bucket object
1065767
@@ -1068,24 +770,15 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
1068770 let m' = oiMetadata oi'
1069771
1070772 step "Validate x-amz-storage-class rrs"
1071− liftIO $
1072− assertEqual
1073− "storageClass did not match"
1074− (Just "REDUCED_REDUNDANCY")
1075− (H.lookup "X-Amz-Storage-Class" m')
1076−
1077− fpE <-
1078− try $
1079− fPutObject
1080− bucket
1081− object''
1082− inputFile''
1083− defaultPutObjectOptions
1084− { pooStorageClass = Just "INVALID_STORAGE_CLASS"
1085− }
773+ liftIO $ assertEqual "storageClass did not match" (Just "REDUCED_REDUNDANCY")
774+ (Map.lookup "X-Amz-Storage-Class" m')
775+
776+ fpE <- try $ fPutObject bucket object'' inputFile'' defaultPutObjectOptions {
777+ pooStorageClass = Just "INVALID_STORAGE_CLASS"
778+ }
1086779 case fpE of
1087780 Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
1088− _ -> return ()
781+ _ -> return ()
1089782
1090783 step "Cleanup actions"
1091784
@@ -1104,7 +797,7 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1104797 fPutObject bucket object inputFile defaultPutObjectOptions
1105798
1106799 step "copy object"
1107− let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
800+ let srcInfo = defaultSourceInfo { srcBucket = bucket, srcObject = object}
1108801 (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
1109802
1110803 -- retrieve obj info to check
@@ -1115,9 +808,8 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1115808
1116809 let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
1117810
1118− liftIO $
1119− (s == size1 && e == etag && isMTimeDiffOk)
1120− @? "Copied object did not match expected."
811+ liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
812+ "Copied object did not match expected."
1121813
1122814 step "cleanup actions"
1123815 removeObject bucket object
@@ -1138,18 +830,12 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1138830 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
1139831
1140832 step "put object parts 1-3"
1141− let srcInfo' = defaultSourceInfo {srcBucket = bucket, srcObject = srcObj}
1142− dstInfo' = defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1143− parts <- forM [1 .. 3] $ \p -> do
1144− (etag', _) <-
1145− copyObjectPart
1146− dstInfo'
1147− srcInfo'
1148− { srcRange = Just $ (,) ((p - 1) * mb5) ((p - 1) * mb5 + (mb5 - 1))
1149− }
1150− uid
1151− (fromIntegral p)
1152− []
833+ let srcInfo' = defaultSourceInfo { srcBucket = bucket, srcObject = srcObj }
834+ dstInfo' = defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj }
835+ parts <- forM [1..3] $ \p -> do
836+ (etag', _) <- copyObjectPart dstInfo' srcInfo'{
837+ srcRange = Just $ (,) ((p-1)*mb5) ((p-1)*mb5 + (mb5 - 1))
838+ } uid (fromIntegral p) []
1153839 return (fromIntegral p, etag')
1154840
1155841 step "complete multipart"
@@ -1195,13 +881,11 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1195881 fPutObject bucket src inputFile' defaultPutObjectOptions
1196882
1197883 step "copy last 10MiB of object"
1198− copyObject
1199− defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1200− defaultSourceInfo
1201− { srcBucket = bucket,
1202− srcObject = src,
1203− srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
1204− }
884+ copyObject defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj } defaultSourceInfo {
885+ srcBucket = bucket
886+ , srcObject = src
887+ , srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
888+ }
1205889
1206890 step "verify uploaded object"
1207891 cSize <- oiSize <$> headObject bucket copyObj []
@@ -1212,72 +896,42 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1212896
1213897getNPutSSECTest :: TestTree
1214898getNPutSSECTest =
1215− funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
1216− -- Skip this test if the server is not using TLS as encryption is
1217− -- disabled anyway.
1218− isTLSConn <- asks (connectIsSecure . mcConnInfo)
1219− if isTLSConn
1220− then do
1221− step "Make an encryption key"
1222− key <- case mkSSECKey $ BS.pack [0 .. 31] of
1223− Nothing -> liftIO $ assertFailure "This should not happen"
1224− Just k -> return k
1225−
1226− let mb1 = 1024 * 1024
1227− obj = "1"
1228− step "Upload an object using the encryption key"
1229− rFile <- mkRandFile mb1
1230− let putOpts = defaultPutObjectOptions {pooSSE = Just $ SSEC key}
1231− fPutObject bucket obj rFile putOpts
1232−
1233− step "Stat object without key - should fail"
1234− headRes <- try $ statObject bucket obj defaultGetObjectOptions
1235− case headRes of
1236− Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
1237− Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
1238− | NC.responseStatus rsp == HT.status400 -> return ()
1239− | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1240− Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1241−
1242− step "Get file and check length"
1243− dstFile <- mkRandFile 0
1244− let getOpts = defaultGetObjectOptions {gooSSECKey = Just key}
1245− fGetObject bucket obj dstFile getOpts
1246−
1247− gotSize <- withNewHandle dstFile getFileSize
1248− liftIO $
1249− gotSize
1250− == Right (Just mb1)
1251− @? "Wrong file size of object when getting"
1252−
1253− step "Cleanup"
1254− deleteObject bucket obj
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)
899+ funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
900+ -- Skip this test if the server is not using TLS as encryption is
901+ -- disabled anyway.
902+ isTLSConn <- asks (connectIsSecure . mcConnInfo)
903+ if isTLSConn
904+ then do step "Make an encryption key"
905+ key <- case mkSSECKey $ BS.pack [0..31] of
906+ Nothing -> liftIO $ assertFailure "This should not happen"
907+ Just k -> return k
908+
909+ let mb1 = 1024*1024
910+ obj = "1"
911+ step "Upload an object using the encryption key"
912+ rFile <- mkRandFile mb1
913+ let putOpts = defaultPutObjectOptions { pooSSE = Just $ SSEC key }
914+ fPutObject bucket obj rFile putOpts
915+
916+ step "Stat object without key - should fail"
917+ headRes <- try $ statObject bucket obj defaultGetObjectOptions
918+ case headRes of
919+ Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
920+ Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
921+ | NC.responseStatus rsp == HT.status400 -> return ()
922+ | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
923+ Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
924+
925+ step "Get file and check length"
926+ dstFile <- mkRandFile 0
927+ let getOpts = defaultGetObjectOptions { gooSSECKey = Just key }
928+ fGetObject bucket obj dstFile getOpts
929+
930+ gotSize <- withNewHandle dstFile getFileSize
931+ liftIO $ gotSize == Right (Just mb1) @?
932+ "Wrong file size of object when getting"
933+
934+ step "Cleanup"
935+ deleteObject bucket obj
936+
937+ else step "Skipping encryption test as server is not using TLS"
test/Network/Minio/API/Test.hs+61−75
@@ -15,102 +15,88 @@
1515--
1616
1717module Network.Minio.API.Test
18− ( bucketNameValidityTests,
19− objectNameValidityTests,
20− parseServerInfoJSONTest,
21− parseHealStatusTest,
22− parseHealStartRespTest,
23− )
24−where
25−
26−import Data.Aeson (eitherDecode)
27−import Network.Minio.API
28−import Network.Minio.AdminAPI
29−import Test.Tasty
30−import Test.Tasty.HUnit
18+ ( bucketNameValidityTests
19+ , objectNameValidityTests
20+ , parseServerInfoJSONTest
21+ , parseHealStatusTest
22+ , parseHealStartRespTest
23+ ) where
24+
25+import Data.Aeson (eitherDecode)
26+import Test.Tasty
27+import Test.Tasty.HUnit
28+
29+import Lib.Prelude
30+
31+import Network.Minio.AdminAPI
32+import Network.Minio.API
3133
3234assertBool' :: Bool -> Assertion
3335assertBool' = assertBool "Test failed!"
3436
3537bucketNameValidityTests :: TestTree
36−bucketNameValidityTests =
37− testGroup
38− "Bucket Name Validity Tests"
39− [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName "",
40− testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab",
41− testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
42− testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD",
43− testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2",
44− testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-",
45− testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg",
46− testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1",
47− testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea",
48− testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d",
49− testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
50− ]
38+bucketNameValidityTests = testGroup "Bucket Name Validity Tests"
39+ [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName ""
40+ , testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab"
41+ , testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
42+ , testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD"
43+ , testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2"
44+ , testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-"
45+ , testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg"
46+ , testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1"
47+ , testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea"
48+ , testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d"
49+ , testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
50+ ]
5151
5252objectNameValidityTests :: TestTree
53−objectNameValidityTests =
54− testGroup
55− "Object Name Validity Tests"
56− [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName "",
57− testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
58− ]
53+objectNameValidityTests = testGroup "Object Name Validity Tests"
54+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""
55+ , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
56+ ]
5957
6058parseServerInfoJSONTest :: TestTree
61−parseServerInfoJSONTest =
62− testGroup "Parse MinIO Admin API ServerInfo JSON test" $
63− map
64− ( \(tName, tDesc, tfn, tVal) ->
65− testCase tName $
66− assertBool tDesc $
67− tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
68− )
69− testCases
59+parseServerInfoJSONTest = testGroup "Parse MinIO Admin API ServerInfo JSON test" $
60+ map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
61+ tfn (eitherDecode tVal :: Either [Char] [ServerInfo])) testCases
7062 where
71− testCases =
72− [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON),
73− ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON),
74− ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
75− ]
63+ testCases = [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON)
64+ , ("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)
66+ ]
7667 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+
7769 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+
7871 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\":[]}}}]"
7972
8073parseHealStatusTest :: TestTree
81−parseHealStatusTest =
82− testGroup "Parse MinIO Admin API HealStatus JSON test" $
83− map
84− ( \(tName, tDesc, tfn, tVal) ->
85− testCase tName $
86− assertBool tDesc $
87− tfn (eitherDecode tVal :: Either [Char] HealStatus)
88− )
89− testCases
74+parseHealStatusTest = testGroup "Parse MinIO Admin API HealStatus JSON test" $
75+ map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
76+ tfn (eitherDecode tVal :: Either [Char] HealStatus)) testCases
77+
9078 where
91− testCases =
92− [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON'),
93− ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON'),
94− ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
95− ]
79+ testCases = [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON')
80+ , ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON')
81+ , ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
82+ ]
83+
9684 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+
9786 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+
9888 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}]}"
9989
10090parseHealStartRespTest :: TestTree
101−parseHealStartRespTest =
102− testGroup "Parse MinIO Admin API HealStartResp JSON test" $
103− map
104− ( \(tName, tDesc, tfn, tVal) ->
105− testCase tName $
106− assertBool tDesc $
107− tfn (eitherDecode tVal :: Either [Char] HealStartResp)
108− )
109− testCases
91+parseHealStartRespTest = testGroup "Parse MinIO Admin API HealStartResp JSON test" $
92+ map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
93+ tfn (eitherDecode tVal :: Either [Char] HealStartResp)) testCases
94+
11095 where
111− testCases =
112− [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON),
113− ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
114− ]
96+ testCases = [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON)
97+ , ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
98+ ]
99+
115100 hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
101+
116102 missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
test/Network/Minio/JsonParser/Test.hs+27−26
@@ -15,26 +15,26 @@
1515--
1616
1717module Network.Minio.JsonParser.Test
18− ( jsonParserTests,
19− )
20−where
18+ (
19+ jsonParserTests
20+ ) where
2121
22−import Lib.Prelude
23−import Network.Minio.Errors
24−import Network.Minio.JsonParser
25−import Test.Tasty
26−import Test.Tasty.HUnit
27−import UnliftIO (MonadUnliftIO)
22+import Test.Tasty
23+import Test.Tasty.HUnit
24+import UnliftIO (MonadUnliftIO)
25+
26+import Lib.Prelude
27+
28+import Network.Minio.Errors
29+import Network.Minio.JsonParser
2830
2931jsonParserTests :: TestTree
30−jsonParserTests =
31− testGroup
32− "JSON Parser Tests"
33− [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34− ]
32+jsonParserTests = testGroup "JSON Parser Tests"
33+ [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34+ ]
3535
3636tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
37−tryValidationErr = try
37+tryValidationErr act = try act
3838
3939assertValidationErr :: MErrV -> Assertion
4040assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
@@ -44,20 +44,21 @@ testParseErrResponseJSON = do
4444 -- 1. Test parsing of an invalid error json.
4545 parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
4646 when (isRight parseResE) $
47− assertFailure $
48− "Parsing should have failed => " ++ show parseResE
47+ assertFailure $ "Parsing should have failed => " ++ show parseResE
4948
5049 forM_ cases $ \(jsondata, sErr) -> do
5150 parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
5251 either assertValidationErr (@?= sErr) parseErr
52+
5353 where
54− cases =
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\"}",
57− ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58− ),
59− -- 3. Test parsing of a valid, empty Resource.
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\"}",
61− ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
62− )
54+ cases = [
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\"}",
57+ ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58+ )
59+ ,
60+ -- 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\"}",
62+ ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
63+ )
6364 ]
test/Network/Minio/TestHelpers.hs+8−8
@@ -15,18 +15,18 @@
1515--
1616
1717module Network.Minio.TestHelpers
18− ( runTestNS,
19− )
20−where
18+ ( runTestNS
19+ ) where
2120
22−import Network.Minio.Data
21+import Network.Minio.Data
2322
24−newtype TestNS = TestNS {testNamespace :: Text}
23+import Lib.Prelude
24+
25+newtype TestNS = TestNS { testNamespace :: Text }
2526
2627instance HasSvcNamespace TestNS where
2728 getSvcNamespace = testNamespace
2829
2930runTestNS :: ReaderT TestNS m a -> m a
30−runTestNS =
31− flip runReaderT $
32− TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
31+runTestNS = flip runReaderT $
32+ TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs+17−15
@@ -15,31 +15,33 @@
1515--
1616
1717module Network.Minio.Utils.Test
18− ( limitedMapConcurrentlyTests,
19− )
20−where
18+ (
19+ limitedMapConcurrentlyTests
20+ ) where
2121
22−import Network.Minio.Utils
23−import Test.Tasty
24−import Test.Tasty.HUnit
22+import Test.Tasty
23+import Test.Tasty.HUnit
24+
25+import Lib.Prelude
26+
27+import Network.Minio.Utils
2528
2629limitedMapConcurrentlyTests :: TestTree
27−limitedMapConcurrentlyTests =
28− testGroup
29− "limitedMapConcurrently Tests"
30− [ testCase "Test with various thread counts" testLMC
31− ]
30+limitedMapConcurrentlyTests = testGroup "limitedMapConcurrently Tests"
31+ [ testCase "Test with various thread counts" testLMC
32+ ]
3233
3334testLMC :: Assertion
3435testLMC = do
3536 let maxNum = 50
3637 -- test with thread count of 1 to 2*maxNum
37− forM_ [1 .. (2 * maxNum)] $ \threads -> do
38− res <- limitedMapConcurrently threads compute [1 .. maxNum]
38+ forM_ [1..(2*maxNum)] $ \threads -> do
39+ res <- limitedMapConcurrently threads compute [1..maxNum]
3940 sum res @?= overallResultCheck maxNum
4041 where
4142 -- simple function to run in each thread
4243 compute :: Int -> IO Int
43− compute n = return $ sum [1 .. n]
44+ compute n = return $ sum [1..n]
45+
4446 -- function to check overall result
45− overallResultCheck n = sum $ map (\t -> (t * (t + 1)) `div` 2) [1 .. n]
47+ overallResultCheck n = sum $ map (\t -> (t * (t+1)) `div` 2) [1..n]
test/Network/Minio/XmlGenerator/Test.hs+98−137
@@ -13,33 +13,30 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE QuasiQuotes #-}
1716
17+{-# LANGUAGE QuasiQuotes #-}
1818module Network.Minio.XmlGenerator.Test
19− ( xmlGeneratorTests,
20− )
21−where
19+ ( xmlGeneratorTests
20+ ) where
21+
22+import Test.Tasty
23+import Test.Tasty.HUnit
24+import Text.RawString.QQ (r)
2225
23−import qualified Data.ByteString.Lazy as LBS
24−import Lib.Prelude
25−import Network.Minio.Data
26−import Network.Minio.TestHelpers
27−import Network.Minio.XmlGenerator
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)
26+import Lib.Prelude
27+
28+import Network.Minio.Data
29+import Network.Minio.TestHelpers
30+import Network.Minio.XmlGenerator
31+import Network.Minio.XmlParser (parseNotification)
3332
3433xmlGeneratorTests :: TestTree
35−xmlGeneratorTests =
36− testGroup
37− "XML Generator Tests"
38− [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig,
39− testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest,
40− testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest,
41− testCase "Test mkSelectRequest" testMkSelectRequest
42− ]
34+xmlGeneratorTests = testGroup "XML Generator Tests"
35+ [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig
36+ , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest
37+ , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest
38+ , testCase "Test mkSelectRequest" testMkSelectRequest
39+ ]
4340
4441testMkCreateBucketConfig :: Assertion
4542testMkCreateBucketConfig = do
@@ -47,136 +44,100 @@ testMkCreateBucketConfig = do
4744 assertEqual "CreateBucketConfiguration xml should match: " expected $
4845 mkCreateBucketConfig ns "EU"
4946 where
50− expected =
51− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
52− \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
53− \<LocationConstraint>EU</LocationConstraint>\
54− \</CreateBucketConfiguration>"
47+ expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
48+ \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
49+ \<LocationConstraint>EU</LocationConstraint>\
50+ \</CreateBucketConfiguration>"
5551
5652testMkCompleteMultipartUploadRequest :: Assertion
5753testMkCompleteMultipartUploadRequest =
5854 assertEqual "completeMultipartUpload xml should match: " expected $
59− mkCompleteMultipartUploadRequest [(1, "abc")]
55+ mkCompleteMultipartUploadRequest [(1, "abc")]
6056 where
61− expected =
62− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
63− \<CompleteMultipartUpload>\
64− \<Part>\
65− \<PartNumber>1</PartNumber><ETag>abc</ETag>\
66− \</Part>\
67− \</CompleteMultipartUpload>"
57+ expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
58+ \<CompleteMultipartUpload>\
59+ \<Part>\
60+ \<PartNumber>1</PartNumber><ETag>abc</ETag>\
61+ \</Part>\
62+ \</CompleteMultipartUpload>"
63+
6864
6965testMkPutNotificationRequest :: Assertion
7066testMkPutNotificationRequest =
7167 forM_ cases $ \val -> do
7268 let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
73− result = fromStrictBS $ mkPutNotificationRequest ns val
69+ result = toS $ mkPutNotificationRequest ns val
7470 ntf <- runExceptT $ runTestNS $ parseNotification result
75− either
76− (\_ -> assertFailure "XML Parse Error!")
77− (@?= val)
78− ntf
71+ either (\_ -> assertFailure "XML Parse Error!")
72+ (@?= val) ntf
7973 where
80− cases =
81− [ Notification
82− []
83− [ NotificationConfig
84− "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
85− "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
86− [ReducedRedundancyLostObject, ObjectCreated]
87− defaultFilter
88− ]
89− [],
90− Notification
91− [ NotificationConfig
92− "1"
93− "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
94− [ObjectCreatedPut]
95− ( Filter $
96− FilterKey $
97− FilterRules
98− [ FilterRule "prefix" "images/",
99− FilterRule "suffix" ".jpg"
100− ]
101− ),
102− NotificationConfig
103− ""
104− "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
105− [ObjectCreated]
106− defaultFilter
107− ]
108− [ NotificationConfig
109− ""
110− "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
111− [ReducedRedundancyLostObject]
112− defaultFilter
113− ]
114− [ NotificationConfig
115− "ObjectCreatedEvents"
116− "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
117− [ObjectCreated]
118− defaultFilter
119− ]
120− ]
74+ cases = [ Notification []
75+ [ NotificationConfig
76+ "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
77+ "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
78+ [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
79+ ]
80+ []
81+ , Notification
82+ [ NotificationConfig
83+ "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
84+ [ObjectCreatedPut]
85+ (Filter $ FilterKey $ FilterRules
86+ [ FilterRule "prefix" "images/"
87+ , FilterRule "suffix" ".jpg"])
88+ , NotificationConfig
89+ "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
90+ [ObjectCreated] defaultFilter
91+ ]
92+ [ NotificationConfig
93+ "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
94+ [ReducedRedundancyLostObject] defaultFilter
95+ ]
96+ [ NotificationConfig
97+ "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
98+ [ObjectCreated] defaultFilter
99+ ]
100+ ]
121101
122102testMkSelectRequest :: Assertion
123103testMkSelectRequest = mapM_ assertFn cases
124104 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
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
134− "Select * from S3Object"
135− SQL
136− ( InputSerialization
137− (Just CompressionTypeGzip)
138− ( InputFormatCSV $
139− fileHeaderInfo FileHeaderIgnore
140− <> recordDelimiter "\n"
141− <> fieldDelimiter ","
142− <> quoteCharacter "\""
143− <> quoteEscapeCharacter "\""
105+ assertFn (a, b) = assertEqual "selectRequest XML should match: " b $ mkSelectRequest a
106+ cases = [ ( SelectRequest "Select * from S3Object" SQL
107+ (InputSerialization (Just CompressionTypeGzip)
108+ (InputFormatCSV $ fileHeaderInfo FileHeaderIgnore
109+ <> recordDelimiter "\n"
110+ <> fieldDelimiter ","
111+ <> quoteCharacter "\""
112+ <> quoteEscapeCharacter "\""
113+ ))
114+ (OutputSerializationCSV $ quoteFields QuoteFieldsAsNeeded
115+ <> recordDelimiter "\n"
116+ <> fieldDelimiter ","
117+ <> quoteCharacter "\""
118+ <> quoteEscapeCharacter "\""
144119 )
145− )
146− ( OutputSerializationCSV $
147− quoteFields QuoteFieldsAsNeeded
148− <> recordDelimiter "\n"
149− <> fieldDelimiter ","
150− <> quoteCharacter "\""
151− <> quoteEscapeCharacter "\""
152− )
153− (Just False),
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>
155−</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
156−</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
157− ),
158− ( setRequestProgressEnabled False $
159− setInputCompressionType CompressionTypeGzip $
160− selectRequest
161− "Select * from S3Object"
162− documentJsonInput
163− (outputJSONFromRecordDelimiter "\n"),
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>
120+ (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>
122+</RecordDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
123+</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
124+ )
125+ , ( setRequestProgressEnabled False $
126+ setInputCompressionType CompressionTypeGzip $
127+ selectRequest "Select * from S3Object" documentJsonInput
128+ (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>
165130</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
166− ),
167− ( setRequestProgressEnabled False $
168− setInputCompressionType CompressionTypeNone $
169− selectRequest
170− "Select * from S3Object"
171− defaultParquetInput
172− ( outputCSVFromProps $
173− quoteFields QuoteFieldsAsNeeded
174− <> recordDelimiter "\n"
175− <> fieldDelimiter ","
176− <> quoteCharacter "\""
177− <> quoteEscapeCharacter "\""
178− ),
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>
180−</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
181− )
182− ]
131+ )
132+ , ( setRequestProgressEnabled False $
133+ setInputCompressionType CompressionTypeNone $
134+ selectRequest "Select * from S3Object" defaultParquetInput
135+ (outputCSVFromProps $ quoteFields QuoteFieldsAsNeeded
136+ <> recordDelimiter "\n"
137+ <> fieldDelimiter ","
138+ <> quoteCharacter "\""
139+ <> quoteEscapeCharacter "\"")
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>
141+</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
142+ )
143+ ]
test/Network/Minio/XmlParser/Test.hs+276−298
@@ -13,49 +13,48 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE QuasiQuotes #-}
1716
17+{-# LANGUAGE QuasiQuotes #-}
1818module Network.Minio.XmlParser.Test
19− ( xmlParserTests,
20− )
21−where
22−
23−import qualified Data.HashMap.Strict as H
24−import Data.Time (fromGregorian)
25−import Lib.Prelude
26−import Network.Minio.Data
27−import Network.Minio.Errors
28−import Network.Minio.TestHelpers
29−import Network.Minio.XmlParser
30−import Test.Tasty
31−import Test.Tasty.HUnit
32−import Text.RawString.QQ (r)
33−import UnliftIO (MonadUnliftIO)
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+
29+import Lib.Prelude
30+
31+import Network.Minio.Data
32+import Network.Minio.Errors
33+import Network.Minio.TestHelpers
34+import Network.Minio.XmlParser
3435
3536xmlParserTests :: TestTree
36−xmlParserTests =
37− testGroup
38− "XML Parser Tests"
39− [ testCase "Test parseLocation" testParseLocation,
40− testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload,
41− testCase "Test parseListObjectsResponse" testParseListObjectsResult,
42− testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result,
43− testCase "Test parseListUploadsresponse" testParseListIncompleteUploads,
44− testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse,
45− testCase "Test parseListPartsResponse" testParseListPartsResponse,
46− testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse,
47− testCase "Test parseNotification" testParseNotification,
48− testCase "Test parseSelectProgress" testParseSelectProgress
49− ]
37+xmlParserTests = testGroup "XML Parser Tests"
38+ [ testCase "Test parseLocation" testParseLocation
39+ , testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload
40+ , testCase "Test parseListObjectsResponse" testParseListObjectsResult
41+ , testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result
42+ , testCase "Test parseListUploadsresponse" testParseListIncompleteUploads
43+ , testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse
44+ , testCase "Test parseListPartsResponse" testParseListPartsResponse
45+ , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse
46+ , testCase "Test parseNotification" testParseNotification
47+ , testCase "Test parseSelectProgress" testParseSelectProgress
48+ ]
5049
5150tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
52−tryValidationErr = try
51+tryValidationErr act = try act
5352
5453assertValidtionErr :: MErrV -> Assertion
5554assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
5655
5756eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
58−eitherValidationErr (Left e) _ = assertValidtionErr e
57+eitherValidationErr (Left e) _ = assertValidtionErr e
5958eitherValidationErr (Right a) f = f a
6059
6160testParseLocation :: Assertion
@@ -63,224 +62,224 @@ testParseLocation = do
6362 -- 1. Test parsing of an invalid location constraint xml.
6463 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
6564 when (isRight parseResE) $
66− assertFailure $
67− "Parsing should have failed => " ++ show parseResE
65+ assertFailure $ "Parsing should have failed => " ++ show parseResE
6866
6967 forM_ cases $ \(xmldata, expectedLocation) -> do
7068 parseLocE <- tryValidationErr $ parseLocation xmldata
7169 either assertValidtionErr (@?= expectedLocation) parseLocE
7270 where
73− cases =
74− [ -- 2. Test parsing of a valid location xml.
75− ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
76− \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
77− "EU"
78− ),
79− -- 3. Test parsing of a valid, empty location xml.
80− ( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
81− "us-east-1"
82− )
71+ cases = [
72+ -- 2. Test parsing of a valid location xml.
73+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
74+ \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
75+ "EU"
76+ )
77+ ,
78+ -- 3. Test parsing of a valid, empty location xml.
79+ ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
80+ "us-east-1"
81+ )
8382 ]
8483
84+
8585testParseNewMultipartUpload :: Assertion
8686testParseNewMultipartUpload = do
8787 forM_ cases $ \(xmldata, expectedUploadId) -> do
8888 parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
8989 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
9090 where
91− cases =
92− [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
93− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
94− \ <Bucket>example-bucket</Bucket>\
95− \ <Key>example-object</Key>\
96− \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
97− \</InitiateMultipartUploadResult>",
98− "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
99− ),
100− ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
101− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
102− \ <Bucket>example-bucket</Bucket>\
103− \ <Key>example-object</Key>\
104− \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
105− \</InitiateMultipartUploadResult>",
106− "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
107− )
91+ cases = [
92+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
93+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
94+ \ <Bucket>example-bucket</Bucket>\
95+ \ <Key>example-object</Key>\
96+ \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
97+ \</InitiateMultipartUploadResult>",
98+ "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
99+ ),
100+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
101+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
102+ \ <Bucket>example-bucket</Bucket>\
103+ \ <Key>example-object</Key>\
104+ \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
105+ \</InitiateMultipartUploadResult>",
106+ "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
107+ )
108108 ]
109109
110110testParseListObjectsResult :: Assertion
111111testParseListObjectsResult = do
112− let xmldata =
113− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
114− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
115− \<Name>bucket</Name>\
116− \<Prefix/>\
117− \<NextContinuationToken>opaque</NextContinuationToken>\
118− \<KeyCount>1000</KeyCount>\
119− \<MaxKeys>1000</MaxKeys>\
120− \<IsTruncated>true</IsTruncated>\
121− \<Contents>\
122− \<Key>my-image.jpg</Key>\
123− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
124− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
125− \<Size>434234</Size>\
126− \<StorageClass>STANDARD</StorageClass>\
127− \</Contents>\
128− \</ListBucketResult>"
129− expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
130− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
131− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
112+ let
113+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
114+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
115+ \<Name>bucket</Name>\
116+ \<Prefix/>\
117+ \<NextContinuationToken>opaque</NextContinuationToken>\
118+ \<KeyCount>1000</KeyCount>\
119+ \<MaxKeys>1000</MaxKeys>\
120+ \<IsTruncated>true</IsTruncated>\
121+ \<Contents>\
122+ \<Key>my-image.jpg</Key>\
123+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
124+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
125+ \<Size>434234</Size>\
126+ \<StorageClass>STANDARD</StorageClass>\
127+ \</Contents>\
128+ \</ListBucketResult>"
129+
130+ expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
131+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 Map.empty
132+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
132133
133134 parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
134135 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
135136
136137testParseListObjectsV1Result :: Assertion
137138testParseListObjectsV1Result = do
138− let xmldata =
139− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
140− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
141− \<Name>bucket</Name>\
142− \<Prefix/>\
143− \<NextMarker>my-image1.jpg</NextMarker>\
144− \<KeyCount>1000</KeyCount>\
145− \<MaxKeys>1000</MaxKeys>\
146− \<IsTruncated>true</IsTruncated>\
147− \<Contents>\
148− \<Key>my-image.jpg</Key>\
149− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
150− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
151− \<Size>434234</Size>\
152− \<StorageClass>STANDARD</StorageClass>\
153− \</Contents>\
154− \</ListBucketResult>"
155− expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
156− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
157− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
139+ let
140+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
141+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
142+ \<Name>bucket</Name>\
143+ \<Prefix/>\
144+ \<NextMarker>my-image1.jpg</NextMarker>\
145+ \<KeyCount>1000</KeyCount>\
146+ \<MaxKeys>1000</MaxKeys>\
147+ \<IsTruncated>true</IsTruncated>\
148+ \<Contents>\
149+ \<Key>my-image.jpg</Key>\
150+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
151+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
152+ \<Size>434234</Size>\
153+ \<StorageClass>STANDARD</StorageClass>\
154+ \</Contents>\
155+ \</ListBucketResult>"
156+
157+ expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
158+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 Map.empty
159+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
158160
159161 parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
160162 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
161163
162164testParseListIncompleteUploads :: Assertion
163165testParseListIncompleteUploads = do
164− let xmldata =
165− "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
166− \<Bucket>example-bucket</Bucket>\
167− \<KeyMarker/>\
168− \<UploadIdMarker/>\
169− \<NextKeyMarker>sample.jpg</NextKeyMarker>\
170− \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
171− \<Delimiter>/</Delimiter>\
172− \<Prefix/>\
173− \<MaxUploads>1000</MaxUploads>\
174− \<IsTruncated>false</IsTruncated>\
175− \<Upload>\
176− \<Key>sample.jpg</Key>\
177− \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
178− \<Initiator>\
179− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
180− \<DisplayName>s3-nickname</DisplayName>\
181− \</Initiator>\
182− \<Owner>\
183− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
184− \<DisplayName>s3-nickname</DisplayName>\
185− \</Owner>\
186− \<StorageClass>STANDARD</StorageClass>\
187− \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
188− \</Upload>\
189− \<CommonPrefixes>\
190− \<Prefix>photos/</Prefix>\
191− \</CommonPrefixes>\
192− \<CommonPrefixes>\
193− \<Prefix>videos/</Prefix>\
194− \</CommonPrefixes>\
195− \</ListMultipartUploadsResult>"
196− expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
197− uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
198− initTime = UTCTime (fromGregorian 2010 11 26) 69857
199− prefixes = ["photos/", "videos/"]
166+ let
167+ xmldata = "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
168+ \<Bucket>example-bucket</Bucket>\
169+ \<KeyMarker/>\
170+ \<UploadIdMarker/>\
171+ \<NextKeyMarker>sample.jpg</NextKeyMarker>\
172+ \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
173+ \<Delimiter>/</Delimiter>\
174+ \<Prefix/>\
175+ \<MaxUploads>1000</MaxUploads>\
176+ \<IsTruncated>false</IsTruncated>\
177+ \<Upload>\
178+ \<Key>sample.jpg</Key>\
179+ \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
180+ \<Initiator>\
181+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
182+ \<DisplayName>s3-nickname</DisplayName>\
183+ \</Initiator>\
184+ \<Owner>\
185+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
186+ \<DisplayName>s3-nickname</DisplayName>\
187+ \</Owner>\
188+ \<StorageClass>STANDARD</StorageClass>\
189+ \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
190+ \</Upload>\
191+ \<CommonPrefixes>\
192+ \<Prefix>photos/</Prefix>\
193+ \</CommonPrefixes>\
194+ \<CommonPrefixes>\
195+ \<Prefix>videos/</Prefix>\
196+ \</CommonPrefixes>\
197+ \</ListMultipartUploadsResult>"
198+ expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
199+ uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
200+ initTime = UTCTime (fromGregorian 2010 11 26) 69857
201+ prefixes = ["photos/", "videos/"]
200202
201203 parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
202204 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
203205
206+
204207testParseCompleteMultipartUploadResponse :: Assertion
205208testParseCompleteMultipartUploadResponse = do
206− let xmldata =
207− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
208− \<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
209− \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
210− \<Bucket>Example-Bucket</Bucket>\
211− \<Key>Example-Object</Key>\
212− \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
213− \</CompleteMultipartUploadResult>"
214− expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
209+ let
210+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
211+\<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
212+ \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
213+ \<Bucket>Example-Bucket</Bucket>\
214+ \<Key>Example-Object</Key>\
215+ \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
216+\</CompleteMultipartUploadResult>"
217+ expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
215218
216219 parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
217220 eitherValidationErr parsedETagE (@?= expectedETag)
218221
219222testParseListPartsResponse :: Assertion
220223testParseListPartsResponse = do
221− let xmldata =
222− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
223− \<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
224− \<Bucket>example-bucket</Bucket>\
225− \<Key>example-object</Key>\
226− \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
227− \<Initiator>\
228− \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
229− \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
230− \</Initiator>\
231− \<Owner>\
232− \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
233− \<DisplayName>someName</DisplayName>\
234− \</Owner>\
235− \<StorageClass>STANDARD</StorageClass>\
236− \<PartNumberMarker>1</PartNumberMarker>\
237− \<NextPartNumberMarker>3</NextPartNumberMarker>\
238− \<MaxParts>2</MaxParts>\
239− \<IsTruncated>true</IsTruncated>\
240− \<Part>\
241− \<PartNumber>2</PartNumber>\
242− \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
243− \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
244− \<Size>10485760</Size>\
245− \</Part>\
246− \<Part>\
247− \<PartNumber>3</PartNumber>\
248− \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
249− \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
250− \<Size>10485760</Size>\
251− \</Part>\
252− \</ListPartsResult>"
253− expectedListResult = ListPartsResult True (Just 3) [part1, part2]
254− part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
255− modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
256− part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
257− modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
224+ let
225+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
226+\<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
227+ \<Bucket>example-bucket</Bucket>\
228+ \<Key>example-object</Key>\
229+ \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
230+ \<Initiator>\
231+ \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
232+ \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
233+ \</Initiator>\
234+ \<Owner>\
235+ \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
236+ \<DisplayName>someName</DisplayName>\
237+ \</Owner>\
238+ \<StorageClass>STANDARD</StorageClass>\
239+ \<PartNumberMarker>1</PartNumberMarker>\
240+ \<NextPartNumberMarker>3</NextPartNumberMarker>\
241+ \<MaxParts>2</MaxParts>\
242+ \<IsTruncated>true</IsTruncated>\
243+ \<Part>\
244+ \<PartNumber>2</PartNumber>\
245+ \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
246+ \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
247+ \<Size>10485760</Size>\
248+ \</Part>\
249+ \<Part>\
250+ \<PartNumber>3</PartNumber>\
251+ \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
252+ \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
253+ \<Size>10485760</Size>\
254+ \</Part>\
255+\</ListPartsResult>"
256+
257+ expectedListResult = ListPartsResult True (Just 3) [part1, part2]
258+ part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
259+ modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
260+ part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
261+ modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
258262
259263 parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
260264 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
261265
262266testParseCopyObjectResponse :: Assertion
263267testParseCopyObjectResponse = do
264− let cases =
265− [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
266− \<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
267− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
268− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
269− \</CopyObjectResult>",
270− ( "\"9b2cf535f27731c974343645a3985328\"",
271− UTCTime (fromGregorian 2009 10 28) 81120
272− )
273− ),
274− ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
275− \<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
276− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
277− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
278− \</CopyPartResult>",
279− ( "\"9b2cf535f27731c974343645a3985328\"",
280− UTCTime (fromGregorian 2009 10 28) 81120
281− )
282− )
283− ]
268+ let
269+ cases = [ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
270+\<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
271+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
272+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
273+\</CopyObjectResult>",
274+ ("\"9b2cf535f27731c974343645a3985328\"",
275+ UTCTime (fromGregorian 2009 10 28) 81120))
276+ , ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
277+\<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
278+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
279+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
280+\</CopyPartResult>",
281+ ("\"9b2cf535f27731c974343645a3985328\"",
282+ UTCTime (fromGregorian 2009 10 28) 81120))]
284283
285284 forM_ cases $ \(xmldata, (etag, modTime)) -> do
286285 parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
@@ -288,89 +287,73 @@ testParseCopyObjectResponse = do
288287
289288testParseNotification :: Assertion
290289testParseNotification = do
291− let cases =
292− [ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
293− \ <TopicConfiguration>\
294− \ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
295− \ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
296− \ <Event>s3:ReducedRedundancyLostObject</Event>\
297− \ <Event>s3:ObjectCreated:*</Event>\
298− \ </TopicConfiguration>\
299− \</NotificationConfiguration>",
300− Notification
301− []
302− [ NotificationConfig
290+ let
291+ cases = [ ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
292+\ <TopicConfiguration>\
293+\ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
294+\ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
295+\ <Event>s3:ReducedRedundancyLostObject</Event>\
296+\ <Event>s3:ObjectCreated:*</Event>\
297+\ </TopicConfiguration>\
298+\</NotificationConfiguration>",
299+ Notification []
300+ [ NotificationConfig
303301 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
304302 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
305− [ReducedRedundancyLostObject, ObjectCreated]
306− defaultFilter
307− ]
308− []
309− ),
310− ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
311− \ <CloudFunctionConfiguration>\
312− \ <Id>ObjectCreatedEvents</Id>\
313− \ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
314− \ <Event>s3:ObjectCreated:*</Event>\
315− \ </CloudFunctionConfiguration>\
316− \ <QueueConfiguration>\
317− \ <Id>1</Id>\
318− \ <Filter>\
319− \ <S3Key>\
320− \ <FilterRule>\
321− \ <Name>prefix</Name>\
322− \ <Value>images/</Value>\
323− \ </FilterRule>\
324− \ <FilterRule>\
325− \ <Name>suffix</Name>\
326− \ <Value>.jpg</Value>\
327− \ </FilterRule>\
328− \ </S3Key>\
329− \ </Filter>\
330− \ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
331− \ <Event>s3:ObjectCreated:Put</Event>\
332− \ </QueueConfiguration>\
333− \ <TopicConfiguration>\
334− \ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
335− \ <Event>s3:ReducedRedundancyLostObject</Event>\
336− \ </TopicConfiguration>\
337− \ <QueueConfiguration>\
338− \ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
339− \ <Event>s3:ObjectCreated:*</Event>\
340− \ </QueueConfiguration>)\
341− \</NotificationConfiguration>",
342− Notification
343− [ NotificationConfig
344− "1"
345− "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
346− [ObjectCreatedPut]
347− ( Filter $
348− FilterKey $
349− FilterRules
350− [ FilterRule "prefix" "images/",
351− FilterRule "suffix" ".jpg"
352− ]
353− ),
354− NotificationConfig
355− ""
356− "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
357− [ObjectCreated]
358− defaultFilter
359− ]
360− [ NotificationConfig
361− ""
362− "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
363− [ReducedRedundancyLostObject]
364− defaultFilter
365− ]
366− [ NotificationConfig
367− "ObjectCreatedEvents"
368− "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
369− [ObjectCreated]
370− defaultFilter
371− ]
372− )
373− ]
303+ [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
304+ ]
305+ [])
306+ , ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
307+\ <CloudFunctionConfiguration>\
308+\ <Id>ObjectCreatedEvents</Id>\
309+\ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
310+\ <Event>s3:ObjectCreated:*</Event>\
311+\ </CloudFunctionConfiguration>\
312+\ <QueueConfiguration>\
313+\ <Id>1</Id>\
314+\ <Filter>\
315+\ <S3Key>\
316+\ <FilterRule>\
317+\ <Name>prefix</Name>\
318+\ <Value>images/</Value>\
319+\ </FilterRule>\
320+\ <FilterRule>\
321+\ <Name>suffix</Name>\
322+\ <Value>.jpg</Value>\
323+\ </FilterRule>\
324+\ </S3Key>\
325+\ </Filter>\
326+\ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
327+\ <Event>s3:ObjectCreated:Put</Event>\
328+\ </QueueConfiguration>\
329+\ <TopicConfiguration>\
330+\ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
331+\ <Event>s3:ReducedRedundancyLostObject</Event>\
332+\ </TopicConfiguration>\
333+\ <QueueConfiguration>\
334+\ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
335+\ <Event>s3:ObjectCreated:*</Event>\
336+\ </QueueConfiguration>)\
337+\</NotificationConfiguration>",
338+ Notification [ NotificationConfig
339+ "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
340+ [ObjectCreatedPut]
341+ (Filter $ FilterKey $ FilterRules
342+ [FilterRule "prefix" "images/",
343+ FilterRule "suffix" ".jpg"])
344+ , NotificationConfig
345+ "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
346+ [ObjectCreated] defaultFilter
347+ ]
348+ [ NotificationConfig
349+ "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
350+ [ReducedRedundancyLostObject] defaultFilter
351+ ]
352+ [ NotificationConfig
353+ "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
354+ [ObjectCreated] defaultFilter
355+ ])
356+ ]
374357
375358 forM_ cases $ \(xmldata, val) -> do
376359 result <- runExceptT $ runTestNS $ parseNotification xmldata
@@ -379,25 +362,20 @@ testParseNotification = do
379362-- | Tests parsing of both progress and stats
380363testParseSelectProgress :: Assertion
381364testParseSelectProgress = do
382− let cases =
383− [ ( [r|<?xml version="1.0" encoding="UTF-8"?>
365+ let cases = [ ([r|<?xml version="1.0" encoding="UTF-8"?>
384366<Progress>
385367 <BytesScanned>512</BytesScanned>
386368 <BytesProcessed>1024</BytesProcessed>
387369 <BytesReturned>1024</BytesReturned>
388−</Progress>|],
389− Progress 512 1024 1024
390− ),
391− ( [r|<?xml version="1.0" encoding="UTF-8"?>
370+</Progress>|] , Progress 512 1024 1024)
371+ , ([r|<?xml version="1.0" encoding="UTF-8"?>
392372<Stats>
393373 <BytesScanned>512</BytesScanned>
394374 <BytesProcessed>1024</BytesProcessed>
395375 <BytesReturned>1024</BytesReturned>
396−</Stats>|],
397− Progress 512 1024 1024
398− )
399− ]
400−
401− forM_ cases $ \(xmldata, progress) -> do
402− result <- runExceptT $ parseSelectProgress xmldata
403− eitherValidationErr result (@?= progress)
376+</Stats>|], Progress 512 1024 1024)
377+ ]
378+
379+ forM_ cases $ \(xmldata, progress) -> do
380+ result <- runExceptT $ parseSelectProgress xmldata
381+ eitherValidationErr result (@?= progress)
test/Spec.hs+82−83
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -14,17 +14,21 @@
1414-- limitations under the License.
1515--
1616
17−import qualified Data.ByteString as B
18−import qualified Data.List as L
19−import Lib.Prelude
20−import Network.Minio.API.Test
21−import Network.Minio.CopyObject
22−import Network.Minio.Data
23−import Network.Minio.Utils.Test
24−import Network.Minio.XmlGenerator.Test
25−import Network.Minio.XmlParser.Test
26−import Test.Tasty
27−import Test.Tasty.QuickCheck as QC
17+import Test.Tasty
18+import Test.Tasty.QuickCheck as QC
19+
20+import qualified Data.ByteString as B
21+import qualified Data.List as L
22+
23+import Lib.Prelude
24+
25+import Network.Minio.API.Test
26+import Network.Minio.CopyObject
27+import Network.Minio.Data
28+import Network.Minio.PutObject
29+import Network.Minio.Utils.Test
30+import Network.Minio.XmlGenerator.Test
31+import Network.Minio.XmlParser.Test
2832
2933main :: IO ()
3034main = defaultMain tests
@@ -47,87 +51,82 @@ properties = testGroup "Properties" [qcProps] -- [scProps]
4751-- ]
4852
4953qcProps :: TestTree
50−qcProps =
51− testGroup
52− "(checked by QuickCheck)"
53− [ QC.testProperty "selectPartSizes:" $
54− \n ->
55− let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
54+qcProps = testGroup "(checked by QuickCheck)"
55+ [ QC.testProperty "selectPartSizes:" $
56+ \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
57+
5658 -- check that pns increments from 1.
57− isPNumsAscendingFrom1 = all (uncurry (==)) $ zip pns [1 ..]
58− consPairs [] = []
59− consPairs [_] = []
60− consPairs (a : (b : c)) = (a, b) : consPairs (b : c)
59+ isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1..]
60+
61+ consPairs [] = []
62+ consPairs [_] = []
63+ consPairs (a:(b:c)) = (a, b):(consPairs (b:c))
64+
6165 -- check `offs` is monotonically increasing.
62− isOffsetsAsc = all (uncurry (<)) $ consPairs offs
66+ isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs
67+
6368 -- check sizes sums to n.
6469 isSumSizeOk = sum sizes == n
70+
6571 -- check sizes are constant except last
6672 isSizesConstantExceptLast =
67− all (uncurry (==)) (consPairs $ L.init sizes)
73+ all (\(a, b) -> a == b) (consPairs $ L.init sizes)
74+
6875 -- check each part except last is at least minPartSize;
6976 -- last part may be 0 only if it is the only part.
7077 nparts = length sizes
7178 isMinPartSizeOk =
72− if
73− | nparts > 1 -> -- last part can be smaller but > 0
74− all (>= minPartSize) (take (nparts - 1) sizes)
75− && all (> 0) (drop (nparts - 1) sizes)
76− | nparts == 1 -> -- size may be 0 here.
77− maybe True (\x -> x >= 0 && x <= minPartSize) $
78− listToMaybe sizes
79− | otherwise -> False
80− in n < 0
81− || ( isPNumsAscendingFrom1
82− && isOffsetsAsc
83− && isSumSizeOk
84− && isSizesConstantExceptLast
85− && isMinPartSizeOk
86− ),
87− QC.testProperty "selectCopyRanges:" $
88− \(start, end) ->
89− let (_, pairs) = L.unzip (selectCopyRanges (start, end))
90− -- is last part's snd offset end?
91− isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
92− -- is first part's fst offset start
93− isFirstPartOk = maybe False ((start ==) . fst) $ listToMaybe pairs
94− -- each pair is >=64MiB except last, and all those parts
95− -- have same size.
96− initSizes = maybe [] (map (\(a, b) -> b - a + 1) . init) (nonEmpty pairs)
97− isPartSizesOk =
98− all (>= minPartSize) initSizes
99− && maybe
100− True
101− (\k -> all (== k) initSizes)
102− (listToMaybe initSizes)
103− -- returned offsets are contiguous.
104− fsts = drop 1 $ map fst pairs
105− snds = take (length pairs - 1) $ map snd pairs
106− isContParts =
107− length fsts == length snds
108− && all (\(a, b) -> a == b + 1) (zip fsts snds)
109− in start < 0
110− || start > end
111− || (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts),
112− QC.testProperty "mkSSECKey:" $
113− \w8s ->
114− let bs = B.pack w8s
115− r = mkSSECKey bs
116− in case r of
117− Just _ -> B.length bs == 32
79+ if | nparts > 1 -> -- last part can be smaller but > 0
80+ all (>= minPartSize) (take (nparts - 1) sizes) &&
81+ all (\s -> s > 0) (drop (nparts - 1) sizes)
82+ | nparts == 1 -> -- size may be 0 here.
83+ maybe True (\x -> x >= 0 && x <= minPartSize) $
84+ headMay sizes
85+ | otherwise -> False
86+
87+ in n < 0 ||
88+ (isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk &&
89+ isSizesConstantExceptLast && isMinPartSizeOk)
90+
91+ , QC.testProperty "selectCopyRanges:" $
92+ \(start, end) ->
93+ let (_, pairs) = L.unzip (selectCopyRanges (start, end))
94+
95+ -- is last part's snd offset end?
96+ isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
97+ -- is first part's fst offset start
98+ isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
99+
100+ -- each pair is >=64MiB except last, and all those parts
101+ -- have same size.
102+ initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs
103+ isPartSizesOk = all (>= minPartSize) initSizes &&
104+ maybe True (\k -> all (== k) initSizes)
105+ (headMay initSizes)
106+
107+ -- returned offsets are contiguous.
108+ fsts = drop 1 $ map fst pairs
109+ snds = take (length pairs - 1) $ map snd pairs
110+ isContParts = length fsts == length snds &&
111+ and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
112+
113+ in start < 0 || start > end ||
114+ (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts)
115+
116+ , QC.testProperty "mkSSECKey:" $
117+ \w8s -> let bs = B.pack w8s
118+ r = mkSSECKey bs
119+ in case r of
120+ Just _ -> B.length bs == 32
118121 Nothing -> B.length bs /= 32
119− ]
122+ ]
120123
121124unitTests :: TestTree
122−unitTests =
123− testGroup
124− "Unit tests"
125− [ xmlGeneratorTests,
126− xmlParserTests,
127− bucketNameValidityTests,
128− objectNameValidityTests,
129− parseServerInfoJSONTest,
130− parseHealStatusTest,
131− parseHealStartRespTest,
132− limitedMapConcurrentlyTests
133− ]
125+unitTests = testGroup "Unit tests" [ xmlGeneratorTests, xmlParserTests
126+ , bucketNameValidityTests
127+ , objectNameValidityTests
128+ , parseServerInfoJSONTest
129+ , parseHealStatusTest
130+ , parseHealStartRespTest
131+ , limitedMapConcurrentlyTests
132+ ]
test/cert/private.keydeleted−28
@@ -1,28 +0,0 @@
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.crtdeleted−19
@@ -1,19 +0,0 @@
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-----