git.iko.soy/typeable/minio-hs
on master help sign in
72 files changed+7298−5127
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml+266 .stylish-haskell.yaml−233 .travis.yml−52 CHANGELOG.md+62 CNAME+1 README.md+98−31 Setup.hs−18 docs/API.md+183−182 examples/AssumeRole.hs+47 examples/BucketExists.hs+6−9 examples/CopyObject.hs+16−17 examples/FileUploader.hs+28−28 examples/GetConfig.hs+7−7 examples/GetObject.hs+9−14 examples/HeadObject.hs+10−13 examples/Heal.hs+13−10 examples/ListBuckets.hs+5−7 examples/ListIncompleteUploads.hs+19−21 examples/ListObjects.hs+15−17 examples/MakeBucket.hs+8−11 examples/PresignedGetObject.hs+37−33 examples/PresignedPostPolicy.hs+44−41 examples/PresignedPutObject.hs+22−23 examples/PutObject.hs+13−16 examples/README.md+17 examples/RemoveBucket.hs+5−10 examples/RemoveIncompleteUpload.hs+9−12 examples/RemoveObject.hs+8−9 examples/SelectObject.hs+25−28 examples/ServerInfo.hs+8−8 examples/ServiceSendRestart.hs+7−8 examples/ServiceSendStop.hs+7−8 examples/ServiceStatus.hs+8−8 examples/SetConfig.hs+5−6 minio-hs.cabal+249−170 src/Lib/Prelude.hs+33−11 src/Network/Minio.hs+254−218 src/Network/Minio/API.hs+259−159 src/Network/Minio/APICommon.hs+36−24 src/Network/Minio/AdminAPI.hs+543−391 src/Network/Minio/CopyObject.hs+48−34 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+730−557 src/Network/Minio/Data/ByteString.hs+19−18 src/Network/Minio/Data/Crypto.hs+32−33 src/Network/Minio/Data/Time.hs+17−13 src/Network/Minio/Errors.hs+59−52 src/Network/Minio/JsonParser.hs+23−16 src/Network/Minio/ListOps.hs+98−38 src/Network/Minio/PresignedOperations.hs+215−152 src/Network/Minio/PutObject.hs+85−69 src/Network/Minio/S3API.hs+486−314 src/Network/Minio/SelectAPI.hs+233−241 src/Network/Minio/Sign/V4.hs+432−329 src/Network/Minio/Utils.hs+150−123 src/Network/Minio/XmlCommon.hs+65 src/Network/Minio/XmlGenerator.hs+165−99 src/Network/Minio/XmlParser.hs+107−152 stack.yaml+4−4 stack.yaml.lock+19 test/LiveServer.hs+807−461 test/Network/Minio/API/Test.hs+75−61 test/Network/Minio/JsonParser/Test.hs+26−27 test/Network/Minio/TestHelpers.hs+8−8 test/Network/Minio/Utils/Test.hs+15−17 test/Network/Minio/XmlGenerator/Test.hs+137−98 test/Network/Minio/XmlParser/Test.hs+298−276 test/Spec.hs+83−82 test/cert/private.key+28 test/cert/public.crt+19
.github/workflows/ci.ymlnew file+266
@@ -0,0 +1,266 @@
1+name: CI
2+
3+# Trigger the workflow on push or pull request, but only for the master branch
4+on:
5+ pull_request:
6+ branches: [master]
7+ push:
8+ branches: [master]
9+
10+# This ensures that previous jobs for the PR are canceled when the PR is
11+# updated.
12+concurrency:
13+ group: ${{ github.workflow }}-${{ github.head_ref }}
14+ cancel-in-progress: true
15+
16+# Env vars for tests
17+env:
18+ MINIO_ACCESS_KEY: minio
19+ MINIO_SECRET_KEY: minio123
20+ MINIO_LOCAL: 1
21+
22+jobs:
23+ ormolu:
24+ runs-on: ubuntu-latest
25+ steps:
26+ - uses: actions/checkout@v4
27+ - uses: haskell-actions/run-ormolu@v15
28+ with:
29+ version: "0.5.0.1"
30+
31+ hlint:
32+ runs-on: ubuntu-latest
33+ steps:
34+ - uses: actions/checkout@v4
35+
36+ - name: "Set up HLint"
37+ uses: haskell-actions/hlint-setup@v2
38+ with:
39+ version: "3.5"
40+
41+ - name: "Run HLint"
42+ uses: haskell-actions/hlint-run@v2
43+ with:
44+ path: '["src/", "test/", "examples"]'
45+ fail-on: warning
46+
47+ cabal:
48+ name: ${{ matrix.os }} / ghc-${{ matrix.ghc }} / cabal-${{ matrix.cabal }}
49+ runs-on: ${{ matrix.os }}
50+ needs: ormolu
51+ strategy:
52+ matrix:
53+ os: [ubuntu-latest, windows-latest, macos-latest]
54+ cabal: ["3.8", "latest"]
55+ ghc:
56+ - "9.8"
57+ - "9.6"
58+ - "9.4"
59+ - "9.2"
60+ - "9.0"
61+ - "8.10"
62+ exclude:
63+ # macos llvm issue for versions less than 9.2
64+ - os: macos-latest
65+ ghc: "8.10"
66+ - os: macos-latest
67+ ghc: "9.0"
68+ # Cabal 3.8 supports GHC < 9.6
69+ - cabal: "3.8"
70+ ghc: "9.6"
71+ - cabal: "3.8"
72+ ghc: "9.8"
73+
74+ steps:
75+ - uses: actions/checkout@v4
76+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
77+
78+ - uses: haskell-actions/setup@v2
79+ id: setup
80+ name: Setup Haskell
81+ with:
82+ ghc-version: ${{ matrix.ghc }}
83+ cabal-version: ${{ matrix.cabal }}
84+ cabal-update: true
85+
86+ - name: Configure
87+ run: |
88+ cabal configure --enable-tests --enable-benchmarks --test-show-details=direct -fexamples -fdev -flive-test
89+ cabal build all --dry-run
90+ # The last step generates dist-newstyle/cache/plan.json for the cache key.
91+
92+ - name: Restore cached dependencies
93+ uses: actions/cache/restore@v4
94+ id: cache
95+ env:
96+ key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
97+ with:
98+ path: ${{ steps.setup.outputs.cabal-store }}
99+ key: ${{ env.key }}-plan-${{ hashFiles('**/plan.json') }}
100+ restore-keys: ${{ env.key }}-
101+
102+ - name: Install dependencies
103+ # If we had an exact cache hit, the dependencies will be up to date.
104+ if: steps.cache.outputs.cache-hit != 'true'
105+ run: cabal build all --only-dependencies
106+
107+ # Cache dependencies already here, so that we do not have to rebuild them should the subsequent steps fail.
108+ - name: Save cached dependencies
109+ uses: actions/cache/save@v4
110+ # If we had an exact cache hit, trying to save the cache would error because of key clash.
111+ if: steps.cache.outputs.cache-hit != 'true'
112+ with:
113+ path: ${{ steps.setup.outputs.cabal-store }}
114+ key: ${{ steps.cache.outputs.cache-primary-key }}
115+
116+ - name: Build
117+ run: |
118+ cabal build all
119+
120+ - name: Setup TLS certs for MinIO for testing (Linux)
121+ if: matrix.os == 'ubuntu-latest'
122+ run: |
123+ mkdir -p /tmp/minio /tmp/minio-config/certs
124+ cp test/cert/* /tmp/minio-config/certs/
125+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
126+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
127+ sudo update-ca-certificates
128+
129+ ## Currently disable TLS setup for MacOS due to issues in trusting it on MacOS.
130+ - name: Setup TLS certs for MinIO for testing (MacOS)
131+ if: matrix.os == 'macos-latest'
132+ run: |
133+ mkdir -p /tmp/minio /tmp/minio-config/certs
134+ cp test/cert/* /tmp/minio-config/certs/
135+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
136+ # sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
137+
138+ - name: Setup MinIO for testing (Windows)
139+ if: matrix.os == 'windows-latest'
140+ run: |
141+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
142+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
143+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
144+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
145+
146+ - name: Test (Linux)
147+ if: matrix.os == 'ubuntu-latest'
148+ env:
149+ MINIO_SECURE: 1
150+ run: |
151+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
152+ ghc --version
153+ cabal --version
154+ cabal test all
155+
156+ - name: Test (MacOS)
157+ if: matrix.os == 'macos-latest'
158+ # # Leave MINIO_SECURE unset to disable TLS in tests.
159+ # env:
160+ # MINIO_SECURE: 1
161+ run: |
162+ /tmp/minio/minio server --quiet data1 data2 data3 data4 2>&1 > minio.log &
163+ ghc --version
164+ cabal --version
165+ cabal test all
166+
167+ - name: Test (Windows)
168+ if: matrix.os == 'windows-latest'
169+ env:
170+ MINIO_SECURE: 1
171+ run: |
172+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
173+ ghc --version
174+ cabal --version
175+ cabal test all
176+
177+ stack:
178+ name: stack / ghc ${{ matrix.ghc }}
179+ runs-on: ${{ matrix.os }}
180+ env:
181+ MINIO_SECURE: 1
182+ strategy:
183+ matrix:
184+ ghc:
185+ - "8.10.7"
186+ - "9.0.2"
187+ - "9.2.8"
188+ - "9.4.8"
189+ - "9.6.5"
190+ - "9.8.2"
191+ os: [ubuntu-latest]
192+
193+ steps:
194+ - uses: actions/checkout@v4
195+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
196+
197+ - uses: haskell-actions/setup@v2
198+ with:
199+ ghc-version: ${{ matrix.ghc }}
200+ enable-stack: true
201+ stack-version: "latest"
202+
203+ - uses: actions/cache@v4
204+ name: Cache ~/.stack
205+ with:
206+ path: ~/.stack
207+ key: ${{ runner.os }}-stack-global-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}
208+ restore-keys: |
209+ ${{ runner.os }}-stack-global-
210+ - uses: actions/cache@v4
211+ name: Cache .stack-work
212+ with:
213+ path: .stack-work
214+ key: ${{ runner.os }}-stack-work-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}-${{ hashFiles('**/*.hs') }}
215+ restore-keys: |
216+ ${{ runner.os }}-stack-work-
217+
218+ - name: Install dependencies
219+ run: |
220+ stack --version
221+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
222+
223+ - name: Build
224+ run: |
225+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --flag minio-hs:examples --flag minio-hs:live-test --flag minio-hs:dev
226+
227+ - name: Setup MinIO for testing (Linux)
228+ if: matrix.os == 'ubuntu-latest'
229+ run: |
230+ mkdir -p /tmp/minio /tmp/minio-config/certs
231+ cp test/cert/* /tmp/minio-config/certs/
232+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
233+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
234+ sudo update-ca-certificates
235+
236+ - name: Setup MinIO for testing (MacOS)
237+ if: matrix.os == 'macos-latest'
238+ run: |
239+ mkdir -p /tmp/minio /tmp/minio-config/certs
240+ cp test/cert/* /tmp/minio-config/certs/
241+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
242+ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
243+
244+ - name: Setup MinIO for testing (Windows)
245+ if: matrix.os == 'windows-latest'
246+ run: |
247+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
248+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
249+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
250+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
251+
252+ - name: Test (Non-Windows)
253+ if: matrix.os != 'windows-latest'
254+ run: |
255+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
256+ ghc --version
257+ stack --version
258+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
259+
260+ - name: Test (Windows)
261+ if: matrix.os == 'windows-latest'
262+ run: |
263+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
264+ ghc --version
265+ cabal --version
266+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
.stylish-haskell.yamldeleted−233
@@ -1,233 +0,0 @@
1−# stylish-haskell configuration file
2−# ==================================
3−
4−# The stylish-haskell tool is mainly configured by specifying steps. These steps
5−# are a list, so they have an order, and one specific step may appear more than
6−# once (if needed). Each file is processed by these steps in the given order.
7−steps:
8− # Convert some ASCII sequences to their Unicode equivalents. This is disabled
9− # by default.
10− # - unicode_syntax:
11− # # In order to make this work, we also need to insert the UnicodeSyntax
12− # # language pragma. If this flag is set to true, we insert it when it's
13− # # not already present. You may want to disable it if you configure
14− # # language extensions using some other method than pragmas. Default:
15− # # true.
16− # add_language_pragma: true
17−
18− # Align the right hand side of some elements. This is quite conservative
19− # and only applies to statements where each element occupies a single
20− # line.
21− - simple_align:
22− cases: true
23− top_level_patterns: true
24− records: true
25−
26− # Import cleanup
27− - imports:
28− # There are different ways we can align names and lists.
29− #
30− # - global: Align the import names and import list throughout the entire
31− # file.
32− #
33− # - file: Like global, but don't add padding when there are no qualified
34− # imports in the file.
35− #
36− # - group: Only align the imports per group (a group is formed by adjacent
37− # import lines).
38− #
39− # - none: Do not perform any alignment.
40− #
41− # Default: global.
42− align: global
43−
44− # The following options affect only import list alignment.
45− #
46− # List align has following options:
47− #
48− # - after_alias: Import list is aligned with end of import including
49− # 'as' and 'hiding' keywords.
50− #
51− # > import qualified Data.List as List (concat, foldl, foldr, head,
52− # > init, last, length)
53− #
54− # - with_alias: Import list is aligned with start of alias or hiding.
55− #
56− # > import qualified Data.List as List (concat, foldl, foldr, head,
57− # > init, last, length)
58− #
59− # - new_line: Import list starts always on new line.
60− #
61− # > import qualified Data.List as List
62− # > (concat, foldl, foldr, head, init, last, length)
63− #
64− # Default: after_alias
65− list_align: after_alias
66−
67− # Right-pad the module names to align imports in a group:
68− #
69− # - true: a little more readable
70− #
71− # > import qualified Data.List as List (concat, foldl, foldr,
72− # > init, last, length)
73− # > import qualified Data.List.Extra as List (concat, foldl, foldr,
74− # > init, last, length)
75− #
76− # - false: diff-safe
77− #
78− # > import qualified Data.List as List (concat, foldl, foldr, init,
79− # > last, length)
80− # > import qualified Data.List.Extra as List (concat, foldl, foldr,
81− # > init, last, length)
82− #
83− # Default: true
84− pad_module_names: true
85−
86− # Long list align style takes effect when import is too long. This is
87− # determined by 'columns' setting.
88− #
89− # - inline: This option will put as much specs on same line as possible.
90− #
91− # - new_line: Import list will start on new line.
92− #
93− # - new_line_multiline: Import list will start on new line when it's
94− # short enough to fit to single line. Otherwise it'll be multiline.
95− #
96− # - multiline: One line per import list entry.
97− # Type with constructor list acts like single import.
98− #
99− # > import qualified Data.Map as M
100− # > ( empty
101− # > , singleton
102− # > , ...
103− # > , delete
104− # > )
105− #
106− # Default: inline
107− long_list_align: inline
108−
109− # Align empty list (importing instances)
110− #
111− # Empty list align has following options
112− #
113− # - inherit: inherit list_align setting
114− #
115− # - right_after: () is right after the module name:
116− #
117− # > import Vector.Instances ()
118− #
119− # Default: inherit
120− empty_list_align: inherit
121−
122− # List padding determines indentation of import list on lines after import.
123− # This option affects 'long_list_align'.
124− #
125− # - <integer>: constant value
126− #
127− # - module_name: align under start of module name.
128− # Useful for 'file' and 'group' align settings.
129− list_padding: 4
130−
131− # Separate lists option affects formatting of import list for type
132− # or class. The only difference is single space between type and list
133− # of constructors, selectors and class functions.
134− #
135− # - true: There is single space between Foldable type and list of it's
136− # functions.
137− #
138− # > import Data.Foldable (Foldable (fold, foldl, foldMap))
139− #
140− # - false: There is no space between Foldable type and list of it's
141− # functions.
142− #
143− # > import Data.Foldable (Foldable(fold, foldl, foldMap))
144− #
145− # Default: true
146− separate_lists: true
147−
148− # Space surround option affects formatting of import lists on a single
149− # line. The only difference is single space after the initial
150− # parenthesis and a single space before the terminal parenthesis.
151− #
152− # - true: There is single space associated with the enclosing
153− # parenthesis.
154− #
155− # > import Data.Foo ( foo )
156− #
157− # - false: There is no space associated with the enclosing parenthesis
158− #
159− # > import Data.Foo (foo)
160− #
161− # Default: false
162− space_surround: false
163−
164− # Language pragmas
165− - language_pragmas:
166− # We can generate different styles of language pragma lists.
167− #
168− # - vertical: Vertical-spaced language pragmas, one per line.
169− #
170− # - compact: A more compact style.
171− #
172− # - compact_line: Similar to compact, but wrap each line with
173− # `{-#LANGUAGE #-}'.
174− #
175− # Default: vertical.
176− style: vertical
177−
178− # Align affects alignment of closing pragma brackets.
179− #
180− # - true: Brackets are aligned in same column.
181− #
182− # - false: Brackets are not aligned together. There is only one space
183− # between actual import and closing bracket.
184− #
185− # Default: true
186− align: true
187−
188− # stylish-haskell can detect redundancy of some language pragmas. If this
189− # is set to true, it will remove those redundant pragmas. Default: true.
190− remove_redundant: true
191−
192− # Replace tabs by spaces. This is disabled by default.
193− # - tabs:
194− # # Number of spaces to use for each tab. Default: 8, as specified by the
195− # # Haskell report.
196− # spaces: 8
197−
198− # Remove trailing whitespace
199− - trailing_whitespace: {}
200−
201−# A common setting is the number of columns (parts of) code will be wrapped
202−# to. Different steps take this into account. Default: 80.
203−columns: 80
204−
205−# By default, line endings are converted according to the OS. You can override
206−# preferred format here.
207−#
208−# - native: Native newline format. CRLF on Windows, LF on other OSes.
209−#
210−# - lf: Convert to LF ("\n").
211−#
212−# - crlf: Convert to CRLF ("\r\n").
213−#
214−# Default: native.
215−newline: native
216−
217−# Sometimes, language extensions are specified in a cabal file or from the
218−# command line instead of using language pragmas in the file. stylish-haskell
219−# needs to be aware of these, so it can parse the file correctly.
220−#
221−# No language extensions are enabled by default.
222−language_extensions:
223− - BangPatterns
224− - FlexibleContexts
225− - FlexibleInstances
226− - MultiParamTypeClasses
227− - MultiWayIf
228− - NoImplicitPrelude
229− - OverloadedStrings
230− - RankNTypes
231− - ScopedTypeVariables
232− - TupleSections
233− - TypeFamilies
.travis.ymldeleted−52
@@ -1,52 +0,0 @@
1−sudo: true
2−language: haskell
3−
4−git:
5− depth: 5
6−
7−cabal: "2.4"
8−
9−cache:
10− directories:
11− - "$HOME/.cabal/store"
12− - "$HOME/.stack"
13− - "$TRAVIS_BUILD_DIR/.stack-work"
14−
15−matrix:
16− include:
17−
18− # Cabal
19− - ghc: 8.2.2
20− - ghc: 8.4.4
21− - ghc: 8.6.5
22−
23− # Stack
24− - ghc: 8.6.5
25− env: STACK_YAML="$TRAVIS_BUILD_DIR/stack.yaml"
26−
27−install:
28− - |
29− if [ -z "$STACK_YAML" ]; then
30− ghc --version
31− cabal --version
32− cabal new-update
33− cabal new-build --enable-tests --enable-benchmarks
34− else
35− # install stack
36− curl -sSL https://get.haskellstack.org/ | sh
37−
38− # build project with stack
39− stack --version
40− stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks
41− fi
42−
43−script:
44− - |
45− if [ -z "$STACK_YAML" ]; then
46− cabal new-test --enable-tests
47− else
48− stack test --system-ghc
49− fi
50−
51−notifications:
52− email: false
CHANGELOG.md+62
@@ -1,6 +1,68 @@
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+
466## Version 1.3.1
567
668* Add TLS helpers to check if server uses TLS, and to disable
CNAMEnew file+1
@@ -0,0 +1 @@
1+minio-hs.min.io
README.md+98−31
@@ -1,53 +1,65 @@
1−# MinIO Client SDK for Haskell [![Build Status](https://travis-ci.org/minio/minio-hs.svg?branch=master)](https://travis-ci.org/minio/minio-hs)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
1+# MinIO Haskell Client SDK for Amazon S3 Compatible Cloud Storage [![CI](https://github.com/minio/minio-hs/actions/workflows/ci.yml/badge.svg)](https://github.com/minio/minio-hs/actions/workflows/ci.yml)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
22
3−The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and Amazon S3 compatible object storage server.
3+The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
44
5−## Minimum Requirements
6−
7−- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
5+This guide assumes that you have a working [Haskell development environment](https://www.haskell.org/downloads/).
86
97## Installation
108
11−```sh
12−git clone https://github.com/minio/minio-hs.git
9+### Add to your project
1310
14−cd minio-hs/
11+Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
1512
16−stack install
17−```
13+### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
1814
19−Tests can be run with:
15+#### For a cabal based environment
2016
21−```sh
17+Download the library source and change to the extracted directory:
2218
23−stack test
19+``` sh
20+$ cabal get minio-hs
21+$ cd minio-hs-1.6.0/ # directory name could be different
22+```
2423
24+Then load the `ghci` REPL environment with the library and browse the available APIs:
25+
26+``` sh
27+$ cabal repl
28+ghci> :browse Network.Minio
2529```
2630
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).
31+#### For a stack based environment
3332
34−Documentation can be locally built with:
33+From your home folder or any non-haskell project directory, just run:
3534
3635```sh
36+stack install minio-hs
37+```
3738
38−stack haddock
39+Then start an interpreter session and browse the available APIs with:
3940
41+```sh
42+$ stack ghci
43+> :browse Network.Minio
4044```
4145
42−## Quick-Start Example - File Uploader
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.
4355
4456### FileUploader.hs
4557``` haskell
4658#!/usr/bin/env stack
47−-- stack --resolver lts-11.1 runghc --package minio-hs --package optparse-applicative --package filepath
59+-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
4860
4961--
50−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
62+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
5163--
5264-- Licensed under the Apache License, Version 2.0 (the "License");
5365-- you may not use this file except in compliance with the License.
@@ -76,7 +88,7 @@ import UnliftIO (throwIO, try)
7688import Prelude
7789
7890-- | The following example uses minio's play server at
79−-- https://play.min.io:9000. The endpoint and associated
91+-- https://play.min.io. The endpoint and associated
8092-- credentials are provided via the libary constant,
8193--
8294-- > minioPlayCI :: ConnectInfo
@@ -106,16 +118,19 @@ main = do
106118 res <- runMinio minioPlayCI $ do
107119 -- Make a bucket; catch bucket already exists exception if thrown.
108120 bErr <- try $ makeBucket bucket Nothing
121+
122+ -- If the bucket already exists, we would get a specific
123+ -- `ServiceErr` exception thrown.
109124 case bErr of
110− Left (MErrService BucketAlreadyOwnedByYou) -> return ()
111− Left e -> throwIO e
112− Right _ -> return ()
125+ Left BucketAlreadyOwnedByYou -> return ()
126+ Left e -> throwIO e
127+ Right _ -> return ()
113128
114− -- Upload filepath to bucket; object is derived from filepath.
115− fPutObject bucket object filepath def
129+ -- Upload filepath to bucket; object name is derived from filepath.
130+ fPutObject bucket object filepath defaultPutObjectOptions
116131
117132 case res of
118− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
133+ Left e -> putStrLn $ "file upload failed due to " ++ show e
119134 Right () -> putStrLn "file upload succeeded."
120135```
121136
@@ -129,3 +144,55 @@ main = do
129144## Contribute
130145
131146[Contributors Guide](https://github.com/minio/minio-hs/blob/master/CONTRIBUTING.md)
147+
148+### Development
149+
150+#### Download the source
151+
152+```sh
153+$ git clone https://github.com/minio/minio-hs.git
154+$ cd minio-hs/
155+```
156+
157+#### Build the package:
158+
159+With `cabal`:
160+
161+```sh
162+$ # Configure cabal for development enabling all optional flags defined by the package.
163+$ cabal configure --enable-tests --test-show-details=direct -fexamples -fdev -flive-test
164+$ cabal build
165+```
166+
167+With `stack`:
168+
169+``` sh
170+$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
171+```
172+#### Running tests:
173+
174+A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000` with the credentials `access_key=minio` and `secret_key=minio123`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
175+
176+With `cabal`:
177+
178+```sh
179+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
180+$ cabal test
181+```
182+
183+With `stack`:
184+
185+``` sh
186+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
187+stack test --flag minio-hs:live-test --flag minio-hs:dev
188+```
189+
190+This will run all the test suites.
191+
192+#### Building documentation:
193+
194+```sh
195+$ cabal haddock
196+$ # OR
197+$ stack haddock
198+```
Setup.hsdeleted−18
@@ -1,18 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−import Distribution.Simple
18−main = defaultMain
docs/API.md+183−182
@@ -20,15 +20,15 @@ awsCI { connectAccesskey = "your-access-key"
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:9000/`.
81+`https://play.min.io/`.
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,26 +233,27 @@ __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 |
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 |
256257
257258__Example__
258259
@@ -265,7 +266,7 @@ import Prelude
265266
266267
267268-- | The following example uses MinIO play server at
268−-- https://play.min.io:9000. The endpoint and associated
269+-- https://play.min.io. The endpoint and associated
269270-- credentials are provided via the libary constant,
270271--
271272-- > minioPlayCI :: ConnectInfo
@@ -294,26 +295,26 @@ __Parameters__
294295In the expression `listObjectsV1 bucketName prefix recursive` the
295296arguments are:
296297
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 '/'. |
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 '/'. |
302303
303304__Return Value__
304305
305−|Return type |Description |
306−|:---|:---|
307−| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
306+| Return type | Description |
307+|:------------------------------------|:------------------------------------------------------------------------|
308+| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
308309
309310__ObjectInfo record type__
310311
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 |
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 |
317318
318319__Example__
319320
@@ -326,7 +327,7 @@ import Prelude
326327
327328
328329-- | The following example uses MinIO play server at
329−-- https://play.min.io:9000. The endpoint and associated
330+-- https://play.min.io. The endpoint and associated
330331-- credentials are provided via the libary constant,
331332--
332333-- > minioPlayCI :: ConnectInfo
@@ -354,11 +355,11 @@ __Parameters__
354355In the expression `listIncompleteUploads bucketName prefix recursive`
355356the parameters are:
356357
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 '/'. |
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 '/'. |
362363
363364__Return Value__
364365
@@ -368,11 +369,11 @@ __Return Value__
368369
369370__UploadInfo record type__
370371
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 |
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 |
376377
377378__Example__
378379
@@ -384,7 +385,7 @@ import Conduit
384385import Prelude
385386
386387-- | The following example uses MinIO play server at
387−-- https://play.min.io:9000. The endpoint and associated
388+-- https://play.min.io. The endpoint and associated
388389-- credentials are provided via the libary constant,
389390--
390391-- > minioPlayCI :: ConnectInfo
@@ -415,29 +416,29 @@ __Parameters__
415416In the expression `getObject bucketName objectName opts` the parameters
416417are:
417418
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 |
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 |
423424
424425__GetObjectOptions record type__
425426
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 |
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 |
433434
434435__Return Value__
435436
436437The return value can be incrementally read to process the contents of
437438the object.
438−|Return type |Description |
439−|:---|:---|
440−| _Minio (C.ConduitM () ByteString Minio ())_ | A Conduit source of `ByteString` values. |
439+| Return type | Description |
440+|:--------------------------------------------|:-----------------------------------------|
441+| _Minio (C.ConduitM () ByteString Minio ())_ | A Conduit source of `ByteString` values. |
441442
442443__Example__
443444
@@ -451,7 +452,7 @@ import qualified Data.Conduit.Binary as CB
451452import Prelude
452453
453454-- | The following example uses MinIO play server at
454−-- https://play.min.io:9000. The endpoint and associated
455+-- https://play.min.io. The endpoint and associated
455456-- credentials are provided via the libary constant,
456457--
457458-- > minioPlayCI :: ConnectInfo
@@ -482,13 +483,13 @@ __Parameters__
482483In the expression `putObject bucketName objectName inputSrc` the parameters
483484are:
484485
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 |
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 |
492493
493494__Example__
494495
@@ -501,7 +502,7 @@ import qualified Data.Conduit.Combinators as CC
501502import Prelude
502503
503504-- | The following example uses MinIO play server at
504−-- https://play.min.io:9000. The endpoint and associated
505+-- https://play.min.io. The endpoint and associated
505506-- credentials are provided via the libary constant,
506507--
507508-- > minioPlayCI :: ConnectInfo
@@ -533,23 +534,23 @@ __Parameters__
533534In the expression `fGetObject bucketName objectName inputFile` the parameters
534535are:
535536
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 |
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 |
542543
543544
544545__GetObjectOptions record type__
545546
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 |
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 |
553554
554555``` haskell
555556
@@ -561,7 +562,7 @@ import Data.Conduit.Binary (sinkLbs)
561562import Prelude
562563
563564-- | The following example uses MinIO play server at
564−-- https://play.min.io:9000. The endpoint and associated
565+-- https://play.min.io. The endpoint and associated
565566-- credentials are provided via the libary constant,
566567--
567568-- > minioPlayCI :: ConnectInfo
@@ -592,11 +593,11 @@ __Parameters__
592593In the expression `fPutObject bucketName objectName inputFile` the parameters
593594are:
594595
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 |
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 |
600601
601602__Example__
602603
@@ -629,28 +630,28 @@ __Parameters__
629630In the expression `copyObject dstInfo srcInfo` the parameters
630631are:
631632
632−|Param |Type |Description |
633−|:---|:---| :---|
633+| Param | Type | Description |
634+|:----------|:------------------|:----------------------------------------------------------|
634635| `dstInfo` | _DestinationInfo_ | A value representing properties of the destination object |
635−| `srcInfo` | _SourceInfo_ | A value representing properties of the source object |
636+| `srcInfo` | _SourceInfo_ | A value representing properties of the source object |
636637
637638
638639__SourceInfo record type__
639640
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 |
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 |
649650
650651__Destination record type__
651652
652−|Field |Type |Description |
653−|:---|:---| :---|
653+| Field | Type | Description |
654+|:------------|:---------|:-----------------------------------------------------|
654655| `dstBucket` | `Bucket` | Name of destination bucket in server-side copyObject |
655656| `dstObject` | `Object` | Name of destination object in server-side copyObject |
656657
@@ -684,10 +685,10 @@ __Parameters__
684685In the expression `removeObject bucketName objectName` the parameters
685686are:
686687
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 |
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 |
691692
692693__Example__
693694
@@ -718,10 +719,10 @@ __Parameters__
718719In the expression `removeIncompleteUpload bucketName objectName` the parameters
719720are:
720721
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 |
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 |
725726
726727__Example__
727728
@@ -752,11 +753,11 @@ __Parameters__
752753In the expression `selectObjectContent bucketName objectName selReq`
753754the parameters are:
754755
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 |
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 |
760761
761762__SelectRequest record__
762763
@@ -804,9 +805,9 @@ __Parameters__
804805
805806In the expression `bucketExists bucketName` the parameters are:
806807
807−|Param |Type |Description |
808−|:---|:---| :---|
809−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
808+| Param | Type | Description |
809+|:-------------|:----------------------------|:-------------------|
810+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
810811
811812
812813## 4. Presigned operations
@@ -828,22 +829,22 @@ __Parameters__
828829In the expression `presignedGetObjectUrl bucketName objectName expiry queryParams headers`
829830the parameters are:
830831
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 |
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 |
838839
839840__Return Value__
840841
841842Returns the generated URL - it will include authentication
842843information.
843844
844−|Return type |Description |
845−|:---|:---|
846−| _ByteString_ | Generated presigned URL |
845+| Return type | Description |
846+|:-------------|:------------------------|
847+| _ByteString_ | Generated presigned URL |
847848
848849__Example__
849850
@@ -886,21 +887,21 @@ __Parameters__
886887In the expression `presignedPutObjectUrl bucketName objectName expiry headers`
887888the parameters are:
888889
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 |
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 |
895896
896897__Return Value__
897898
898899Returns the generated URL - it will include authentication
899900information.
900901
901−|Return type |Description |
902−|:---|:---|
903−| _ByteString_ | Generated presigned URL |
902+| Return type | Description |
903+|:-------------|:------------------------|
904+| _ByteString_ | Generated presigned URL |
904905
905906__Example__
906907
@@ -928,7 +929,7 @@ main = do
928929```
929930
930931<a name="presignedPostPolicy"></a>
931−### presignedPostPolicy :: PostPolicy -> Minio (ByteString, Map.Map Text ByteString)
932+### presignedPostPolicy :: PostPolicy -> Minio (ByteString, HashMap Text ByteString)
932933
933934Generate a presigned URL and POST policy to upload files via a POST
934935request. This is intended for browser uploads and generates form data
@@ -940,10 +941,10 @@ The `PostPolicy` argument is created using the `newPostPolicy` function:
940941
941942In the expression `newPostPolicy expirationTime conditions` the parameters are:
942943
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 |
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 |
947948
948949The policy conditions are created using various helper functions -
949950please refer to the Haddocks for details.
@@ -965,7 +966,7 @@ import Network.Minio
965966
966967import qualified Data.ByteString as B
967968import qualified Data.ByteString.Char8 as Char8
968−import qualified Data.Map.Strict as Map
969+import qualified Data.HashMap.Strict as H
969970import qualified Data.Text.Encoding as Enc
970971import qualified Data.Time as Time
971972
@@ -1005,7 +1006,7 @@ main = do
10051006 let
10061007 formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
10071008 "'", v, "'"]
1008− formOptions = B.intercalate " " $ map formFn $ Map.toList formData
1009+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
10091010
10101011
10111012 return $ B.intercalate " " $
examples/AssumeRole.hsnew file+47
@@ -0,0 +1,47 @@
1+--
2+-- MinIO Haskell SDK, (C) 2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE OverloadedStrings #-}
17+
18+import Control.Monad.IO.Class (liftIO)
19+import Network.Minio
20+import Prelude
21+
22+main :: IO ()
23+main = do
24+ -- Use play credentials for example.
25+ let assumeRole =
26+ STSAssumeRole
27+ ( CredentialValue
28+ "Q3AM3UQ867SPQQA43P2F"
29+ "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
30+ Nothing
31+ )
32+ $ defaultSTSAssumeRoleOptions
33+ { saroLocation = Just "us-east-1",
34+ saroEndpoint = Just "https://play.min.io:9000"
35+ }
36+
37+ -- Retrieve temporary credentials and print them.
38+ cv <- requestSTSCredential assumeRole
39+ print $ "Temporary credentials" ++ show (credentialValueText $ fst cv)
40+ print $ "Expiry" ++ show (snd cv)
41+
42+ -- Configure 'ConnectInfo' to request temporary credentials on demand.
43+ ci <- setSTSCredential assumeRole "https://play.min.io"
44+ res <- runMinio ci $ do
45+ buckets <- listBuckets
46+ liftIO $ print $ "Top 5 buckets: " ++ show (take 5 buckets)
47+ print res
examples/BucketExists.hs+6−9
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,20 +16,17 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.IO.Class (liftIO)
24−import Prelude
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
23+import Prelude
2524
2625-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
3532 let bucket = "missingbucket"
@@ -39,5 +36,5 @@ main = do
3936 liftIO $ putStrLn $ "Does " ++ show bucket ++ " exist? - " ++ show foundBucket
4037
4138 case res1 of
42− Left e -> putStrLn $ "bucketExists failed." ++ show e
39+ Left e -> putStrLn $ "bucketExists failed." ++ show e
4340 Right () -> return ()
examples/CopyObject.hs+16−17
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,41 +16,40 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.Catch (catchIf)
24−import Prelude
21+import Network.Minio
22+import UnliftIO.Exception (catch, throwIO)
2523
2624-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
2826-- credentials are provided via the libary constant,
2927--
3028-- > minioPlayCI :: ConnectInfo
31−--
32−
33−ignoreMinioErr :: ServiceErr -> Minio ()
34−ignoreMinioErr = return . const ()
35−
3629main :: IO ()
3730main = do
38− let
39− bucket = "test"
31+ let bucket = "test"
4032 object = "obj"
4133 objectCopy = "obj-copy"
4234 localFile = "/etc/lsb-release"
4335
4436 res1 <- runMinio minioPlayCI $ do
4537 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
46− catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
38+ catch
39+ (makeBucket bucket Nothing)
40+ ( \e -> case e of
41+ BucketAlreadyOwnedByYou -> return ()
42+ _ -> throwIO e
43+ )
4744
4845 -- 2. Upload a file to bucket/object.
49− fPutObject bucket object localFile
46+ fPutObject bucket object localFile defaultPutObjectOptions
5047
5148 -- 3. Copy bucket/object to bucket/objectCopy.
52− copyObject def {dstBucket = bucket, dstObject = objectCopy} def { srcBucket = bucket , srcObject = object }
49+ copyObject
50+ defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
51+ defaultSourceInfo {srcBucket = bucket, srcObject = object}
5352
5453 case res1 of
55− Left e -> putStrLn $ "copyObject failed." ++ show e
54+ Left e -> putStrLn $ "copyObject failed." ++ show e
5655 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+28−28
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs --package optparse-applicative --package filepath
2+-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
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−
20−
21−{-# LANGUAGE OverloadedStrings #-}
19+{-# LANGUAGE OverloadedStrings #-}
2220{-# LANGUAGE ScopedTypeVariables #-}
23−import Network.Minio
2421
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
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
3228
3329-- | The following example uses minio's play server at
34−-- https://play.min.io:9000. The endpoint and associated
30+-- https://play.min.io. The endpoint and associated
3531-- credentials are provided via the libary constant,
3632--
3733-- > minioPlayCI :: ConnectInfo
38−--
3934
4035-- optparse-applicative package based command-line parsing.
4136fileNameArgs :: Parser FilePath
42−fileNameArgs = strArgument
43− (metavar "FILENAME"
44− <> help "Name of file to upload to AWS S3 or a MinIO server")
37+fileNameArgs =
38+ strArgument
39+ ( metavar "FILENAME"
40+ <> help "Name of file to upload to AWS S3 or a MinIO server"
41+ )
4542
46−cmdParser = info
47− (helper <*> fileNameArgs)
48− (fullDesc
49− <> progDesc "FileUploader"
50− <> header
51− "FileUploader - a simple file-uploader program using minio-hs")
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+ )
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 (MErrService BucketAlreadyOwnedByYou) -> return ()
66− Left e -> throwIO e
67− Right _ -> return ()
65+ Left 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 def
70+ fPutObject bucket object filepath defaultPutObjectOptions
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-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 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−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
23−
24−import Prelude
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
2523
2624main :: IO ()
2725main = do
28− res <- runMinio def $
26+ res <-
27+ runMinio
28+ minioPlayCI
2929 getConfig
3030 print res
examples/GetObject.hs+9−14
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,31 +16,26 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import qualified Data.Conduit as C
21+import qualified Data.Conduit as C
2422import qualified Data.Conduit.Binary as CB
25−
26−import Prelude
23+import Network.Minio
24+import Prelude
2725
2826-- | The following example uses minio's play server at
29−-- https://play.min.io:9000. The endpoint and associated
27+-- https://play.min.io. The endpoint and associated
3028-- credentials are provided via the libary constant,
3129--
3230-- > minioPlayCI :: ConnectInfo
33−--
34−
3531main :: IO ()
3632main = do
37− let
38− bucket = "my-bucket"
33+ let bucket = "my-bucket"
3934 object = "my-object"
4035 res <- runMinio minioPlayCI $ do
41− src <- getObject bucket object def
42− C.connect src $ CB.sinkFileCautious "/tmp/my-object"
36+ src <- getObject bucket object defaultGetObjectOptions
37+ C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
4338
4439 case res of
45− Left e -> putStrLn $ "getObject failed." ++ (show e)
40+ Left e -> putStrLn $ "getObject failed." ++ show e
4641 Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs+10−13
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,28 +16,25 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.S3API
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.S3API
23+import Prelude
2524
2625-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
3733 object = "passwd"
38− res <- runMinio minioPlayCI $
39− headObject bucket object
34+ res <-
35+ runMinio minioPlayCI $
36+ headObject bucket object []
4037
4138 case res of
42− Left e -> putStrLn $ "headObject failed." ++ show e
39+ Left e -> putStrLn $ "headObject failed." ++ show e
4340 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
examples/Heal.hs+13−10
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,18 +17,21 @@
1717-- limitations under the License.
1818--
1919
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
23−
24−import Prelude
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
2523
2624main :: IO ()
2725main = do
28− res <- runMinio def $
26+ res <- runMinio minioPlayCI $
2927 do
30− hsr <- startHeal Nothing Nothing HealOpts { hoRecursive = True
31− , hoDryRun = False
32− }
28+ hsr <-
29+ startHeal
30+ Nothing
31+ Nothing
32+ HealOpts
33+ { hoRecursive = True,
34+ hoDryRun = False
35+ }
3336 getHealStatus Nothing Nothing (hsrClientToken hsr)
3437 print res
examples/ListBuckets.hs+5−7
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,19 +16,17 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.IO.Class (liftIO)
24−import Prelude
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
23+import Prelude
2524
2625-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
3230
3331-- This example list buckets that belongs to the user and returns
3432-- region of the first bucket returned.
examples/ListIncompleteUploads.hs+19−21
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,38 +16,36 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Conduit
24−import Prelude
21+import Conduit
22+import Network.Minio
23+import Prelude
2524
2625-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
3733
3834 -- Performs a recursive listing of incomplete uploads under bucket "test"
3935 -- on a local minio server.
40− res <- runMinio minioPlayCI $
41− runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
36+ res <-
37+ runMinio minioPlayCI $
38+ runConduit $
39+ listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
4240 print res
4341
44− {-
45− Following is the output of the above program on a local MinIO server.
42+{-
43+ Following is the output of the above program on a local MinIO server.
4644
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− -}
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+-}
examples/ListObjects.hs+15−17
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,33 +16,31 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−
23−import Conduit
24−import Prelude
2520
21+import Conduit
22+import Network.Minio
23+import Prelude
2624
2725-- | The following example uses minio's play server at
28−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2927-- credentials are provided via the libary constant,
3028--
3129-- > minioPlayCI :: ConnectInfo
32−--
33−
3430main :: IO ()
3531main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
3833
3934 -- Performs a recursive listing of all objects under bucket "test"
4035 -- on play.min.io.
41− res <- runMinio minioPlayCI $
42− runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
36+ res <-
37+ runMinio minioPlayCI $
38+ runConduit $
39+ listObjects bucket Nothing True .| mapM_C (liftIO . print)
4340 print res
44− {-
45− Following is the output of the above program on a local MinIO server.
4641
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− -}
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+-}
examples/MakeBucket.hs+8−11
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,24 +16,21 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
20−
2119{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
2320
24−import Prelude
21+import Network.Minio
22+import Prelude
2523
2624-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
2826-- credentials are provided via the libary constant,
2927--
3028-- > minioPlayCI :: ConnectInfo
31−--
32−
3329main :: IO ()
3430main = do
3531 let bucket = "my-bucket"
36− res <- runMinio minioPlayCI $
37− -- N B the region provided for makeBucket is optional.
38− makeBucket bucket (Just "us-east-1")
32+ res <-
33+ runMinio minioPlayCI $
34+ -- N B the region provided for makeBucket is optional.
35+ makeBucket bucket (Just "us-east-1")
3936 print res
examples/PresignedGetObject.hs+37−33
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,42 +16,40 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.IO.Class (liftIO)
24−import qualified Data.ByteString.Char8 as B
25−import Data.CaseInsensitive (original)
21+import Control.Monad.IO.Class (liftIO)
22+import qualified Data.ByteString.Char8 as B
23+import Data.CaseInsensitive (original)
2624import qualified Data.Conduit.Combinators as CC
27−import qualified Data.Text.Encoding as E
25+import qualified Data.Text.Encoding as E
26+import Network.Minio
2827
2928-- | The following example uses minio's play server at
30−-- https://play.min.io:9000. The endpoint and associated
29+-- https://play.min.io. The endpoint and associated
3130-- credentials are provided via the libary constant,
3231--
3332-- > minioPlayCI :: ConnectInfo
34−--
35−
3633main :: IO ()
3734main = do
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\"")]
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+ ]
4745
4846 res <- runMinio minioPlayCI $ do
4947 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
50− putObject bucket object (CC.repeat "a") (Just kb15) def
51− liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
48+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
49+ liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
5250
5351 -- Extract Etag of uploaded object
54− oi <- statObject bucket object
52+ oi <- statObject bucket object defaultGetObjectOptions
5553 let etag = oiETag oi
5654
5755 -- Set header to add an if-match constraint - this makes sure
@@ -61,23 +59,29 @@ main = do
6159 -- Generate a URL with 7 days expiry time - note that the headers
6260 -- used above must be added to the request with the signed URL
6361 -- generated.
64− url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)
65− queryParam headers
62+ url <-
63+ presignedGetObjectUrl
64+ "my-bucket"
65+ "my-object"
66+ (7 * 24 * 3600)
67+ queryParam
68+ headers
6669
6770 return (headers, etag, url)
6871
6972 case res of
7073 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
71− Right (headers, etag, url) -> do
72−
74+ Right (headers, _, url) -> do
7375 -- We generate a curl command to demonstrate usage of the signed
7476 -- 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, "'"]]
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, "'"]]
8083
81− putStrLn $ "The following curl command would use the presigned " ++
82− "URL to fetch the object and write it to \"/tmp/myfile\":"
84+ putStrLn $
85+ "The following curl command would use the presigned "
86+ ++ "URL to fetch the object and write it to \"/tmp/myfile\":"
8387 B.putStrLn curlCmd
examples/PresignedPostPolicy.hs+44−41
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,69 +16,72 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import qualified Data.ByteString as B
21+import qualified Data.ByteString as B
2422import qualified Data.ByteString.Char8 as Char8
25−import qualified Data.Map.Strict as Map
26−import qualified Data.Text.Encoding as Enc
27−import qualified Data.Time as Time
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
2827
2928-- | The following example uses minio's play server at
30−-- https://play.min.io:9000. The endpoint and associated
29+-- https://play.min.io. The endpoint and associated
3130-- credentials are provided via the libary constant,
3231--
3332-- > minioPlayCI :: ConnectInfo
34−--
35−
3633main :: IO ()
3734main = do
3835 now <- Time.getCurrentTime
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− ]
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+ ]
6056
6157 case policyE of
62− Left err -> putStrLn $ show err
58+ Left err -> print err
6359 Right policy -> do
6460 res <- runMinio minioPlayCI $ do
6561 (url, formData) <- presignedPostPolicy policy
6662
6763 -- a curl command is output to demonstrate using the generated
6864 -- URL and form-data
69− let
70− formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
71− "'", v, "'"]
72− formOptions = B.intercalate " " $ map formFn $ Map.toList formData
73−
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
7475
75− return $ B.intercalate " " $
76− ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
76+ return $
77+ B.intercalate
78+ " "
79+ ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
7780
7881 case res of
79− Left e -> putStrLn $ "post-policy error: " ++ (show e)
82+ Left e -> putStrLn $ "post-policy error: " ++ show e
8083 Right cmd -> do
81− putStrLn $ "Put a photo at /tmp/photo.jpg and run command:\n"
84+ putStrLn "Put a photo at /tmp/photo.jpg and run command:\n"
8285
8386 -- print the generated curl command
8487 Char8.putStrLn cmd
examples/PresignedPutObject.hs+22−23
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,44 +16,43 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
2321import qualified Data.ByteString.Char8 as B
24−import Data.CaseInsensitive (original)
22+import Data.CaseInsensitive (original)
23+import Network.Minio
2524
2625-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
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")]
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+ ]
4039 res <- runMinio minioPlayCI $ do
41−
4240 -- generate a URL with 7 days expiry time
43− presignedPutObjectUrl "my-bucket" "my-object" (7*24*3600) headers
41+ presignedPutObjectUrl "my-bucket" "my-object" (7 * 24 * 3600) headers
4442
4543 case res of
4644 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
4745 Right url -> do
48−
4946 -- We generate a curl command to demonstrate usage of the signed
5047 -- URL.
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\":"
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\":"
5958 B.putStrLn curlCmd
examples/PutObject.hs+13−16
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,39 +16,36 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
2321import qualified Data.Conduit.Combinators as CC
24−
25−import Prelude
22+import Network.Minio
23+import Prelude
2624
2725-- | The following example uses minio's play server at
28−-- https://play.min.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2927-- credentials are provided via the libary constant,
3028--
3129-- > minioPlayCI :: ConnectInfo
32−--
33−
3430main :: IO ()
3531main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
3833 object = "obj"
3934 localFile = "/etc/lsb-release"
4035 kb15 = 15 * 1024
4136
4237 -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
43− res1 <- runMinio minioPlayCI $
44− putObject bucket object (CC.repeat "a") (Just kb15) def
38+ res1 <-
39+ runMinio minioPlayCI $
40+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
4541 case res1 of
46− Left e -> putStrLn $ "putObject failed." ++ show e
42+ Left e -> putStrLn $ "putObject failed." ++ show e
4743 Right () -> putStrLn "putObject succeeded."
4844
4945 -- Eg 2. Upload a file using fPutObject with default options.
50− res2 <- runMinio minioPlayCI $
51− fPutObject bucket object localFile def
46+ res2 <-
47+ runMinio minioPlayCI $
48+ fPutObject bucket object localFile defaultPutObjectOptions
5249 case res2 of
53− Left e -> putStrLn $ "fPutObject failed." ++ show e
50+ Left e -> putStrLn $ "fPutObject failed." ++ show e
5451 Right () -> putStrLn "fPutObject succeeded."
examples/README.mdnew file+17
@@ -0,0 +1,17 @@
1+# Examples
2+
3+The examples in this directory illustrate usage of various APIs provided by this library. Each file is self-contained and can be run like a script directly.
4+
5+To build the examples, the build flag `examples` needs to be turned on:
6+
7+```sh
8+stack build --flag minio-hs:examples
9+```
10+
11+Now to run and example script [BucketExists.hs](https://github.com/minio/minio-hs/blob/master/examples/BucketExists.hs):
12+
13+```sh
14+stack exec BucketExists
15+```
16+
17+The CI system is configured to build these examples with every change, so they should be current.
examples/RemoveBucket.hs+5−10
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,23 +16,18 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
20−
2119{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
2320
24−import Prelude
21+import Network.Minio
22+import Prelude
2523
2624-- | The following example uses minio's play server at
27−-- https://play.min.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
2826-- credentials are provided via the libary constant,
2927--
3028-- > minioPlayCI :: ConnectInfo
31−--
32−
3329main :: IO ()
3430main = do
35− let
36− bucket = "my-bucket"
31+ let bucket = "my-bucket"
3732 res <- runMinio minioPlayCI $ removeBucket bucket
3833 print res
examples/RemoveIncompleteUpload.hs+9−12
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,27 +16,24 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Prelude
21+import Network.Minio
22+import Prelude
2423
2524-- | The following example uses minio's play server at
26−-- https://play.min.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
2726-- credentials are provided via the libary constant,
2827--
2928-- > minioPlayCI :: ConnectInfo
30−--
31−
3229main :: IO ()
3330main = do
34− let
35− bucket = "mybucket"
36− object = "myobject"
31+ let bucket = "mybucket"
32+ object = "myobject"
3733
38− res <- runMinio minioPlayCI $
39− removeIncompleteUpload bucket object
34+ res <-
35+ runMinio minioPlayCI $
36+ removeIncompleteUpload bucket object
4037
4138 case res of
4239 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/RemoveObject.hs+8−9
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,20 +16,19 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
2120
22−import Network.Minio
23−import Prelude
21+import Network.Minio
22+import Prelude
2423
2524main :: IO ()
2625main = do
27− let
28− bucket = "mybucket"
29− object = "myobject"
26+ let bucket = "mybucket"
27+ object = "myobject"
3028
31− res <- runMinio minioPlayCI $
32− removeObject bucket object
29+ res <-
30+ runMinio minioPlayCI $
31+ removeObject bucket object
3332
3433 case res of
3534 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/SelectObject.hs+25−28
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-13.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2019 MinIO, Inc.
@@ -16,35 +16,32 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# 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
2620
27−import Prelude
21+import qualified Conduit as C
22+import Control.Monad (unless)
23+import Network.Minio
24+import Prelude
2825
2926main :: IO ()
3027main = do
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
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
examples/ServerInfo.hs+8−8
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 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−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
23−
24−import Prelude
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
2523
2624main :: IO ()
2725main = do
28− res <- runMinio def $
29− getServerInfo
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ getServerInfo
3030 print res
examples/ServiceSendRestart.hs+7−8
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,13 @@
1717-- limitations under the License.
1818--
1919
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
23−
24−import Prelude
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
2523
2624main :: IO ()
2725main = do
28− res <- runMinio def $
29− serviceSendAction ServiceActionRestart
26+ res <-
27+ runMinio minioPlayCI $
28+ serviceSendAction ServiceActionRestart
3029 print res
examples/ServiceSendStop.hs+7−8
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -17,14 +17,13 @@
1717-- limitations under the License.
1818--
1919
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
23−
24−import Prelude
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
2523
2624main :: IO ()
2725main = do
28− res <- runMinio def $
29− serviceSendAction ServiceActionStop
26+ res <-
27+ runMinio minioPlayCI $
28+ serviceSendAction ServiceActionStop
3029 print res
examples/ServiceStatus.hs+8−8
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 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−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
23−
24−import Prelude
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
2523
2624main :: IO ()
2725main = do
28− res <- runMinio def $
29− serviceStatus
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ serviceStatus
3030 print res
examples/SetConfig.hs+5−6
@@ -1,5 +1,5 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-11.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
55-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
@@ -16,16 +16,15 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
2524
2625main :: IO ()
2726main = do
28− res <- runMinio def $
27+ res <- runMinio minioPlayCI $
2928 do
3029 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\"}}}}"
3130 setConfig config
minio-hs.cabal+249−170
@@ -1,5 +1,6 @@
1+cabal-version: 2.4
12name: minio-hs
2−version: 1.4.0
3+version: 1.7.0
34synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
45 storage.
56description: The MinIO Haskell client library provides simple APIs to
@@ -13,22 +14,70 @@ maintainer: dev@min.io
1314category: Network, AWS, Object Storage
1415build-type: Simple
1516stability: Experimental
16−extra-source-files:
17+extra-doc-files:
1718 CHANGELOG.md
1819 CONTRIBUTING.md
1920 docs/API.md
20− examples/*.hs
2121 README.md
22+extra-source-files:
23+ examples/*.hs
2224 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
2331
24−cabal-version: >=1.10
32+source-repository head
33+ type: git
34+ location: https://github.com/minio/minio-hs.git
2535
26−library
27− hs-source-dirs: src
36+Flag dev
37+ Description: Build package in development mode
38+ Default: False
39+ Manual: True
40+
41+common base-settings
2842 ghc-options: -Wall
29− exposed-modules: Network.Minio
30− , Network.Minio.AdminAPI
31− , Network.Minio.S3API
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+
3281 other-modules: Lib.Prelude
3382 , Network.Minio.API
3483 , Network.Minio.APICommon
@@ -46,23 +95,31 @@ library
4695 , Network.Minio.Utils
4796 , Network.Minio.XmlGenerator
4897 , Network.Minio.XmlParser
98+ , Network.Minio.XmlCommon
4999 , 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+
50108 build-depends: base >= 4.7 && < 5
51− , protolude >= 0.2 && < 0.3
52− , aeson >= 1.2
109+ , relude >= 0.7 && < 2
110+ , aeson >= 1.2 && < 3
53111 , base64-bytestring >= 1.0
54112 , binary >= 0.8.5.0
55113 , bytestring >= 0.10
56114 , case-insensitive >= 1.2
57115 , conduit >= 1.3
58116 , conduit-extra >= 1.3
59− , connection
60− , containers >= 0.5
117+ , crypton-connection
61118 , cryptonite >= 0.25
62119 , cryptonite-conduit >= 0.2
120+ , data-default-class
63121 , digest >= 0.0.1
64122 , directory
65− , exceptions
66123 , filepath >= 1.4
67124 , http-client >= 0.5
68125 , http-client-tls
@@ -70,205 +127,227 @@ library
70127 , http-types >= 0.12
71128 , ini
72129 , memory >= 0.14
73− , raw-strings-qq >= 1
130+ , network-uri
74131 , resourcet >= 1.2
75132 , retry
76133 , text >= 1.2
77− , time >= 1.8
134+ , time >= 1.9
135+ , time-units ^>= 1.0.0
78136 , transformers >= 0.5
79− , unliftio >= 0.2
80− , unliftio-core >= 0.1
137+ , unliftio >= 0.2 && < 0.3
138+ , unliftio-core >= 0.2 && < 0.3
81139 , unordered-containers >= 0.2
82140 , xml-conduit >= 1.8
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
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
95148
96149Flag live-test
97− Default: True
150+ Description: Build the test suite that runs against a live MinIO server
151+ Default: False
98152 Manual: True
99153
100154test-suite minio-hs-live-server-test
155+ import: base-settings
101156 type: exitcode-stdio-1.0
102157 hs-source-dirs: test, src
103158 main-is: LiveServer.hs
104159 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
105− default-language: Haskell2010
106− default-extensions: BangPatterns
107− , FlexibleContexts
108− , FlexibleInstances
109− , MultiParamTypeClasses
110− , MultiWayIf
111− , NoImplicitPrelude
112− , OverloadedStrings
113− , RankNTypes
114− , ScopedTypeVariables
115− , TupleSections
116− , TypeFamilies
117− other-modules: Lib.Prelude
118− , Network.Minio
119− , Network.Minio.API
120− , Network.Minio.API.Test
121− , Network.Minio.APICommon
160+ other-modules: Network.Minio
161+ , Network.Minio.S3API
122162 , 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
163+ , Network.Minio.API.Test
130164 , 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
137165 , Network.Minio.TestHelpers
138− , Network.Minio.Utils
139166 , Network.Minio.Utils.Test
140− , Network.Minio.XmlGenerator
141167 , Network.Minio.XmlGenerator.Test
142− , Network.Minio.XmlParser
143168 , Network.Minio.XmlParser.Test
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
169+ , Network.Minio.Credentials
170+ build-depends: minio-hs
171+ , raw-strings-qq
172172 , tasty
173173 , tasty-hunit
174174 , tasty-quickcheck
175175 , tasty-smallcheck
176− , temporary
177− , text
178− , time
179− , transformers
180− , unliftio
181− , unliftio-core
182− , unordered-containers
183− , xml-conduit
176+ , QuickCheck
184177 if !flag(live-test)
185178 buildable: False
186179
187180test-suite minio-hs-test
181+ import: base-settings
188182 type: exitcode-stdio-1.0
189183 hs-source-dirs: test, src
190184 main-is: Spec.hs
191− build-depends: base >= 4.7 && < 5
192− , minio-hs
193− , protolude >= 0.1.6
185+ build-depends: minio-hs
186+ , raw-strings-qq
194187 , 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
219188 , tasty
220189 , tasty-hunit
221190 , tasty-quickcheck
222191 , tasty-smallcheck
223− , temporary
224− , text
225− , time
226− , transformers
227− , unliftio
228− , unliftio-core
229− , unordered-containers
230− , xml-conduit
231192 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
244193 other-modules: Lib.Prelude
245194 , Network.Minio
246− , Network.Minio.API
247− , Network.Minio.API.Test
248− , Network.Minio.APICommon
249− , Network.Minio.AdminAPI
250− , Network.Minio.CopyObject
251− , Network.Minio.Data
252− , Network.Minio.Data.ByteString
253− , Network.Minio.Data.Crypto
254− , Network.Minio.Data.Time
255− , Network.Minio.Errors
256− , Network.Minio.JsonParser
257− , Network.Minio.JsonParser.Test
258− , Network.Minio.ListOps
259− , Network.Minio.PresignedOperations
260− , Network.Minio.PutObject
261195 , Network.Minio.S3API
262− , Network.Minio.SelectAPI
263− , Network.Minio.Sign.V4
196+ , Network.Minio.AdminAPI
264197 , Network.Minio.TestHelpers
265− , Network.Minio.Utils
198+ , Network.Minio.API.Test
199+ , Network.Minio.JsonParser.Test
266200 , Network.Minio.Utils.Test
267− , Network.Minio.XmlGenerator
268201 , Network.Minio.XmlGenerator.Test
269− , Network.Minio.XmlParser
270202 , Network.Minio.XmlParser.Test
203+ , Network.Minio.Credentials
271204
272−source-repository head
273− type: git
274− location: https://github.com/minio/minio-hs
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
244+
245+executable GetConfig
246+ import: examples-settings
247+ scope: private
248+ main-is: GetConfig.hs
249+
250+executable GetObject
251+ import: examples-settings
252+ scope: private
253+ main-is: GetObject.hs
254+
255+executable HeadObject
256+ import: examples-settings
257+ scope: private
258+ main-is: HeadObject.hs
259+
260+executable Heal
261+ import: examples-settings
262+ scope: private
263+ main-is: Heal.hs
264+
265+executable ListBuckets
266+ import: examples-settings
267+ scope: private
268+ main-is: ListBuckets.hs
269+
270+executable ListIncompleteUploads
271+ import: examples-settings
272+ scope: private
273+ main-is: ListIncompleteUploads.hs
274+
275+executable ListObjects
276+ import: examples-settings
277+ scope: private
278+ main-is: ListObjects.hs
279+
280+executable MakeBucket
281+ import: examples-settings
282+ scope: private
283+ main-is: MakeBucket.hs
284+
285+executable PresignedGetObject
286+ import: examples-settings
287+ scope: private
288+ main-is: PresignedGetObject.hs
289+
290+executable PresignedPostPolicy
291+ import: examples-settings
292+ scope: private
293+ main-is: PresignedPostPolicy.hs
294+
295+executable PresignedPutObject
296+ import: examples-settings
297+ scope: private
298+ main-is: PresignedPutObject.hs
299+
300+executable PutObject
301+ import: examples-settings
302+ scope: private
303+ main-is: PutObject.hs
304+
305+executable RemoveBucket
306+ import: examples-settings
307+ scope: private
308+ main-is: RemoveBucket.hs
309+
310+executable RemoveIncompleteUpload
311+ import: examples-settings
312+ scope: private
313+ main-is: RemoveIncompleteUpload.hs
314+
315+executable RemoveObject
316+ import: examples-settings
317+ scope: private
318+ main-is: RemoveObject.hs
319+
320+executable SelectObject
321+ import: examples-settings
322+ scope: private
323+ main-is: SelectObject.hs
324+
325+executable ServerInfo
326+ import: examples-settings
327+ scope: private
328+ main-is: ServerInfo.hs
329+
330+executable ServiceSendRestart
331+ import: examples-settings
332+ scope: private
333+ main-is: ServiceSendRestart.hs
334+
335+executable ServiceSendStop
336+ import: examples-settings
337+ scope: private
338+ main-is: ServiceSendStop.hs
339+
340+executable ServiceStatus
341+ import: examples-settings
342+ scope: private
343+ main-is: ServiceStatus.hs
344+
345+executable SetConfig
346+ import: examples-settings
347+ scope: private
348+ main-is: SetConfig.hs
349+
350+executable AssumeRole
351+ import: examples-settings
352+ scope: private
353+ main-is: AssumeRole.hs
src/Lib/Prelude.hs+33−11
@@ -15,19 +15,41 @@
1515--
1616
1717module Lib.Prelude
18− ( module Exports
19− , both
20− ) where
18+ ( module Exports,
19+ both,
20+ showBS,
21+ toStrictBS,
22+ fromStrictBS,
23+ lastMay,
24+ )
25+where
2126
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)
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+ )
3040
3141-- | Apply a function on both elements of a pair
3242both :: (a -> b) -> (a, a) -> (b, b)
3343both f (a, b) = (f a, f b)
44+
45+showBS :: (Show a) => a -> ByteString
46+showBS a = encodeUtf8 (show a :: Text)
47+
48+toStrictBS :: LByteString -> ByteString
49+toStrictBS = LB.toStrict
50+
51+fromStrictBS :: ByteString -> LByteString
52+fromStrictBS = LB.fromStrict
53+
54+lastMay :: [a] -> Maybe a
55+lastMay a = last <$> nonEmpty a
src/Network/Minio.hs+254−218
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,219 +16,237 @@
1616
1717-- |
1818-- Module: Network.Minio
19−-- Copyright: (c) 2017-2019 MinIO Dev Team
19+-- Copyright: (c) 2017-2023 MinIO Dev Team
2020-- License: Apache 2.0
2121-- Maintainer: MinIO Dev Team <dev@min.io>
2222--
23−-- Types and functions to access S3 compatible object storage servers
24−-- like MinIO.
25−
23+-- Types and functions to conveniently access S3 compatible object
24+-- storage servers like MinIO.
2625module Network.Minio
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
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
213233
214234{-
215235This module exports the high-level MinIO API for object storage.
216236-}
217237
218−import qualified Data.Conduit as C
219−import qualified Data.Conduit.Binary as CB
238+import qualified Data.Conduit as C
239+import qualified Data.Conduit.Binary as CB
220240import qualified Data.Conduit.Combinators as CC
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
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
232250
233251-- | Lists buckets.
234252listBuckets :: Minio [BucketInfo]
@@ -240,11 +258,15 @@ listBuckets = getService
240258fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
241259fGetObject bucket object fp opts = do
242260 src <- getObject bucket object opts
243− C.connect src $ CB.sinkFileCautious fp
261+ C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
244262
245263-- | Upload the given file to the given object.
246−fPutObject :: Bucket -> Object -> FilePath
247− -> PutObjectOptions -> Minio ()
264+fPutObject ::
265+ Bucket ->
266+ Object ->
267+ FilePath ->
268+ PutObjectOptions ->
269+ Minio ()
248270fPutObject bucket object f opts =
249271 void $ putObjectInternal bucket object opts $ ODFile f Nothing
250272
@@ -252,8 +274,13 @@ fPutObject bucket object f opts =
252274-- known; this helps the library select optimal part sizes to perform
253275-- a multipart upload. If not specified, it is assumed that the object
254276-- can be potentially 5TiB and selects multipart sizes appropriately.
255−putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio ()
256− -> Maybe Int64 -> PutObjectOptions -> Minio ()
277+putObject ::
278+ Bucket ->
279+ Object ->
280+ C.ConduitM () ByteString Minio () ->
281+ Maybe Int64 ->
282+ PutObjectOptions ->
283+ Minio ()
257284putObject bucket object src sizeMay opts =
258285 void $ putObjectInternal bucket object opts $ ODStream src sizeMay
259286
@@ -263,18 +290,25 @@ putObject bucket object src sizeMay opts =
263290-- copy operation if the new object is to be greater than 5GiB in
264291-- size.
265292copyObject :: DestinationInfo -> SourceInfo -> Minio ()
266−copyObject dstInfo srcInfo = void $ copyObjectInternal (dstBucket dstInfo)
267− (dstObject dstInfo) srcInfo
293+copyObject dstInfo srcInfo =
294+ void $
295+ copyObjectInternal
296+ (dstBucket dstInfo)
297+ (dstObject dstInfo)
298+ srcInfo
268299
269300-- | Remove an object from the object store.
270301removeObject :: Bucket -> Object -> Minio ()
271302removeObject = deleteObject
272303
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)
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
278312
279313-- | Get an object's metadata from the object store. It accepts the
280314-- same options as GetObject.
@@ -304,6 +338,8 @@ bucketExists = headBucket
304338-- | Removes an ongoing multipart upload of an object.
305339removeIncompleteUpload :: Bucket -> Object -> Minio ()
306340removeIncompleteUpload bucket object = do
307− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
308− C..| CC.sinkList
341+ uploads <-
342+ C.runConduit $
343+ listIncompleteUploads bucket (Just object) False
344+ C..| CC.sinkList
309345 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs+259−159
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,169 +15,260 @@
1515--
1616
1717module Network.Minio.API
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
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
5359
5460-- | Fetch bucket location (region)
5561getLocation :: Bucket -> Minio Region
5662getLocation bucket = do
57− resp <- executeRequest $ defaultS3ReqInfo {
58− riBucket = Just bucket
59− , riQueryParams = [("location", Nothing)]
60− , riNeedsLocation = False
61− }
63+ resp <-
64+ executeRequest $
65+ defaultS3ReqInfo
66+ { riBucket = Just bucket,
67+ riQueryParams = [("location", Nothing)],
68+ riNeedsLocation = False
69+ }
6270 parseLocation $ NC.responseBody resp
6371
64−
6572-- | Looks for region in RegionMap and updates it using getLocation if
6673-- absent.
6774discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
6875discoverRegion ri = runMaybeT $ do
6976 bucket <- MaybeT $ return $ riBucket ri
7077 regionMay <- lift $ lookupRegionCache bucket
71− maybe (do
72− l <- lift $ getLocation bucket
73− lift $ addToRegionCache bucket l
74− return l
75− ) return regionMay
76−
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.
7788getRegion :: S3ReqInfo -> Minio (Maybe Region)
7889getRegion ri = do
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
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
91101
92102getRegionHost :: Region -> Minio Text
93103getRegionHost r = do
94− ci <- asks mcConnInfo
95−
96− if "amazonaws.com" `T.isSuffixOf` connectHost ci
97− then maybe (throwIO $ MErrVRegionNotSupported r)
98− return (Map.lookup r awsRegionMap)
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)
99112 else return $ connectHost ci
100113
114+-- | Computes the appropriate host, path and region for the request.
115+--
116+-- For AWS, always use virtual bucket style, unless bucket has periods. For
117+-- MinIO and other non-AWS, default to path style.
118+getHostPathRegion :: S3ReqInfo -> Minio (Text, ByteString, Maybe Region)
119+getHostPathRegion ri = do
120+ ci <- asks mcConnInfo
121+ regionMay <- getRegion ri
122+ case riBucket ri of
123+ Nothing ->
124+ -- Implies a ListBuckets request.
125+ return (connectHost ci, "/", regionMay)
126+ Just bucket -> do
127+ regionHost <- case regionMay of
128+ Nothing -> return $ connectHost ci
129+ Just "" -> return $ connectHost ci
130+ Just r -> getRegionHost r
131+ let pathStyle =
132+ ( regionHost,
133+ getS3Path (riBucket ri) (riObject ri),
134+ regionMay
135+ )
136+ virtualStyle =
137+ ( bucket <> "." <> regionHost,
138+ encodeUtf8 $ "/" <> fromMaybe "" (riObject ri),
139+ regionMay
140+ )
141+ ( if isAWSConnectInfo ci
142+ then
143+ return $
144+ if bucketHasPeriods bucket
145+ then pathStyle
146+ else virtualStyle
147+ else return pathStyle
148+ )
149+
150+-- | requestSTSCredential requests temporary credentials using the Security Token
151+-- Service API. The returned credential will include a populated 'SessionToken'
152+-- and an 'ExpiryTime'.
153+requestSTSCredential :: (STSCredentialProvider p) => p -> IO (CredentialValue, ExpiryTime)
154+requestSTSCredential p = do
155+ endpoint <- maybe (throwIO $ MErrValidation MErrVSTSEndpointNotFound) return $ getSTSEndpoint p
156+ let endPt = NC.parseRequest_ $ toString endpoint
157+ settings
158+ | NC.secure endPt = NC.tlsManagerSettings
159+ | otherwise = defaultManagerSettings
160+
161+ mgr <- NC.newManager settings
162+ liftIO $ retrieveSTSCredentials p ("", 0, False) mgr
163+
101164buildRequest :: S3ReqInfo -> Minio NC.Request
102165buildRequest ri = do
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− }
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
168193
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+ }
169258
170259retryAPIRequest :: Minio a -> Minio a
171260retryAPIRequest apiCall = do
172− resE <- retrying retryPolicy (const shouldRetry) $
173− const $ try apiCall
261+ resE <-
262+ retrying retryPolicy (const shouldRetry) $
263+ const $
264+ try apiCall
174265 either throwIO return resE
175266 where
176267 -- Retry using the full-jitter backoff method for up to 10 mins
177268 -- total
178− retryPolicy = limitRetriesByCumulativeDelay tenMins
179− $ fullJitterBackoff oneMilliSecond
180−
269+ retryPolicy =
270+ limitRetriesByCumulativeDelay tenMins $
271+ fullJitterBackoff oneMilliSecond
181272 oneMilliSecond = 1000 -- in microseconds
182273 tenMins = 10 * 60 * 1000000 -- in microseconds
183274 -- retry on connection related failure
@@ -189,23 +280,23 @@ retryAPIRequest apiCall = do
189280 -- API request failed with a retryable exception
190281 Left httpExn@(NC.HttpExceptionRequest _ exn) ->
191282 case (exn :: NC.HttpExceptionContent) of
192− NC.ResponseTimeout -> return True
193− NC.ConnectionTimeout -> return True
283+ NC.ResponseTimeout -> return True
284+ NC.ConnectionTimeout -> return True
194285 NC.ConnectionFailure _ -> return True
195286 -- We received an unexpected exception
196− _ -> throwIO httpExn
287+ _ -> throwIO httpExn
197288 -- We received an unexpected exception
198289 Left someOtherExn -> throwIO someOtherExn
199290
200−
201291executeRequest :: S3ReqInfo -> Minio (Response LByteString)
202292executeRequest ri = do
203293 req <- buildRequest ri
204294 mgr <- asks mcConnManager
205295 retryAPIRequest $ httpLbs req mgr
206296
207−mkStreamRequest :: S3ReqInfo
208− -> Minio (Response (C.ConduitM () ByteString Minio ()))
297+mkStreamRequest ::
298+ S3ReqInfo ->
299+ Minio (Response (C.ConduitM () ByteString Minio ()))
209300mkStreamRequest ri = do
210301 req <- buildRequest ri
211302 mgr <- asks mcConnManager
@@ -214,41 +305,50 @@ mkStreamRequest ri = do
214305-- Bucket name validity check according to AWS rules.
215306isValidBucketName :: Bucket -> Bool
216307isValidBucketName bucket =
217− not (or [ len < 3 || len > 63
218− , or (map labelCheck labels)
219− , or (map labelCharsCheck labels)
220− , isIPCheck
221− ])
308+ not
309+ ( or
310+ [ len < 3 || len > 63,
311+ any labelCheck labels,
312+ any labelCharsCheck labels,
313+ isIPCheck
314+ ]
315+ )
222316 where
223317 len = T.length bucket
224318 labels = T.splitOn "." bucket
225−
226319 -- does label `l` fail basic checks of length and start/end?
227320 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
228−
229321 -- does label `l` have non-allowed characters?
230− labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
231− x == '-' ||
232− C.isDigit x)) l
233−
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
234333 -- does label `l` have non-digit characters?
235334 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
236335 labelAsNums = map (not . labelNonDigits) labels
237−
238336 -- check if bucket name looks like an IP
239337 isIPCheck = and labelAsNums && length labelAsNums == 4
240338
241339-- Throws exception iff bucket name is invalid according to AWS rules.
242−checkBucketNameValidity :: MonadIO m => Bucket -> m ()
340+checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
243341checkBucketNameValidity bucket =
244− when (not $ isValidBucketName bucket) $
245− throwIO $ MErrVInvalidBucketName bucket
342+ unless (isValidBucketName bucket) $
343+ throwIO $
344+ MErrVInvalidBucketName bucket
246345
247346isValidObjectName :: Object -> Bool
248347isValidObjectName object =
249348 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
250349
251−checkObjectNameValidity :: MonadIO m => Object -> m ()
350+checkObjectNameValidity :: (MonadIO m) => Object -> m ()
252351checkObjectNameValidity object =
253− when (not $ isValidObjectName object) $
254− throwIO $ MErrVInvalidObjectName object
352+ unless (isValidObjectName object) $
353+ throwIO $
354+ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hs+36−24
@@ -16,37 +16,39 @@
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 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
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
3130
3231sha256Header :: ByteString -> HT.Header
33−sha256Header = ("x-amz-content-sha256", )
32+sha256Header = ("x-amz-content-sha256",)
3433
3534-- | This function throws an error if the payload is a conduit (as it
3635-- will not be possible to re-read the conduit after it is consumed).
3736getPayloadSHA256Hash :: Payload -> Minio ByteString
3837getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
39−getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
40− sourceHandleRange h
41− (return . fromIntegral $ off)
42− (return . fromIntegral $ size)
38+getPayloadSHA256Hash (PayloadH h off size) =
39+ hashSHA256FromSource $
40+ sourceHandleRange
41+ h
42+ (return . fromIntegral $ off)
43+ (return . fromIntegral $ size)
4344getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
4445
4546getRequestBody :: Payload -> NC.RequestBody
4647getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
4748getRequestBody (PayloadH h off size) =
48− NC.requestBodySource (fromIntegral size) $
49− sourceHandleRange h
49+ NC.requestBodySource size $
50+ sourceHandleRange
51+ h
5052 (return . fromIntegral $ off)
5153 (return . fromIntegral $ size)
5254getRequestBody (PayloadC n src) = NC.requestBodySource n src
@@ -55,14 +57,24 @@ mkStreamingPayload :: Payload -> Payload
5557mkStreamingPayload payload =
5658 case payload of
5759 PayloadBS bs ->
58− PayloadC (fromIntegral $ BS.length bs)
60+ PayloadC
61+ (fromIntegral $ BS.length bs)
5962 (C.sourceLazy $ LB.fromStrict bs)
6063 PayloadH h off len ->
61− PayloadC len $ sourceHandleRange h
62− (return . fromIntegral $ off)
63− (return . fromIntegral $ len)
64+ PayloadC len $
65+ sourceHandleRange
66+ h
67+ (return . fromIntegral $ off)
68+ (return . fromIntegral $ len)
6469 _ -> payload
6570
6671isStreamingPayload :: Payload -> Bool
6772isStreamingPayload (PayloadC _ _) = True
68−isStreamingPayload _ = False
73+isStreamingPayload _ = False
74+
75+-- | Checks if the connect info is for Amazon S3.
76+isAWSConnectInfo :: ConnectInfo -> Bool
77+isAWSConnectInfo ci = ".amazonaws.com" `T.isSuffixOf` connectHost ci
78+
79+bucketHasPeriods :: Bucket -> Bool
80+bucketHasPeriods b = isJust $ T.find (== '.') b
src/Network/Minio/AdminAPI.hs+543−391
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2018-2023 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,270 +16,341 @@
1616
1717module Network.Minio.AdminAPI
1818 ( -- * MinIO Admin API
19+
1920 --------------------
21+
2022 -- | Provides MinIO admin API and related types. It is in
2123 -- experimental state.
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
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
7988
8089data DriveInfo = DriveInfo
81− { diUuid :: Text
82− , diEndpoint :: Text
83− , diState :: Text
84− } deriving (Eq, Show)
90+ { diUuid :: Text,
91+ diEndpoint :: Text,
92+ diState :: Text
93+ }
94+ deriving stock (Show, Eq)
8595
8696instance FromJSON DriveInfo where
87− parseJSON = withObject "DriveInfo" $ \v -> DriveInfo
88− <$> v .: "uuid"
89− <*> v .: "endpoint"
90− <*> v .: "state"
97+ parseJSON = withObject "DriveInfo" $ \v ->
98+ DriveInfo
99+ <$> v
100+ .: "uuid"
101+ <*> v
102+ .: "endpoint"
103+ <*> v
104+ .: "state"
91105
92106data StorageClass = StorageClass
93− { scParity :: Int
94− , scData :: Int
95− } deriving (Eq, Show)
107+ { scParity :: Int,
108+ scData :: Int
109+ }
110+ deriving stock (Show, Eq)
96111
97112data ErasureInfo = ErasureInfo
98− { eiOnlineDisks :: Int
99− , eiOfflineDisks :: Int
100− , eiStandard :: StorageClass
101− , eiReducedRedundancy :: StorageClass
102− , eiSets :: [[DriveInfo]]
103− } deriving (Eq, Show)
113+ { eiOnlineDisks :: Int,
114+ eiOfflineDisks :: Int,
115+ eiStandard :: StorageClass,
116+ eiReducedRedundancy :: StorageClass,
117+ eiSets :: [[DriveInfo]]
118+ }
119+ deriving stock (Show, Eq)
104120
105121instance FromJSON ErasureInfo where
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)
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)
121144
122145instance FromJSON Backend where
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)
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)
129152
130153data ConnStats = ConnStats
131− { csTransferred :: Int64
132− , csReceived :: Int64
133− } deriving (Eq, Show)
154+ { csTransferred :: Int64,
155+ csReceived :: Int64
156+ }
157+ deriving stock (Show, Eq)
134158
135159instance FromJSON ConnStats where
136− parseJSON = withObject "ConnStats" $ \v -> ConnStats
137− <$> v .: "transferred"
138− <*> v .: "received"
160+ parseJSON = withObject "ConnStats" $ \v ->
161+ ConnStats
162+ <$> v
163+ .: "transferred"
164+ <*> v
165+ .: "received"
139166
140167data ServerProps = ServerProps
141− { spUptime :: NominalDiffTime
142− , spVersion :: Text
143− , spCommitId :: Text
144− , spRegion :: Text
145− , spSqsArns :: [Text]
146− } deriving (Eq, Show)
168+ { spUptime :: NominalDiffTime,
169+ spVersion :: Text,
170+ spCommitId :: Text,
171+ spRegion :: Text,
172+ spSqsArns :: [Text]
173+ }
174+ deriving stock (Show, Eq)
147175
148176instance FromJSON ServerProps where
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
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
157185
158186data StorageInfo = StorageInfo
159− { siUsed :: Int64
160− , siBackend :: Backend
161− } deriving (Eq, Show)
187+ { siUsed :: Int64,
188+ siBackend :: Backend
189+ }
190+ deriving stock (Show, Eq)
162191
163192instance FromJSON StorageInfo where
164− parseJSON = withObject "StorageInfo" $ \v -> StorageInfo
165− <$> v .: "Used"
166− <*> v .: "Backend"
193+ parseJSON = withObject "StorageInfo" $ \v ->
194+ StorageInfo
195+ <$> v
196+ .: "Used"
197+ <*> v
198+ .: "Backend"
167199
168200data CountNAvgTime = CountNAvgTime
169− { caCount :: Int64
170− , caAvgDuration :: Text
171− } deriving (Eq, Show)
201+ { caCount :: Int64,
202+ caAvgDuration :: Text
203+ }
204+ deriving stock (Show, Eq)
172205
173206instance FromJSON CountNAvgTime where
174− parseJSON = withObject "CountNAvgTime" $ \v -> CountNAvgTime
175− <$> v .: "count"
176− <*> v .: "avgDuration"
207+ parseJSON = withObject "CountNAvgTime" $ \v ->
208+ CountNAvgTime
209+ <$> v
210+ .: "count"
211+ <*> v
212+ .: "avgDuration"
177213
178214data HttpStats = HttpStats
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)
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)
190227
191228instance FromJSON HttpStats where
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"
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"
203251
204252data SIData = SIData
205− { sdStorage :: StorageInfo
206− , sdConnStats :: ConnStats
207− , sdHttpStats :: HttpStats
208− , sdProps :: ServerProps
209− } deriving (Eq, Show)
253+ { sdStorage :: StorageInfo,
254+ sdConnStats :: ConnStats,
255+ sdHttpStats :: HttpStats,
256+ sdProps :: ServerProps
257+ }
258+ deriving stock (Show, Eq)
210259
211260instance FromJSON SIData where
212− parseJSON = withObject "SIData" $ \v -> SIData
213− <$> v .: "storage"
214− <*> v .: "network"
215− <*> v .: "http"
216− <*> v .: "server"
261+ parseJSON = withObject "SIData" $ \v ->
262+ SIData
263+ <$> v
264+ .: "storage"
265+ <*> v
266+ .: "network"
267+ <*> v
268+ .: "http"
269+ <*> v
270+ .: "server"
217271
218272data ServerInfo = ServerInfo
219− { siError :: Text
220− , siAddr :: Text
221− , siData :: SIData
222− } deriving (Eq, Show)
273+ { siError :: Text,
274+ siAddr :: Text,
275+ siData :: SIData
276+ }
277+ deriving stock (Show, Eq)
223278
224279instance FromJSON ServerInfo where
225− parseJSON = withObject "ServerInfo" $ \v -> ServerInfo
226− <$> v .: "error"
227− <*> v .: "addr"
228− <*> v .: "data"
280+ parseJSON = withObject "ServerInfo" $ \v ->
281+ ServerInfo
282+ <$> v
283+ .: "error"
284+ <*> v
285+ .: "addr"
286+ <*> v
287+ .: "data"
229288
230289data ServerVersion = ServerVersion
231− { svVersion :: Text
232− , svCommitId :: Text
233− } deriving (Eq, Show)
290+ { svVersion :: Text,
291+ svCommitId :: Text
292+ }
293+ deriving stock (Show, Eq)
234294
235295instance FromJSON ServerVersion where
236− parseJSON = withObject "ServerVersion" $ \v -> ServerVersion
237− <$> v .: "version"
238− <*> v .: "commitID"
296+ parseJSON = withObject "ServerVersion" $ \v ->
297+ ServerVersion
298+ <$> v
299+ .: "version"
300+ <*> v
301+ .: "commitID"
239302
240303data ServiceStatus = ServiceStatus
241− { ssVersion :: ServerVersion
242− , ssUptime :: NominalDiffTime
243− } deriving (Eq, Show)
304+ { ssVersion :: ServerVersion,
305+ ssUptime :: NominalDiffTime
306+ }
307+ deriving stock (Show, Eq)
244308
245309instance FromJSON ServiceStatus where
246− parseJSON = withObject "ServiceStatus" $ \v -> do
247− serverVersion <- v .: "serverVersion"
248− uptimeNs <- v .: "uptime"
249− let uptime = uptimeNs / 1e9
250− return $ ServiceStatus serverVersion uptime
310+ parseJSON = withObject "ServiceStatus" $ \v -> do
311+ serverVersion <- v .: "serverVersion"
312+ uptimeNs <- v .: "uptime"
313+ let uptime = uptimeNs / 1e9
314+ return $ ServiceStatus serverVersion uptime
251315
252−data ServiceAction = ServiceActionRestart
253− | ServiceActionStop
254− deriving (Eq, Show)
316+data ServiceAction
317+ = ServiceActionRestart
318+ | ServiceActionStop
319+ deriving stock (Show, Eq)
255320
256321instance ToJSON ServiceAction where
257− toJSON a = object [ "action" .= serviceActionToText a ]
322+ toJSON a = object ["action" .= serviceActionToText a]
258323
259324serviceActionToText :: ServiceAction -> Text
260325serviceActionToText a = case a of
261326 ServiceActionRestart -> "restart"
262− ServiceActionStop -> "stop"
327+ ServiceActionStop -> "stop"
263328
264329adminPath :: ByteString
265330adminPath = "/minio/admin"
266331
267332data HealStartResp = HealStartResp
268− { hsrClientToken :: Text
269− , hsrClientAddr :: Text
270− , hsrStartTime :: UTCTime
271− } deriving (Eq, Show)
333+ { hsrClientToken :: Text,
334+ hsrClientAddr :: Text,
335+ hsrStartTime :: UTCTime
336+ }
337+ deriving stock (Show, Eq)
272338
273339instance FromJSON HealStartResp where
274− parseJSON = withObject "HealStartResp" $ \v -> HealStartResp
275− <$> v .: "clientToken"
276− <*> v .: "clientAddress"
277− <*> v .: "startTime"
340+ parseJSON = withObject "HealStartResp" $ \v ->
341+ HealStartResp
342+ <$> v
343+ .: "clientToken"
344+ <*> v
345+ .: "clientAddress"
346+ <*> v
347+ .: "startTime"
278348
279349data HealOpts = HealOpts
280− { hoRecursive :: Bool
281− , hoDryRun :: Bool
282− } deriving (Eq, Show)
350+ { hoRecursive :: Bool,
351+ hoDryRun :: Bool
352+ }
353+ deriving stock (Show, Eq)
283354
284355instance ToJSON HealOpts where
285356 toJSON (HealOpts r d) =
@@ -288,197 +359,259 @@ instance ToJSON HealOpts where
288359 pairs ("recursive" .= r <> "dryRun" .= d)
289360
290361instance FromJSON HealOpts where
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)
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)
300375
301376instance FromJSON HealItemType where
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)
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)
308383
309384data NodeSummary = NodeSummary
310− { nsName :: Text
311− , nsErrSet :: Bool
312− , nsErrMessage :: Text
313− } deriving (Eq, Show)
385+ { nsName :: Text,
386+ nsErrSet :: Bool,
387+ nsErrMessage :: Text
388+ }
389+ deriving stock (Show, Eq)
314390
315391instance FromJSON NodeSummary where
316− parseJSON = withObject "NodeSummary" $ \v -> NodeSummary
317− <$> v .: "name"
318− <*> v .: "errSet"
319− <*> v .: "errMsg"
392+ parseJSON = withObject "NodeSummary" $ \v ->
393+ NodeSummary
394+ <$> v
395+ .: "name"
396+ <*> v
397+ .: "errSet"
398+ <*> v
399+ .: "errMsg"
320400
321401data SetConfigResult = SetConfigResult
322− { scrStatus :: Bool
323− , scrNodeSummary :: [NodeSummary]
324− } deriving (Eq, Show)
402+ { scrStatus :: Bool,
403+ scrNodeSummary :: [NodeSummary]
404+ }
405+ deriving stock (Show, Eq)
325406
326407instance FromJSON SetConfigResult where
327− parseJSON = withObject "SetConfigResult" $ \v -> SetConfigResult
328− <$> v .: "status"
329− <*> v .: "nodeResults"
408+ parseJSON = withObject "SetConfigResult" $ \v ->
409+ SetConfigResult
410+ <$> v
411+ .: "status"
412+ <*> v
413+ .: "nodeResults"
330414
331415data HealResultItem = HealResultItem
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)
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)
345430
346431instance FromJSON HealResultItem where
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")
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+ )
362462
363463data HealStatus = HealStatus
364− { hsSummary :: Text
365− , hsStartTime :: UTCTime
366− , hsSettings :: HealOpts
367− , hsNumDisks :: Int
368− , hsFailureDetail :: Maybe Text
369− , hsItems :: Maybe [HealResultItem]
370− } deriving (Eq, Show)
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)
371472
372473instance FromJSON HealStatus where
373− parseJSON = withObject "HealStatus" $ \v -> HealStatus
374− <$> v .: "Summary"
375− <*> v .: "StartTime"
376− <*> v .: "Settings"
377− <*> v .: "NumDisks"
378− <*> v .:? "Detail"
379− <*> v .: "Items"
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"
380488
381489healPath :: Maybe Bucket -> Maybe Text -> ByteString
382490healPath bucket prefix = do
383− if (isJust bucket)
384− then encodeUtf8 $ "v1/heal/" <> fromMaybe "" bucket <> "/"
385− <> fromMaybe "" prefix
386− else encodeUtf8 $ "v1/heal/"
491+ if isJust bucket
492+ then
493+ encodeUtf8 $
494+ "v1/heal/"
495+ <> fromMaybe "" bucket
496+ <> "/"
497+ <> fromMaybe "" prefix
498+ else encodeUtf8 ("v1/heal/" :: Text)
387499
388500-- | Get server version and uptime.
389501serviceStatus :: Minio ServiceStatus
390502serviceStatus = do
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
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
403518
404519-- | Send service restart or stop action to MinIO server.
405520serviceSendAction :: ServiceAction -> Minio ()
406521serviceSendAction action = do
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− }
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+ }
415533
416534-- | Get the current config file from server.
417535getConfig :: Minio ByteString
418536getConfig = do
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
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
427548
428549-- | Set a new config to the server.
429550setConfig :: ByteString -> Minio SetConfigResult
430551setConfig config = do
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
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
443567
444568-- | Get the progress of currently running heal task, this API should be
445569-- invoked right after `startHeal`. `token` is obtained after `startHeal`
446570-- which should be used to get the heal status.
447571getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
448572getHealStatus bucket prefix token = do
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
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
462589
463590doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
464591doHeal bucket prefix opts forceStart = do
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
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
482615
483616-- | Start a heal sequence that scans data under given (possible empty)
484617-- `bucket` and `prefix`. The `recursive` bool turns on recursive
@@ -500,60 +633,79 @@ forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
500633-- properties, storage information, network statistics, etc.
501634getServerInfo :: Minio [ServerInfo]
502635getServerInfo = do
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
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
514650
515651executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
516652executeAdminRequest ari = do
517− req <- buildAdminRequest ari
518− mgr <- asks mcConnManager
519− httpLbs req mgr
653+ req <- buildAdminRequest ari
654+ mgr <- asks mcConnManager
655+ httpLbs req mgr
520656
521657buildAdminRequest :: AdminReqInfo -> Minio NC.Request
522658buildAdminRequest areq = do
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− }
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+ }
548699 where
549700 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
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)
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)
559711 }
src/Network/Minio/CopyObject.hs+48−34
@@ -16,19 +16,19 @@
1616
1717module Network.Minio.CopyObject where
1818
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−
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
2825
2926-- | Copy an object using single or multipart copy strategy.
30−copyObjectInternal :: Bucket -> Object -> SourceInfo
31− -> Minio ETag
27+copyObjectInternal ::
28+ Bucket ->
29+ Object ->
30+ SourceInfo ->
31+ Minio ETag
3232copyObjectInternal b' o srcInfo = do
3333 let sBucket = srcBucket srcInfo
3434 sObject = srcObject srcInfo
@@ -43,27 +43,33 @@ copyObjectInternal b' o srcInfo = do
4343 startOffset = fst range
4444 endOffset = snd range
4545
46− when (isJust rangeMay &&
47− or [startOffset < 0, endOffset < startOffset,
48− endOffset >= fromIntegral srcSize]) $
49− throwIO $ MErrVInvalidSrcObjByteRange range
46+ when
47+ ( isJust rangeMay
48+ && ( (startOffset < 0)
49+ || (endOffset < startOffset)
50+ || (endOffset >= srcSize)
51+ )
52+ )
53+ $ throwIO
54+ $ MErrVInvalidSrcObjByteRange range
5055
5156 -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
5257 -- 2. If startOffset /= 0 use multipart copy
53− let destSize = (\(a, b) -> b - a + 1 ) $
54− maybe (0, srcSize - 1) identity rangeMay
58+ let destSize =
59+ (\(a, b) -> b - a + 1) $
60+ maybe (0, srcSize - 1) identity rangeMay
5561
5662 if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
5763 then multiPartCopyObject b' o srcInfo srcSize
58−
59− else fst <$> copyObjectSingle b' o srcInfo{srcRange = Nothing} []
64+ else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} []
6065
6166-- | Given the input byte range of the source object, compute the
6267-- splits for a multipart copy object procedure. Minimum part size
6368-- used is minPartSize.
6469selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
65−selectCopyRanges (st, end) = zip pns $
66− map (\(x, y) -> (st + x, st + x + y - 1)) $ zip startOffsets partSizes
70+selectCopyRanges (st, end) =
71+ zip pns $
72+ zipWith (\x y -> (st + x, st + x + y - 1)) startOffsets partSizes
6773 where
6874 size = end - st + 1
6975 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
@@ -71,22 +77,30 @@ selectCopyRanges (st, end) = zip pns $
7177-- | Perform a multipart copy object action. Since we cannot verify
7278-- existing parts based on the source object, there is no resuming
7379-- copy action support.
74−multiPartCopyObject :: Bucket -> Object -> SourceInfo -> Int64
75− -> Minio ETag
80+multiPartCopyObject ::
81+ Bucket ->
82+ Object ->
83+ SourceInfo ->
84+ Int64 ->
85+ Minio ETag
7686multiPartCopyObject b o cps srcSize = do
7787 uid <- newMultipartUpload b o []
7888
79− let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $ srcRange cps
89+ let byteRange = maybe (0, srcSize - 1) identity $ srcRange cps
8090 partRanges = selectCopyRanges byteRange
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
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
91105
92106 completeMultipartUpload b o uid copiedParts
src/Network/Minio/Credentials.hsnew file+77
@@ -0,0 +1,77 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.Credentials
18+ ( CredentialValue (..),
19+ credentialValueText,
20+ STSCredentialProvider (..),
21+ AccessKey (..),
22+ SecretKey (..),
23+ SessionToken (..),
24+ ExpiryTime (..),
25+ STSCredentialStore,
26+ initSTSCredential,
27+ getSTSCredential,
28+ Creds (..),
29+ getCredential,
30+ Endpoint,
31+
32+ -- * STS Assume Role
33+ defaultSTSAssumeRoleOptions,
34+ STSAssumeRole (..),
35+ STSAssumeRoleOptions (..),
36+ )
37+where
38+
39+import Data.Time (diffUTCTime, getCurrentTime)
40+import qualified Network.HTTP.Client as NC
41+import Network.Minio.Credentials.AssumeRole
42+import Network.Minio.Credentials.Types
43+import qualified UnliftIO.MVar as M
44+
45+data STSCredentialStore = STSCredentialStore
46+ { cachedCredentials :: M.MVar (CredentialValue, ExpiryTime),
47+ refreshAction :: Endpoint -> NC.Manager -> IO (CredentialValue, ExpiryTime)
48+ }
49+
50+initSTSCredential :: (STSCredentialProvider p) => p -> IO STSCredentialStore
51+initSTSCredential p = do
52+ let action = retrieveSTSCredentials p
53+ -- start with dummy credential, so that refresh happens for first request.
54+ now <- getCurrentTime
55+ mvar <- M.newMVar (CredentialValue mempty mempty mempty, coerce now)
56+ return $
57+ STSCredentialStore
58+ { cachedCredentials = mvar,
59+ refreshAction = action
60+ }
61+
62+getSTSCredential :: STSCredentialStore -> Endpoint -> NC.Manager -> IO (CredentialValue, Bool)
63+getSTSCredential store ep mgr = M.modifyMVar (cachedCredentials store) $ \cc@(v, expiry) -> do
64+ now <- getCurrentTime
65+ if diffUTCTime now (coerce expiry) > 0
66+ then do
67+ res <- refreshAction store ep mgr
68+ return (res, (fst res, True))
69+ else return (cc, (v, False))
70+
71+data Creds
72+ = CredsStatic CredentialValue
73+ | CredsSTS STSCredentialStore
74+
75+getCredential :: Creds -> Endpoint -> NC.Manager -> IO CredentialValue
76+getCredential (CredsStatic v) _ _ = return v
77+getCredential (CredsSTS s) ep mgr = fst <$> getSTSCredential s ep mgr
src/Network/Minio/Credentials/AssumeRole.hsnew file+266
@@ -0,0 +1,266 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.Credentials.AssumeRole where
18+
19+import qualified Data.ByteArray as BA
20+import qualified Data.ByteString.Lazy as LB
21+import qualified Data.Text as T
22+import qualified Data.Time as Time
23+import Data.Time.Units (Second)
24+import Lib.Prelude (UTCTime, throwIO)
25+import Network.HTTP.Client (RequestBody (RequestBodyBS))
26+import qualified Network.HTTP.Client as NC
27+import Network.HTTP.Types (hContentType, methodPost, renderSimpleQuery)
28+import Network.HTTP.Types.Header (hHost)
29+import Network.Minio.Credentials.Types
30+import Network.Minio.Data.Crypto (hashSHA256)
31+import Network.Minio.Errors (MErrV (..))
32+import Network.Minio.Sign.V4
33+import Network.Minio.Utils (getHostHeader, httpLbs)
34+import Network.Minio.XmlCommon
35+import Text.XML.Cursor hiding (bool)
36+
37+stsVersion :: ByteString
38+stsVersion = "2011-06-15"
39+
40+defaultDurationSeconds :: Second
41+defaultDurationSeconds = 3600
42+
43+-- | Assume Role API argument.
44+--
45+-- @since 1.7.0
46+data STSAssumeRole = STSAssumeRole
47+ { -- | Credentials to use in the AssumeRole STS API.
48+ sarCredentials :: CredentialValue,
49+ -- | Optional settings.
50+ sarOptions :: STSAssumeRoleOptions
51+ }
52+
53+-- | Options for STS Assume Role API.
54+data STSAssumeRoleOptions = STSAssumeRoleOptions
55+ { -- | STS endpoint to which the request will be made. For MinIO, this is the
56+ -- same as the server endpoint. For AWS, this has to be the Security Token
57+ -- Service endpoint. If using with 'setSTSCredential', this option can be
58+ -- left as 'Nothing' and the endpoint in 'ConnectInfo' will be used.
59+ saroEndpoint :: Maybe Text,
60+ -- | Desired validity for the generated credentials.
61+ saroDurationSeconds :: Maybe Second,
62+ -- | IAM policy to apply for the generated credentials.
63+ saroPolicyJSON :: Maybe ByteString,
64+ -- | Location is usually required for AWS.
65+ saroLocation :: Maybe Text,
66+ saroRoleARN :: Maybe Text,
67+ saroRoleSessionName :: Maybe Text
68+ }
69+
70+-- | Default STS Assume Role options - all options are Nothing, except for
71+-- duration which is set to 1 hour.
72+defaultSTSAssumeRoleOptions :: STSAssumeRoleOptions
73+defaultSTSAssumeRoleOptions =
74+ STSAssumeRoleOptions
75+ { saroEndpoint = Nothing,
76+ saroDurationSeconds = Just 3600,
77+ saroPolicyJSON = Nothing,
78+ saroLocation = Nothing,
79+ saroRoleARN = Nothing,
80+ saroRoleSessionName = Nothing
81+ }
82+
83+data AssumeRoleCredentials = AssumeRoleCredentials
84+ { arcCredentials :: CredentialValue,
85+ arcExpiration :: UTCTime
86+ }
87+ deriving stock (Show, Eq)
88+
89+data AssumeRoleResult = AssumeRoleResult
90+ { arrSourceIdentity :: Text,
91+ arrAssumedRoleArn :: Text,
92+ arrAssumedRoleId :: Text,
93+ arrRoleCredentials :: AssumeRoleCredentials
94+ }
95+ deriving stock (Show, Eq)
96+
97+-- | parseSTSAssumeRoleResult parses an XML response of the following form:
98+--
99+-- <AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
100+-- <AssumeRoleResult>
101+-- <SourceIdentity>Alice</SourceIdentity>
102+-- <AssumedRoleUser>
103+-- <Arn>arn:aws:sts::123456789012:assumed-role/demo/TestAR</Arn>
104+-- <AssumedRoleId>ARO123EXAMPLE123:TestAR</AssumedRoleId>
105+-- </AssumedRoleUser>
106+-- <Credentials>
107+-- <AccessKeyId>ASIAIOSFODNN7EXAMPLE</AccessKeyId>
108+-- <SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
109+-- <SessionToken>
110+-- AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
111+-- LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
112+-- QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
113+-- 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
114+-- +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
115+-- </SessionToken>
116+-- <Expiration>2019-11-09T13:34:41Z</Expiration>
117+-- </Credentials>
118+-- <PackedPolicySize>6</PackedPolicySize>
119+-- </AssumeRoleResult>
120+-- <ResponseMetadata>
121+-- <RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
122+-- </ResponseMetadata>
123+-- </AssumeRoleResponse>
124+parseSTSAssumeRoleResult :: (MonadIO m) => ByteString -> Text -> m AssumeRoleResult
125+parseSTSAssumeRoleResult xmldata namespace = do
126+ r <- parseRoot $ LB.fromStrict xmldata
127+ let s3Elem' = s3Elem namespace
128+ sourceIdentity =
129+ T.concat $
130+ r
131+ $/ s3Elem' "AssumeRoleResult"
132+ &/ s3Elem' "SourceIdentity"
133+ &/ content
134+ roleArn =
135+ T.concat $
136+ r
137+ $/ s3Elem' "AssumeRoleResult"
138+ &/ s3Elem' "AssumedRoleUser"
139+ &/ s3Elem' "Arn"
140+ &/ content
141+ roleId =
142+ T.concat $
143+ r
144+ $/ s3Elem' "AssumeRoleResult"
145+ &/ s3Elem' "AssumedRoleUser"
146+ &/ s3Elem' "AssumedRoleId"
147+ &/ content
148+
149+ convSB :: Text -> BA.ScrubbedBytes
150+ convSB = BA.convert . (encodeUtf8 :: Text -> ByteString)
151+
152+ credsInfo = do
153+ cr <-
154+ maybe (Left $ MErrVXmlParse "No Credentials Element found") Right $
155+ listToMaybe $
156+ r $/ s3Elem' "AssumeRoleResult" &/ s3Elem' "Credentials"
157+ let cur = fromNode $ node cr
158+ return
159+ ( CredentialValue
160+ { cvAccessKey =
161+ coerce $
162+ T.concat $
163+ cur $/ s3Elem' "AccessKeyId" &/ content,
164+ cvSecretKey =
165+ coerce $
166+ convSB $
167+ T.concat $
168+ cur
169+ $/ s3Elem' "SecretAccessKey"
170+ &/ content,
171+ cvSessionToken =
172+ Just $
173+ coerce $
174+ convSB $
175+ T.concat $
176+ cur
177+ $/ s3Elem' "SessionToken"
178+ &/ content
179+ },
180+ T.concat $ cur $/ s3Elem' "Expiration" &/ content
181+ )
182+ creds <- either throwIO pure credsInfo
183+ expiry <- parseS3XMLTime $ snd creds
184+ let roleCredentials =
185+ AssumeRoleCredentials
186+ { arcCredentials = fst creds,
187+ arcExpiration = expiry
188+ }
189+ return
190+ AssumeRoleResult
191+ { arrSourceIdentity = sourceIdentity,
192+ arrAssumedRoleArn = roleArn,
193+ arrAssumedRoleId = roleId,
194+ arrRoleCredentials = roleCredentials
195+ }
196+
197+instance STSCredentialProvider STSAssumeRole where
198+ getSTSEndpoint = saroEndpoint . sarOptions
199+ retrieveSTSCredentials sar (host', port', isSecure') mgr = do
200+ -- Assemble STS request
201+ let requiredParams =
202+ [ ("Action", "AssumeRole"),
203+ ("Version", stsVersion)
204+ ]
205+ opts = sarOptions sar
206+
207+ durSecs :: Int =
208+ fromIntegral $
209+ fromMaybe defaultDurationSeconds $
210+ saroDurationSeconds opts
211+ otherParams =
212+ [ ("RoleArn",) . encodeUtf8 <$> saroRoleARN opts,
213+ ("RoleSessionName",) . encodeUtf8 <$> saroRoleSessionName opts,
214+ Just ("DurationSeconds", show durSecs),
215+ ("Policy",) <$> saroPolicyJSON opts
216+ ]
217+ parameters = requiredParams ++ catMaybes otherParams
218+ (host, port, isSecure) =
219+ case getSTSEndpoint sar of
220+ Just ep ->
221+ let endPt = NC.parseRequest_ $ toString ep
222+ in (NC.host endPt, NC.port endPt, NC.secure endPt)
223+ Nothing -> (host', port', isSecure')
224+ reqBody = renderSimpleQuery False parameters
225+ req =
226+ NC.defaultRequest
227+ { NC.host = host,
228+ NC.port = port,
229+ NC.secure = isSecure,
230+ NC.method = methodPost,
231+ NC.requestHeaders =
232+ [ (hHost, getHostHeader (host, port)),
233+ (hContentType, "application/x-www-form-urlencoded")
234+ ],
235+ NC.requestBody = RequestBodyBS reqBody
236+ }
237+
238+ -- Sign the STS request.
239+ timeStamp <- liftIO Time.getCurrentTime
240+ let sp =
241+ SignParams
242+ { spAccessKey = coerce $ cvAccessKey $ sarCredentials sar,
243+ spSecretKey = coerce $ cvSecretKey $ sarCredentials sar,
244+ spSessionToken = coerce $ cvSessionToken $ sarCredentials sar,
245+ spService = ServiceSTS,
246+ spTimeStamp = timeStamp,
247+ spRegion = saroLocation opts,
248+ spExpirySecs = Nothing,
249+ spPayloadHash = Just $ hashSHA256 reqBody
250+ }
251+ signHeaders = signV4 sp req
252+ signedReq =
253+ req
254+ { NC.requestHeaders = NC.requestHeaders req ++ signHeaders
255+ }
256+
257+ -- Make the STS request
258+ resp <- httpLbs signedReq mgr
259+ result <-
260+ parseSTSAssumeRoleResult
261+ (toStrict $ NC.responseBody resp)
262+ "https://sts.amazonaws.com/doc/2011-06-15/"
263+ return
264+ ( arcCredentials $ arrRoleCredentials result,
265+ coerce $ arcExpiration $ arrRoleCredentials result
266+ )
src/Network/Minio/Credentials/Types.hsnew file+90
@@ -0,0 +1,90 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17+{-# LANGUAGE StrictData #-}
18+
19+module Network.Minio.Credentials.Types where
20+
21+import qualified Data.ByteArray as BA
22+import Lib.Prelude (UTCTime)
23+import qualified Network.HTTP.Client as NC
24+
25+-- | Access Key type.
26+newtype AccessKey = AccessKey {unAccessKey :: Text}
27+ deriving stock (Show)
28+ deriving newtype (Eq, IsString, Semigroup, Monoid)
29+
30+-- | Secret Key type - has a show instance that does not print the value.
31+newtype SecretKey = SecretKey {unSecretKey :: BA.ScrubbedBytes}
32+ deriving stock (Show)
33+ deriving newtype (Eq, IsString, Semigroup, Monoid)
34+
35+-- | Session Token type - has a show instance that does not print the value.
36+newtype SessionToken = SessionToken {unSessionToken :: BA.ScrubbedBytes}
37+ deriving stock (Show)
38+ deriving newtype (Eq, IsString, Semigroup, Monoid)
39+
40+-- | Object storage credential data type. It has support for the optional
41+-- [SessionToken](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html)
42+-- for using temporary credentials requested via STS.
43+--
44+-- The show instance for this type does not print the value of secrets for
45+-- security.
46+--
47+-- @since 1.7.0
48+data CredentialValue = CredentialValue
49+ { cvAccessKey :: AccessKey,
50+ cvSecretKey :: SecretKey,
51+ cvSessionToken :: Maybe SessionToken
52+ }
53+ deriving stock (Eq, Show)
54+
55+scrubbedToText :: BA.ScrubbedBytes -> Text
56+scrubbedToText =
57+ let b2t :: ByteString -> Text
58+ b2t = decodeUtf8
59+ s2b :: BA.ScrubbedBytes -> ByteString
60+ s2b = BA.convert
61+ in b2t . s2b
62+
63+-- | Convert a 'CredentialValue' to a text tuple. Use this to output the
64+-- credential to files or other programs.
65+credentialValueText :: CredentialValue -> (Text, Text, Maybe Text)
66+credentialValueText cv =
67+ ( coerce $ cvAccessKey cv,
68+ (scrubbedToText . coerce) $ cvSecretKey cv,
69+ scrubbedToText . coerce <$> cvSessionToken cv
70+ )
71+
72+-- | Endpoint represented by host, port and TLS enabled flag.
73+type Endpoint = (ByteString, Int, Bool)
74+
75+-- | Typeclass for STS credential providers.
76+--
77+-- @since 1.7.0
78+class STSCredentialProvider p where
79+ retrieveSTSCredentials ::
80+ p ->
81+ -- | STS Endpoint (host, port, isSecure)
82+ Endpoint ->
83+ NC.Manager ->
84+ IO (CredentialValue, ExpiryTime)
85+ getSTSEndpoint :: p -> Maybe Text
86+
87+-- | 'ExpiryTime' represents a time at which a credential expires.
88+newtype ExpiryTime = ExpiryTime {unExpiryTime :: UTCTime}
89+ deriving stock (Show)
90+ deriving newtype (Eq)
src/Network/Minio/Data.hs+730−557
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,47 +13,64 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−
17−{-# LANGUAGE CPP #-}
18−{-# LANGUAGE GADTs #-}
16+{-# LANGUAGE CPP #-}
17+{-# LANGUAGE GADTs #-}
1918{-# LANGUAGE GeneralizedNewtypeDeriving #-}
20−{-# LANGUAGE TypeFamilies #-}
19+{-# LANGUAGE StrictData #-}
20+{-# LANGUAGE TypeFamilies #-}
21+
2122module Network.Minio.Data where
2223
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
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
5774
5875-- | max obj size is 5TiB
5976maxObjectSize :: Int64
@@ -73,124 +90,157 @@ maxMultipartParts = 10000
7390-- TODO: Add a type which provides typed constants for region. this
7491-- type should have a IsString instance to infer the appropriate
7592-- constant.
93+
7694-- | awsRegionMap - library constant
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− ]
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+ ]
94129
95130-- | Connection Info data type. To create a 'ConnectInfo' value,
96131-- enable the @OverloadedStrings@ language extension and use the
97132-- `IsString` instance to provide a URL, for example:
98133--
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)
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)
110147
111148instance IsString ConnectInfo where
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
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)
136170findFirst [] = return Nothing
137−findFirst (f:fs) = do c <- f
138− maybe (findFirst fs) (return . Just) c
171+findFirst (f : fs) = do
172+ c <- f
173+ maybe (findFirst fs) (return . Just) c
139174
140−-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
141−fromAWSConfigFile :: Provider
175+-- | This action returns a 'CredentialValue' populated from
176+-- @~\/.aws\/credentials@
177+fromAWSConfigFile :: CredentialLoader
142178fromAWSConfigFile = do
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
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
159199fromAWSEnv = runMaybeT $ do
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)
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
163203
164−-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
165−-- @MINIO_SECRET_KEY@ environment variables.
166−fromMinioEnv :: Provider
204+-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
205+-- and @MINIO_SECRET_KEY@ environment variables.
206+fromMinioEnv :: CredentialLoader
167207fromMinioEnv = runMaybeT $ do
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− }
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}
188236
189237-- | Set the S3 region parameter in the `ConnectInfo`
190238setRegion :: Region -> ConnectInfo -> ConnectInfo
191−setRegion r connInfo = connInfo { connectRegion = r
192− , connectAutoDiscoverRegion = False
193− }
239+setRegion r connInfo =
240+ connInfo
241+ { connectRegion = r,
242+ connectAutoDiscoverRegion = False
243+ }
194244
195245-- | Check if the connection to object storage server is secure
196246-- (i.e. uses TLS)
@@ -202,39 +252,37 @@ isConnectInfoSecure = connectIsSecure
202252-- certificates. Note that this option has no effect, if you provide
203253-- your own Manager in `mkMinioConn`.
204254disableTLSCertValidation :: ConnectInfo -> ConnectInfo
205−disableTLSCertValidation c = c { connectDisableTLSCertValidation = True }
255+disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
206256
207257getHostAddr :: ConnectInfo -> ByteString
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
214−
258+getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
215259
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
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+--
219265-- Credentials should be supplied before use.
220266gcsCI :: ConnectInfo
221−gcsCI = setRegion "us"
222− "https://storage.googleapis.com"
223−
267+gcsCI =
268+ setRegion
269+ "us"
270+ "https://storage.googleapis.com"
224271
225−-- | Default AWS ConnectInfo. Connects to "us-east-1". Credentials
272+-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
226273-- should be supplied before use.
227274awsCI :: ConnectInfo
228275awsCI = "https://s3.amazonaws.com"
229276
230−
231−-- | <https://play.min.io:9000 MinIO Play Server>
277+-- | <https://play.min.io MinIO Play Server>
232278-- ConnectInfo. Credentials are already filled in.
233279minioPlayCI :: ConnectInfo
234−minioPlayCI = let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
235− in setCreds playCreds
236− $ setRegion "us-east-1"
237− "https://play.min.io:9000"
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"
238286
239287-- |
240288-- Represents a bucket in the object store
@@ -244,9 +292,7 @@ type Bucket = Text
244292-- Represents an object name
245293type Object = Text
246294
247−-- |
248−-- Represents a region
249−-- TODO: This could be a Sum Type with all defined regions for AWS.
295+-- | Represents a region
250296type Region = Text
251297
252298-- | A type alias to represent an Entity-Tag returned by S3-compatible APIs.
@@ -255,127 +301,147 @@ type ETag = Text
255301-- | Data type to represent an object encryption key. Create one using
256302-- the `mkSSECKey` function.
257303newtype SSECKey = SSECKey BA.ScrubbedBytes
258− deriving (Eq, Show)
304+ deriving stock (Eq, Show)
259305
260306-- | Validates that the given ByteString is 32 bytes long and creates
261307-- an encryption key.
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
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
267314
268315-- | Data type to represent Server-Side-Encryption settings
269316data SSE where
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
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
282329
283330toPutObjectHeaders :: SSE -> [HT.Header]
284331toPutObjectHeaders sseArg =
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)]
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+ ]
304349
305350-- | Data type for options in PutObject call. Start with the empty
306351-- `defaultPutObjectOptions` and use various the various poo*
307352-- accessors.
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
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
332377 }
333378
334379-- | Provide default `PutObjectOptions`.
335380defaultPutObjectOptions :: PutObjectOptions
336381defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
337382
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−
347383pooToHeaders :: PutObjectOptions -> [HT.Header]
348−pooToHeaders poo = userMetadata
349− ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
350− ++ maybe [] toPutObjectHeaders (pooSSE poo)
384+pooToHeaders poo =
385+ userMetadata
386+ ++ mapMaybe tupToMaybe (zip names values)
387+ ++ maybe [] toPutObjectHeaders (pooSSE poo)
351388 where
352− tupToMaybe (k, Just v) = Just (k, v)
389+ tupToMaybe (k, Just v) = Just (k, v)
353390 tupToMaybe (_, Nothing) = Nothing
354−
355391 userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
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−
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+ ]
368410
369411-- |
370412-- BucketInfo returned for list buckets call
371−data BucketInfo = BucketInfo {
372− biName :: Bucket
373− , biCreationDate :: UTCTime
374− } deriving (Show, Eq)
413+data BucketInfo = BucketInfo
414+ { biName :: Bucket,
415+ biCreationDate :: UTCTime
416+ }
417+ deriving stock (Show, Eq)
375418
376419-- | A type alias to represent a part-number for multipart upload
377420type PartNumber = Int16
378421
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+
379445-- | A type alias to represent an upload-id for multipart upload
380446type UploadId = Text
381447
@@ -384,88 +450,116 @@ type PartTuple = (PartNumber, ETag)
384450
385451-- | Represents result from a listing of object parts of an ongoing
386452-- multipart upload.
387−data ListPartsResult = ListPartsResult {
388− lprHasMore :: Bool
389− , lprNextPart :: Maybe Int
390− , lprParts :: [ObjectPartInfo]
391− } deriving (Show, Eq)
453+data ListPartsResult = ListPartsResult
454+ { lprHasMore :: Bool,
455+ lprNextPart :: Maybe Int,
456+ lprParts :: [ObjectPartInfo]
457+ }
458+ deriving stock (Show, Eq)
392459
393460-- | Represents information about an object part in an ongoing
394461-- multipart upload.
395−data ObjectPartInfo = ObjectPartInfo {
396− opiNumber :: PartNumber
397− , opiETag :: ETag
398− , opiSize :: Int64
399− , opiModTime :: UTCTime
400− } deriving (Show, Eq)
462+data ObjectPartInfo = ObjectPartInfo
463+ { opiNumber :: PartNumber,
464+ opiETag :: ETag,
465+ opiSize :: Int64,
466+ opiModTime :: UTCTime
467+ }
468+ deriving stock (Show, Eq)
401469
402470-- | Represents result from a listing of incomplete uploads to a
403471-- bucket.
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)
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)
411480
412481-- | Represents information about a multipart upload.
413−data UploadInfo = UploadInfo {
414− uiKey :: Object
415− , uiUploadId :: UploadId
416− , uiInitTime :: UTCTime
417− , uiSize :: Int64
418− } deriving (Show, Eq)
482+data UploadInfo = UploadInfo
483+ { uiKey :: Object,
484+ uiUploadId :: UploadId,
485+ uiInitTime :: UTCTime,
486+ uiSize :: Int64
487+ }
488+ deriving stock (Show, Eq)
419489
420490-- | Represents result from a listing of objects in a bucket.
421−data ListObjectsResult = ListObjectsResult {
422− lorHasMore :: Bool
423− , lorNextToken :: Maybe Text
424− , lorObjects :: [ObjectInfo]
425− , lorCPrefixes :: [Text]
426− } deriving (Show, Eq)
491+data ListObjectsResult = ListObjectsResult
492+ { lorHasMore :: Bool,
493+ lorNextToken :: Maybe Text,
494+ lorObjects :: [ObjectInfo],
495+ lorCPrefixes :: [Text]
496+ }
497+ deriving stock (Show, Eq)
427498
428499-- | Represents result from a listing of objects version 1 in a bucket.
429−data ListObjectsV1Result = ListObjectsV1Result {
430− lorHasMore' :: Bool
431− , lorNextMarker :: Maybe Text
432− , lorObjects' :: [ObjectInfo]
433− , lorCPrefixes' :: [Text]
434− } deriving (Show, Eq)
500+data ListObjectsV1Result = ListObjectsV1Result
501+ { lorHasMore' :: Bool,
502+ lorNextMarker :: Maybe Text,
503+ lorObjects' :: [ObjectInfo],
504+ lorCPrefixes' :: [Text]
505+ }
506+ deriving stock (Show, Eq)
435507
436508-- | Represents information about an object.
437509data ObjectInfo = ObjectInfo
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)
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)
445531
446532-- | Represents source object in server-side copy object
447533data SourceInfo = SourceInfo
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)
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)
469563
470564-- | Provide a default for `SourceInfo`
471565defaultSourceInfo :: SourceInfo
@@ -473,9 +567,12 @@ defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing
473567
474568-- | Represents destination object in server-side copy object
475569data DestinationInfo = DestinationInfo
476− { dstBucket :: Text -- ^ Destination bucket
477− , dstObject :: Text -- ^ Destination object key
478− } deriving (Show, Eq)
570+ { -- | Destination bucket
571+ dstBucket :: Text,
572+ -- | Destination object key
573+ dstObject :: Text
574+ }
575+ deriving stock (Show, Eq)
479576
480577-- | Provide a default for `DestinationInfo`
481578defaultDestinationInfo :: DestinationInfo
@@ -484,88 +581,106 @@ defaultDestinationInfo = DestinationInfo "" ""
484581-- | Data type for options when getting an object from the
485582-- service. Start with the empty `defaultGetObjectOptions` and modify
486583-- it using the goo* functions.
487−data GetObjectOptions = GetObjectOptions {
488− -- | Set object's data of given offset begin and end,
584+data GetObjectOptions = GetObjectOptions
585+ { -- | Set object's data of given offset begin and end,
489586 -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
490− gooRange :: Maybe ByteRange
587+ gooRange :: Maybe ByteRange,
491588 -- | Set matching ETag condition, GetObject which matches the following
492589 -- ETag.
493− , gooIfMatch :: Maybe ETag
590+ gooIfMatch :: Maybe ETag,
494591 -- | Set matching ETag none condition, GetObject which does not match
495592 -- the following ETag.
496− , gooIfNoneMatch :: Maybe ETag
593+ gooIfNoneMatch :: Maybe ETag,
497594 -- | Set object unmodified condition, GetObject unmodified since given time.
498− , gooIfUnmodifiedSince :: Maybe UTCTime
595+ gooIfUnmodifiedSince :: Maybe UTCTime,
499596 -- | Set object modified condition, GetObject modified since given time.
500− , gooIfModifiedSince :: Maybe UTCTime
597+ gooIfModifiedSince :: Maybe UTCTime,
501598 -- | Specify SSE-C key
502− , gooSSECKey :: Maybe SSECKey
599+ gooSSECKey :: Maybe SSECKey
503600 }
504601
505602-- | Provide default `GetObjectOptions`.
506603defaultGetObjectOptions :: GetObjectOptions
507604defaultGetObjectOptions =
508− GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
605+ GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
509606
510607gooToHeaders :: GetObjectOptions -> [HT.Header]
511−gooToHeaders goo = rangeHdr ++ zip names values
512− ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
608+gooToHeaders goo =
609+ rangeHdr
610+ ++ zip names values
611+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
513612 where
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−
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+ }
525639
526640-- | A data-type for events that can occur in the object storage
527641-- server. Reference:
528642-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
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"
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"
550665
551666textToEvent :: Text -> Maybe Event
552667textToEvent t = case t of
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
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
560675 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
561− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
562− _ -> Nothing
563−
676+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
677+ _ -> Nothing
564678
565679-- | Filter data type - part of notification configuration
566−data Filter = Filter
680+newtype Filter = Filter
567681 { fFilter :: FilterKey
568− } deriving (Show, Eq)
682+ }
683+ deriving stock (Show, Eq)
569684
570685-- | defaultFilter is empty, used to create a notification
571686-- configuration.
@@ -573,9 +688,10 @@ defaultFilter :: Filter
573688defaultFilter = Filter defaultFilterKey
574689
575690-- | FilterKey contains FilterRules, and is part of a Filter.
576−data FilterKey = FilterKey
691+newtype FilterKey = FilterKey
577692 { fkKey :: FilterRules
578− } deriving (Show, Eq)
693+ }
694+ deriving stock (Show, Eq)
579695
580696-- | defaultFilterKey is empty, used to create notification
581697-- configuration.
@@ -583,16 +699,16 @@ defaultFilterKey :: FilterKey
583699defaultFilterKey = FilterKey defaultFilterRules
584700
585701-- | FilterRules represents a collection of `FilterRule`s.
586−data FilterRules = FilterRules
702+newtype FilterRules = FilterRules
587703 { frFilterRules :: [FilterRule]
588− } deriving (Show, Eq)
704+ }
705+ deriving stock (Show, Eq)
589706
590707-- | defaultFilterRules is empty, used to create notification
591708-- configuration.
592709defaultFilterRules :: FilterRules
593710defaultFilterRules = FilterRules []
594711
595−
596712-- | A filter rule that can act based on the suffix or prefix of an
597713-- object. As an example, let's create two filter rules:
598714--
@@ -603,9 +719,10 @@ defaultFilterRules = FilterRules []
603719-- for objects having a suffix of ".jpg", and the @prefixRule@
604720-- restricts it to objects having a prefix of "images/".
605721data FilterRule = FilterRule
606− { frName :: Text
607− , frValue :: Text
608− } deriving (Show, Eq)
722+ { frName :: Text,
723+ frValue :: Text
724+ }
725+ deriving stock (Show, Eq)
609726
610727-- | Arn is an alias of Text
611728type Arn = Text
@@ -614,11 +731,12 @@ type Arn = Text
614731-- notification system. It could represent a Queue, Topic or Lambda
615732-- Function configuration.
616733data NotificationConfig = NotificationConfig
617− { ncId :: Text
618− , ncArn :: Arn
619− , ncEvents :: [Event]
620− , ncFilter :: Filter
621− } deriving (Show, Eq)
734+ { ncId :: Text,
735+ ncArn :: Arn,
736+ ncEvents :: [Event],
737+ ncFilter :: Filter
738+ }
739+ deriving stock (Show, Eq)
622740
623741-- | A data-type to represent bucket notification configuration. It is
624742-- a collection of queue, topic or lambda function configurations. The
@@ -626,16 +744,16 @@ data NotificationConfig = NotificationConfig
626744-- described at
627745-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
628746data Notification = Notification
629− { nQueueConfigurations :: [NotificationConfig]
630− , nTopicConfigurations :: [NotificationConfig]
631− , nCloudFunctionConfigurations :: [NotificationConfig]
632− } deriving (Eq, Show)
747+ { nQueueConfigurations :: [NotificationConfig],
748+ nTopicConfigurations :: [NotificationConfig],
749+ nCloudFunctionConfigurations :: [NotificationConfig]
750+ }
751+ deriving stock (Show, Eq)
633752
634753-- | The default notification configuration is empty.
635754defaultNotification :: Notification
636755defaultNotification = Notification [] [] []
637756
638−
639757--------------------------------------------------------------------------
640758-- Select API Related Types
641759--------------------------------------------------------------------------
@@ -643,38 +761,42 @@ defaultNotification = Notification [] [] []
643761-- | SelectRequest represents the Select API call. Use the
644762-- `selectRequest` function to create a value of this type.
645763data SelectRequest = SelectRequest
646− { srExpression :: Text
647− , srExpressionType :: ExpressionType
648− , srInputSerialization :: InputSerialization
649− , srOutputSerialization :: OutputSerialization
650− , srRequestProgressEnabled :: Maybe Bool
651− } deriving (Eq, Show)
764+ { srExpression :: Text,
765+ srExpressionType :: ExpressionType,
766+ srInputSerialization :: InputSerialization,
767+ srOutputSerialization :: OutputSerialization,
768+ srRequestProgressEnabled :: Maybe Bool
769+ }
770+ deriving stock (Show, Eq)
652771
653772data ExpressionType = SQL
654− deriving (Eq, Show)
773+ deriving stock (Show, Eq)
655774
656775-- | InputSerialization represents format information of the input
657776-- object being queried. Use one of the smart constructors such as
658777-- `defaultCsvInput` as a starting value, and add compression info
659778-- using `setInputCompressionType`
660779data InputSerialization = InputSerialization
661− { isCompressionType :: Maybe CompressionType
662− , isFormatInfo :: InputFormatInfo
663− } deriving (Eq, Show)
780+ { isCompressionType :: Maybe CompressionType,
781+ isFormatInfo :: InputFormatInfo
782+ }
783+ deriving stock (Show, Eq)
664784
665785-- | Data type representing the compression setting in a Select
666786-- request.
667−data CompressionType = CompressionTypeNone
668− | CompressionTypeGzip
669− | CompressionTypeBzip2
670− deriving (Eq, Show)
787+data CompressionType
788+ = CompressionTypeNone
789+ | CompressionTypeGzip
790+ | CompressionTypeBzip2
791+ deriving stock (Show, Eq)
671792
672793-- | Data type representing input object format information in a
673794-- Select request.
674−data InputFormatInfo = InputFormatCSV CSVInputProp
675− | InputFormatJSON JSONInputProp
676− | InputFormatParquet
677− deriving (Eq, Show)
795+data InputFormatInfo
796+ = InputFormatCSV CSVInputProp
797+ | InputFormatJSON JSONInputProp
798+ | InputFormatParquet
799+ deriving stock (Show, Eq)
678800
679801-- | defaultCsvInput returns InputSerialization with default CSV
680802-- format, and without any compression setting.
@@ -684,14 +806,18 @@ defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
684806-- | linesJsonInput returns InputSerialization with JSON line based
685807-- format with no compression setting.
686808linesJsonInput :: InputSerialization
687−linesJsonInput = InputSerialization Nothing
688− (InputFormatJSON $ JSONInputProp JSONTypeLines)
809+linesJsonInput =
810+ InputSerialization
811+ Nothing
812+ (InputFormatJSON $ JSONInputProp JSONTypeLines)
689813
690814-- | documentJsonInput returns InputSerialization with JSON document
691815-- based format with no compression setting.
692816documentJsonInput :: InputSerialization
693−documentJsonInput = InputSerialization Nothing
694− (InputFormatJSON $ JSONInputProp JSONTypeDocument)
817+documentJsonInput =
818+ InputSerialization
819+ Nothing
820+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)
695821
696822-- | defaultParquetInput returns InputSerialization with Parquet
697823-- format, and no compression setting.
@@ -700,12 +826,14 @@ defaultParquetInput = InputSerialization Nothing InputFormatParquet
700826
701827-- | setInputCompressionType sets the compression type for the input
702828-- of the SelectRequest
703−setInputCompressionType :: CompressionType -> SelectRequest
704− -> SelectRequest
829+setInputCompressionType ::
830+ CompressionType ->
831+ SelectRequest ->
832+ SelectRequest
705833setInputCompressionType c i =
706− let is = srInputSerialization i
707− is' = is { isCompressionType = Just c }
708− in i { srInputSerialization = is' }
834+ let is = srInputSerialization i
835+ is' = is {isCompressionType = Just c}
836+ in i {srInputSerialization = is'}
709837
710838-- | defaultCsvOutput returns OutputSerialization with default CSV
711839-- format.
@@ -722,40 +850,42 @@ defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
722850-- SelectRequest with the SQL query text given by @query@, the input
723851-- serialization settings (compression format and format information)
724852-- @inputSer@ and the output serialization settings @outputSer@.
725−selectRequest :: Text -> InputSerialization -> OutputSerialization
726− -> SelectRequest
853+selectRequest ::
854+ Text ->
855+ InputSerialization ->
856+ OutputSerialization ->
857+ SelectRequest
727858selectRequest sqlQuery inputSer outputSer =
728− SelectRequest { srExpression = sqlQuery
729− , srExpressionType = SQL
730− , srInputSerialization = inputSer
731− , srOutputSerialization = outputSer
732− , srRequestProgressEnabled = Nothing
733− }
859+ SelectRequest
860+ { srExpression = sqlQuery,
861+ srExpressionType = SQL,
862+ srInputSerialization = inputSer,
863+ srOutputSerialization = outputSer,
864+ srRequestProgressEnabled = Nothing
865+ }
734866
735867-- | setRequestProgressEnabled sets the flag for turning on progress
736868-- messages when the Select response is being streamed back to the
737869-- client.
738870setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
739871setRequestProgressEnabled enabled sr =
740− sr { srRequestProgressEnabled = Just enabled }
872+ sr {srRequestProgressEnabled = Just enabled}
741873
742874type CSVInputProp = CSVProp
743875
744876-- | CSVProp represents CSV format properties. It is built up using
745877-- the Monoid instance.
746−data CSVProp = CSVProp (H.HashMap Text Text)
747− deriving (Eq, Show)
878+newtype CSVProp = CSVProp (H.HashMap Text Text)
879+ deriving stock (Show, Eq)
748880
749−#if (__GLASGOW_HASKELL__ >= 804)
750881instance Semigroup CSVProp where
751− (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
752−#endif
882+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
753883
754884instance Monoid CSVProp where
755− mempty = CSVProp mempty
756−#if (__GLASGOW_HASKELL__ < 804)
757− mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
758−#endif
885+ mempty = CSVProp mempty
886+
887+csvPropsList :: CSVProp -> [(Text, Text)]
888+csvPropsList (CSVProp h) = sort $ H.toList h
759889
760890defaultCSVProp :: CSVProp
761891defaultCSVProp = mempty
@@ -779,18 +909,21 @@ quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
779909-- | FileHeaderInfo specifies information about column headers for CSV
780910-- format.
781911data FileHeaderInfo
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)
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)
786919
787920-- | Specify the CSV file header info property.
788921fileHeaderInfo :: FileHeaderInfo -> CSVProp
789−fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString
922+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toStr
790923 where
791− toString FileHeaderNone = "NONE"
792− toString FileHeaderUse = "USE"
793− toString FileHeaderIgnore = "IGNORE"
924+ toStr FileHeaderNone = "NONE"
925+ toStr FileHeaderUse = "USE"
926+ toStr FileHeaderIgnore = "IGNORE"
794927
795928-- | Specify the CSV comment character property. Lines starting with
796929-- this character are ignored by the server.
@@ -803,64 +936,69 @@ allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter"
803936
804937-- | Set the CSV format properties in the InputSerialization.
805938setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
806−setInputCSVProps p is = is { isFormatInfo = InputFormatCSV p }
939+setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
807940
808941-- | Set the CSV format properties in the OutputSerialization.
809942outputCSVFromProps :: CSVProp -> OutputSerialization
810−outputCSVFromProps p = OutputSerializationCSV p
943+outputCSVFromProps = OutputSerializationCSV
811944
812−data JSONInputProp = JSONInputProp { jsonipType :: JSONType }
813− deriving (Eq, Show)
945+newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
946+ deriving stock (Show, Eq)
814947
815948data JSONType = JSONTypeDocument | JSONTypeLines
816− deriving (Eq, Show)
817−
949+ deriving stock (Show, Eq)
818950
819951-- | OutputSerialization represents output serialization settings for
820952-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
821953-- a starting point.
822−data OutputSerialization = OutputSerializationJSON JSONOutputProp
823− | OutputSerializationCSV CSVOutputProp
824− deriving (Eq, Show)
954+data OutputSerialization
955+ = OutputSerializationJSON JSONOutputProp
956+ | OutputSerializationCSV CSVOutputProp
957+ deriving stock (Show, Eq)
825958
826959type CSVOutputProp = CSVProp
827960
828961-- | quoteFields is an output serialization parameter
829962quoteFields :: QuoteFields -> CSVProp
830−quoteFields q = CSVProp $ H.singleton "QuoteFields" $
831− case q of
832− QuoteFieldsAsNeeded -> "ASNEEDED"
833− QuoteFieldsAlways -> "ALWAYS"
963+quoteFields q = CSVProp $
964+ H.singleton "QuoteFields" $
965+ case q of
966+ QuoteFieldsAsNeeded -> "ASNEEDED"
967+ QuoteFieldsAlways -> "ALWAYS"
834968
835969-- | Represent the QuoteField setting.
836970data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
837− deriving (Eq, Show)
971+ deriving stock (Show, Eq)
838972
839−data JSONOutputProp = JSONOutputProp { jsonopRecordDelimiter :: Maybe Text }
840− deriving (Eq, Show)
973+newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
974+ deriving stock (Show, Eq)
841975
842976-- | Set the output record delimiter for JSON format
843977outputJSONFromRecordDelimiter :: Text -> OutputSerialization
844978outputJSONFromRecordDelimiter t =
845− OutputSerializationJSON (JSONOutputProp $ Just t)
979+ OutputSerializationJSON (JSONOutputProp $ Just t)
846980
847981-- Response related types
848982
849983-- | An EventMessage represents each kind of message received from the server.
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)
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)
8641002
8651003msgHeaderValueType :: Word8
8661004msgHeaderValueType = 7
@@ -868,11 +1006,12 @@ msgHeaderValueType = 7
8681006type MessageHeader = (MsgHeaderName, Text)
8691007
8701008-- | Represent the progress event returned in the Select response.
871−data Progress = Progress { pBytesScanned :: Int64
872− , pBytesProcessed :: Int64
873− , pBytesReturned :: Int64
874− }
875− deriving (Eq, Show)
1009+data Progress = Progress
1010+ { pBytesScanned :: Int64,
1011+ pBytesProcessed :: Int64,
1012+ pBytesReturned :: Int64
1013+ }
1014+ deriving stock (Show, Eq)
8761015
8771016-- | Represent the stats event returned at the end of the Select
8781017-- response.
@@ -885,86 +1024,89 @@ type Stats = Progress
8851024-- | Represents different kinds of payload that are used with S3 API
8861025-- requests.
8871026data Payload
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
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
8911030
8921031defaultPayload :: Payload
8931032defaultPayload = PayloadBS ""
8941033
895−data AdminReqInfo = AdminReqInfo {
896− ariMethod :: Method
897− , ariPayloadHash :: Maybe ByteString
898− , ariPayload :: Payload
899− , ariPath :: ByteString
900− , ariHeaders :: [Header]
901− , ariQueryParams :: Query
1034+data AdminReqInfo = AdminReqInfo
1035+ { ariMethod :: Method,
1036+ ariPayloadHash :: Maybe ByteString,
1037+ ariPayload :: Payload,
1038+ ariPath :: ByteString,
1039+ ariHeaders :: [Header],
1040+ ariQueryParams :: Query
9021041 }
9031042
9041043data S3ReqInfo = S3ReqInfo
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
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
9141054 }
9151055
9161056defaultS3ReqInfo :: S3ReqInfo
917−defaultS3ReqInfo = S3ReqInfo HT.methodGet Nothing Nothing
918− [] [] defaultPayload Nothing Nothing True
1057+defaultS3ReqInfo =
1058+ S3ReqInfo
1059+ HT.methodGet
1060+ Nothing
1061+ Nothing
1062+ []
1063+ []
1064+ defaultPayload
1065+ Nothing
1066+ Nothing
1067+ True
1068+ Nothing
9191069
9201070getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
9211071getS3Path b o =
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
1072+ let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
1073+ in B.concat ["/", B.intercalate "/" segments]
9291074
930−type RegionMap = Map.Map Bucket Region
1075+type RegionMap = H.HashMap Bucket Region
9311076
9321077-- | The Minio Monad - all computations accessing object storage
9331078-- happens in it.
934−newtype Minio a = Minio {
935− unMinio :: ReaderT MinioConn (ResourceT IO) a
1079+newtype Minio a = Minio
1080+ { unMinio :: ReaderT MinioConn (ResourceT IO) a
9361081 }
937− deriving (
938− Functor
939− , Applicative
940− , Monad
941− , MonadIO
942− , MonadReader MinioConn
943− , MonadResource
1082+ deriving newtype
1083+ ( Functor,
1084+ Applicative,
1085+ Monad,
1086+ MonadIO,
1087+ MonadReader MinioConn,
1088+ MonadResource,
1089+ MonadUnliftIO
9441090 )
9451091
946−instance MonadUnliftIO Minio where
947− askUnliftIO = Minio $ ReaderT $ \r ->
948− withUnliftIO $ \u ->
949− return (UnliftIO (unliftIO u . flip runReaderT r . unMinio))
950−
9511092-- | MinioConn holds connection info and a connection pool to allow
9521093-- for efficient resource re-use.
9531094data MinioConn = MinioConn
954− { mcConnInfo :: ConnectInfo
955− , mcConnManager :: NC.Manager
956− , mcRegionMap :: MVar RegionMap
1095+ { mcConnInfo :: ConnectInfo,
1096+ mcConnManager :: NC.Manager,
1097+ mcRegionMap :: MVar RegionMap
9571098 }
9581099
9591100class HasSvcNamespace env where
9601101 getSvcNamespace :: env -> Text
9611102
9621103instance HasSvcNamespace MinioConn where
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/"
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+ )
9681110
9691111-- | Takes connection information and returns a connection object to
9701112-- be passed to 'runMinio'. The returned value can be kept in the
@@ -972,11 +1114,16 @@ instance HasSvcNamespace MinioConn where
9721114-- object storage is accessed.
9731115connect :: ConnectInfo -> IO MinioConn
9741116connect ci = do
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
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
9801127 mgr <- NC.newManager settings
9811128 mkMinioConn ci mgr
9821129
@@ -984,35 +1131,61 @@ connect ci = do
9841131-- `MinioConn`. This reuses connections, but otherwise it is similar
9851132-- to `runMinio`.
9861133runMinioWith :: MinioConn -> Minio a -> IO (Either MinioErr a)
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
1134+runMinioWith conn m = runResourceT $ runMinioResWith conn m
9991135
10001136-- | Given `ConnectInfo` and a HTTP connection manager, create a
10011137-- `MinioConn`.
10021138mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
10031139mkMinioConn ci mgr = do
1004− rMapMVar <- M.newMVar Map.empty
1005− return $ MinioConn ci mgr rMapMVar
1140+ rMapMVar <- M.newMVar H.empty
1141+ return $ MinioConn ci mgr rMapMVar
10061142
10071143-- | Run the Minio action and return the result or an error.
10081144runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
10091145runMinio ci m = do
10101146 conn <- connect ci
1011− runMinioWith conn m
1147+ runResourceT $ runMinioResWith conn m
1148+
1149+-- | Similar to 'runMinioWith'. Allows applications to allocate/release
1150+-- its resources along side MinIO's internal resources.
1151+runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)
1152+runMinioResWith conn m =
1153+ flip runReaderT conn . unMinio $
1154+ fmap Right m
1155+ `U.catches` [ U.Handler handlerServiceErr,
1156+ U.Handler handlerHE,
1157+ U.Handler handlerFE,
1158+ U.Handler handlerValidation
1159+ ]
1160+ where
1161+ handlerServiceErr = return . Left . MErrService
1162+ handlerHE = return . Left . MErrHTTP
1163+ handlerFE = return . Left . MErrIO
1164+ handlerValidation = return . Left . MErrValidation
10121165
1013−s3Name :: Text -> Text -> Name
1014−s3Name ns s = Name s (Just ns) Nothing
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
10151172
10161173-- | Format as per RFC 1123.
10171174formatRFC1123 :: UTCTime -> T.Text
10181175formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1176+
1177+lookupRegionCache :: Bucket -> Minio (Maybe Region)
1178+lookupRegionCache b = do
1179+ rMVar <- asks mcRegionMap
1180+ rMap <- UM.readMVar rMVar
1181+ return $ H.lookup b rMap
1182+
1183+addToRegionCache :: Bucket -> Region -> Minio ()
1184+addToRegionCache b region = do
1185+ rMVar <- asks mcRegionMap
1186+ UM.modifyMVar_ rMVar $ return . H.insert b region
1187+
1188+deleteFromRegionCache :: Bucket -> Minio ()
1189+deleteFromRegionCache b = do
1190+ rMVar <- asks mcRegionMap
1191+ UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/Data/ByteString.hs+19−18
@@ -13,23 +13,21 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−
1716{-# LANGUAGE FlexibleInstances #-}
17+
1818module Network.Minio.Data.ByteString
19− (
20− stripBS
21− , UriEncodable(..)
22− ) where
19+ ( stripBS,
20+ UriEncodable (..),
21+ )
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 (isSpace, toUpper, isAsciiUpper, isAsciiLower, isDigit)
28+import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace, toUpper)
2929import qualified Data.Text as T
30−import Numeric (showHex)
31−
32−import Lib.Prelude
30+import Numeric (showHex)
3331
3432stripBS :: ByteString -> ByteString
3533stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
@@ -39,8 +37,10 @@ class UriEncodable s where
3937
4038instance UriEncodable [Char] where
4139 uriEncode encodeSlash payload =
42− LB.toStrict $ BB.toLazyByteString $ mconcat $
43− map (`uriEncodeChar` encodeSlash) payload
40+ LB.toStrict $
41+ BB.toLazyByteString $
42+ mconcat $
43+ map (`uriEncodeChar` encodeSlash) payload
4444
4545instance UriEncodable ByteString where
4646 -- assumes that uriEncode is passed ASCII encoded strings.
@@ -59,16 +59,17 @@ 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 == '~') = BB.char7 ch
62+ || isAsciiLower ch
63+ || isDigit ch
64+ || (ch == '_')
65+ || (ch == '-')
66+ || (ch == '.')
67+ || (ch == '~') =
68+ BB.char7 ch
6869 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
6970 where
7071 f :: Word8 -> BB.Builder
7172 f n = BB.char7 '%' <> BB.string7 hexStr
7273 where
7374 hexStr = map toUpper $ showHex q $ showHex r ""
74− (q, r) = divMod (fromIntegral n) (16::Word8)
75+ (q, r) = divMod n (16 :: Word8)
src/Network/Minio/Data/Crypto.hs+32−33
@@ -15,55 +15,54 @@
1515--
1616
1717module Network.Minio.Data.Crypto
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
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
4342
4443hashSHA256 :: ByteString -> ByteString
4544hashSHA256 = digestToBase16 . hashWith SHA256
4645
47−hashSHA256FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
46+hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
4847hashSHA256FromSource src = do
4948 digest <- C.connect src sinkSHA256Hash
5049 return $ digestToBase16 digest
5150 where
5251 -- To help with type inference
53− sinkSHA256Hash :: Monad m => C.ConduitM ByteString Void m (Digest SHA256)
52+ sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
5453 sinkSHA256Hash = sinkHash
5554
5655-- Returns MD5 hash hex encoded.
5756hashMD5 :: ByteString -> ByteString
5857hashMD5 = digestToBase16 . hashWith MD5
5958
60−hashMD5FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
59+hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
6160hashMD5FromSource src = do
6261 digest <- C.connect src sinkMD5Hash
6362 return $ digestToBase16 digest
6463 where
6564 -- To help with type inference
66− sinkMD5Hash :: Monad m => C.ConduitM ByteString Void m (Digest MD5)
65+ sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
6766 sinkMD5Hash = sinkHash
6867
6968hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -72,15 +71,15 @@ hmacSHA256 message key = hmac key message
7271hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
7372hmacSHA256RawBS message key = convert $ hmacSHA256 message key
7473
75−digestToBS :: ByteArrayAccess a => a -> ByteString
74+digestToBS :: (ByteArrayAccess a) => a -> ByteString
7675digestToBS = convert
7776
78−digestToBase16 :: ByteArrayAccess a => a -> ByteString
77+digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
7978digestToBase16 = convertToBase Base16
8079
8180-- Returns MD5 hash base 64 encoded.
82−hashMD5ToBase64 :: ByteArrayAccess a => a -> ByteString
81+hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
8382hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
8483
85−encodeToBase64 :: ByteArrayAccess a => a -> ByteString
84+encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
8685encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+17−13
@@ -15,20 +15,24 @@
1515--
1616
1717module Network.Minio.Data.Time
18− (
19− awsTimeFormat
20− , awsTimeFormatBS
21− , awsDateFormat
22− , awsDateFormatBS
23− , awsParseTime
24− , iso8601TimeFormat
25− ) where
26−
27−
28−import Data.ByteString.Char8 (pack)
18+ ( awsTimeFormat,
19+ awsTimeFormatBS,
20+ awsDateFormat,
21+ awsDateFormatBS,
22+ awsParseTime,
23+ iso8601TimeFormat,
24+ UrlExpiry,
25+ )
26+where
27+
28+import Data.ByteString.Char8 (pack)
2929import qualified Data.Time as Time
30+import Data.Time.Format.ISO8601 (iso8601Show)
31+import Lib.Prelude
3032
31−import Lib.Prelude
33+-- | Time to expire for a presigned URL. It interpreted as a number of
34+-- seconds. The maximum duration that can be specified is 7 days.
35+type UrlExpiry = Int
3236
3337awsTimeFormat :: UTCTime -> [Char]
3438awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
@@ -46,4 +50,4 @@ awsParseTime :: [Char] -> Maybe UTCTime
4650awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
4751
4852iso8601TimeFormat :: UTCTime -> [Char]
49−iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
53+iso8601TimeFormat = iso8601Show
src/Network/Minio/Errors.hs+59−52
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,76 +14,83 @@
1414-- limitations under the License.
1515--
1616
17−module Network.Minio.Errors where
17+module Network.Minio.Errors
18+ ( MErrV (..),
19+ ServiceErr (..),
20+ MinioErr (..),
21+ toServiceErr,
22+ )
23+where
1824
19−import Control.Exception
25+import Control.Exception (IOException)
2026import qualified Network.HTTP.Conduit as NC
2127
22−import Lib.Prelude
23−
24−
2528---------------------------------
2629-- Errors
2730---------------------------------
31+
2832-- | Various validation errors
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)
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)
4854
4955instance Exception MErrV
5056
5157-- | Errors returned by S3 compatible service
52−data ServiceErr = BucketAlreadyExists
53− | BucketAlreadyOwnedByYou
54− | NoSuchBucket
55− | InvalidBucketName
56− | NoSuchKey
57− | SelectErr Text Text
58− | ServiceErr Text Text
59− deriving (Show, Eq)
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)
6067
6168instance Exception ServiceErr
6269
6370toServiceErr :: Text -> Text -> ServiceErr
64−toServiceErr "NoSuchKey" _ = NoSuchKey
65−toServiceErr "NoSuchBucket" _ = NoSuchBucket
66−toServiceErr "InvalidBucketName" _ = InvalidBucketName
71+toServiceErr "NoSuchKey" _ = NoSuchKey
72+toServiceErr "NoSuchBucket" _ = NoSuchBucket
73+toServiceErr "InvalidBucketName" _ = InvalidBucketName
6774toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
68−toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
69−toServiceErr code message = ServiceErr code message
70−
75+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
76+toServiceErr code message = ServiceErr code message
7177
7278-- | Errors thrown by the library
73−data MinioErr = MErrHTTP NC.HttpException
74− | MErrIO IOException
75− | MErrService ServiceErr
76− | MErrValidation MErrV
77− deriving (Show)
79+data MinioErr
80+ = MErrHTTP NC.HttpException
81+ | MErrIO IOException
82+ | MErrService ServiceErr
83+ | MErrValidation MErrV
84+ deriving stock (Show)
7885
7986instance Eq MinioErr where
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
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
8895
8996instance Exception MinioErr
src/Network/Minio/JsonParser.hs+23−16
@@ -15,28 +15,35 @@
1515--
1616
1717module Network.Minio.JsonParser
18− (
19− parseErrResponseJSON
20− ) where
18+ ( parseErrResponseJSON,
19+ )
20+where
2121
22−import Data.Aeson (FromJSON, eitherDecode, parseJSON,
23− withObject, (.:))
24−import qualified Data.Text as T
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
2532
26−import Lib.Prelude
33+data AdminErrJSON = AdminErrJSON
34+ { aeCode :: Text,
35+ aeMessage :: Text
36+ }
37+ deriving stock (Eq, Show)
2738
28−import Network.Minio.Errors
29−
30−data AdminErrJSON = AdminErrJSON { aeCode :: Text
31− , aeMessage :: Text
32− } deriving (Eq, Show)
3339instance FromJSON AdminErrJSON where
34− parseJSON = withObject "AdminErrJSON" $ \v -> AdminErrJSON
35− <$> v .: "Code"
36− <*> v .: "Message"
40+ parseJSON = withObject "AdminErrJSON" $ \v ->
41+ AdminErrJSON
42+ <$> v .: "Code"
43+ <*> v .: "Message"
3744
3845parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
3946parseErrResponseJSON jsondata =
4047 case eitherDecode jsondata of
4148 Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
42− Left err -> throwIO $ MErrVJsonParse $ T.pack err
49+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs+98−38
@@ -16,20 +16,50 @@
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−
23−import Lib.Prelude
24−
25−import Network.Minio.Data
26−import Network.Minio.S3API
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+ )
2756
2857-- | Represents a list output item - either an object or an object
2958-- prefix (i.e. a directory).
30−data ListItem = ListItemObject ObjectInfo
31− | ListItemPrefix Text
32− deriving (Show, Eq)
59+data ListItem
60+ = ListItemObject ObjectInfo
61+ | ListItemPrefix Text
62+ deriving stock (Show, Eq)
3363
3464-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
3565-- similar to a file system tree traversal.
@@ -48,73 +78,103 @@ listObjects bucket prefix recurse = loop Nothing
4878 where
4979 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
5080 loop nextToken = do
51− let
52− delimiter = bool (Just "/") Nothing recurse
81+ let delimiter = bool (Just "/") Nothing recurse
5382
5483 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
5584 CL.sourceList $ map ListItemObject $ lorObjects res
5685 unless recurse $
57− CL.sourceList $ map ListItemPrefix $ lorCPrefixes res
86+ CL.sourceList $
87+ map ListItemPrefix $
88+ lorCPrefixes res
5889 when (lorHasMore res) $
5990 loop (lorNextToken res)
6091
6192-- | Lists objects - similar to @listObjects@, however uses the older
6293-- V1 AWS S3 API. Prefer @listObjects@ to this.
63−listObjectsV1 :: Bucket -> Maybe Text -> Bool
64− -> C.ConduitM () ListItem Minio ()
94+listObjectsV1 ::
95+ Bucket ->
96+ Maybe Text ->
97+ Bool ->
98+ C.ConduitM () ListItem Minio ()
6599listObjectsV1 bucket prefix recurse = loop Nothing
66100 where
67101 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
68102 loop nextMarker = do
69− let
70− delimiter = bool (Just "/") Nothing recurse
103+ let delimiter = bool (Just "/") Nothing recurse
71104
72105 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
73106 CL.sourceList $ map ListItemObject $ lorObjects' res
74107 unless recurse $
75− CL.sourceList $ map ListItemPrefix $ lorCPrefixes' res
108+ CL.sourceList $
109+ map ListItemPrefix $
110+ lorCPrefixes' res
76111 when (lorHasMore' res) $
77112 loop (lorNextMarker res)
78113
79114-- | List incomplete uploads in a bucket matching the given prefix. If
80115-- recurse is set to True incomplete uploads for the given prefix are
81116-- recursively listed.
82−listIncompleteUploads :: Bucket -> Maybe Text -> Bool
83− -> C.ConduitM () UploadInfo Minio ()
117+listIncompleteUploads ::
118+ Bucket ->
119+ Maybe Text ->
120+ Bool ->
121+ C.ConduitM () UploadInfo Minio ()
84122listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
85123 where
86124 loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
87125 loop nextKeyMarker nextUploadIdMarker = do
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
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
98145
99146 CL.sourceList $
100− map (\((uKey, uId, uInitTime), size) ->
101− UploadInfo uKey uId uInitTime size
102− ) $ zip (lurUploads res) aggrSizes
147+ zipWith
148+ ( curry
149+ ( \((uKey, uId, uInitTime), size) ->
150+ UploadInfo uKey uId uInitTime size
151+ )
152+ )
153+ (lurUploads res)
154+ aggrSizes
103155
104156 when (lurHasMore res) $
105157 loop (lurNextKey res) (lurNextUpload res)
106158
107−
108159-- | List object parts of an ongoing multipart upload for given
109160-- bucket, object and uploadId.
110−listIncompleteParts :: Bucket -> Object -> UploadId
111− -> C.ConduitM () ObjectPartInfo Minio ()
161+listIncompleteParts ::
162+ Bucket ->
163+ Object ->
164+ UploadId ->
165+ C.ConduitM () ObjectPartInfo Minio ()
112166listIncompleteParts bucket object uploadId = loop Nothing
113167 where
114168 loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
115169 loop nextPartMarker = do
116− res <- lift $ listIncompleteParts' bucket object uploadId Nothing
117− nextPartMarker
170+ res <-
171+ lift $
172+ listIncompleteParts'
173+ bucket
174+ object
175+ uploadId
176+ Nothing
177+ nextPartMarker
118178 CL.sourceList $ lprParts res
119179 when (lprHasMore res) $
120180 loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs+215−152
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,45 +13,51 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16+{-# LANGUAGE CPP #-}
1617
1718module Network.Minio.PresignedOperations
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
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 -}
5561
5662-- | Generate a presigned URL. This function allows for advanced usage
5763-- - for simple cases prefer the `presigned*Url` functions.
@@ -61,42 +67,36 @@ import Network.Minio.Sign.V4
6167--
6268-- All extra query parameters or headers are signed, and therefore are
6369-- required to be sent when the generated URL is actually used.
64−makePresignedUrl :: UrlExpiry -> HT.Method -> Maybe Bucket -> Maybe Object
65− -> Maybe Region -> HT.Query -> HT.RequestHeaders
66− -> Minio ByteString
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
6779makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
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
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
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,12 +105,22 @@ 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 :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders
109− -> Minio ByteString
108+presignedPutObjectUrl ::
109+ Bucket ->
110+ Object ->
111+ UrlExpiry ->
112+ HT.RequestHeaders ->
113+ Minio ByteString
110114presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
111115 region <- asks (Just . connectRegion . mcConnInfo)
112− makePresignedUrl expirySeconds HT.methodPut
113− (Just bucket) (Just object) region [] extraHeaders
116+ makePresignedUrl
117+ expirySeconds
118+ HT.methodPut
119+ (Just bucket)
120+ (Just object)
121+ region
122+ []
123+ extraHeaders
114124
115125-- | Generate a URL with authentication signature to GET (download) an
116126-- object. All extra query parameters and headers passed here will be
@@ -121,12 +131,23 @@ presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
121131--
122132-- For a list of possible request parameters and headers, please refer
123133-- to the GET object REST API AWS S3 documentation.
124−presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query
125− -> HT.RequestHeaders -> Minio ByteString
134+presignedGetObjectUrl ::
135+ Bucket ->
136+ Object ->
137+ UrlExpiry ->
138+ HT.Query ->
139+ HT.RequestHeaders ->
140+ Minio ByteString
126141presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
127142 region <- asks (Just . connectRegion . mcConnInfo)
128− makePresignedUrl expirySeconds HT.methodGet
129− (Just bucket) (Just object) region extraQuery extraHeaders
143+ makePresignedUrl
144+ expirySeconds
145+ HT.methodGet
146+ (Just bucket)
147+ (Just object)
148+ region
149+ extraQuery
150+ extraHeaders
130151
131152-- | Generate a URL with authentication signature to make a HEAD
132153-- request on an object. This is used to fetch metadata about an
@@ -135,50 +156,74 @@ presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
135156--
136157-- For a list of possible headers to pass, please refer to the HEAD
137158-- object REST API AWS S3 documentation.
138−presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry
139− -> HT.RequestHeaders -> Minio ByteString
159+presignedHeadObjectUrl ::
160+ Bucket ->
161+ Object ->
162+ UrlExpiry ->
163+ HT.RequestHeaders ->
164+ Minio ByteString
140165presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
141166 region <- asks (Just . connectRegion . mcConnInfo)
142− makePresignedUrl expirySeconds HT.methodHead
143− (Just bucket) (Just object) region [] extraHeaders
167+ makePresignedUrl
168+ expirySeconds
169+ HT.methodHead
170+ (Just bucket)
171+ (Just object)
172+ region
173+ []
174+ extraHeaders
144175
145176-- | Represents individual conditions in a Post Policy document.
146−data PostPolicyCondition = PPCStartsWith Text Text
147− | PPCEquals Text Text
148− | PPCRange Text Int64 Int64
149− deriving (Show, Eq)
177+data PostPolicyCondition
178+ = PPCStartsWith Text Text
179+ | PPCEquals Text Text
180+ | PPCRange Text Int64 Int64
181+ deriving stock (Show, Eq)
150182
183+{- ORMOLU_DISABLE -}
151184instance Json.ToJSON PostPolicyCondition where
152185 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
153189 toJSON (PPCEquals k v) = Json.object [k .= v]
190+#endif
154191 toJSON (PPCRange k minVal maxVal) =
155192 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
156193
157194 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
158198 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
199+#endif
159200 toEncoding (PPCRange k minVal maxVal) =
160201 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202+{- ORMOLU_ENABLE -}
161203
162204-- | A PostPolicy is required to perform uploads via browser forms.
163−data PostPolicy = PostPolicy {
164− expiration :: UTCTime
165− , conditions :: [PostPolicyCondition]
166− } deriving (Show, Eq)
205+data PostPolicy = PostPolicy
206+ { expiration :: UTCTime,
207+ conditions :: [PostPolicyCondition]
208+ }
209+ deriving stock (Show, Eq)
167210
168211instance Json.ToJSON PostPolicy where
169212 toJSON (PostPolicy e c) =
170− Json.object $ [ "expiration" .= iso8601TimeFormat e
171− , "conditions" .= c
172− ]
213+ Json.object
214+ [ "expiration" .= iso8601TimeFormat e,
215+ "conditions" .= c
216+ ]
173217 toEncoding (PostPolicy e c) =
174218 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
175219
176220-- | Possible validation errors when creating a PostPolicy.
177−data PostPolicyError = PPEKeyNotSpecified
178− | PPEBucketNotSpecified
179− | PPEConditionKeyEmpty
180− | PPERangeInvalid
181− deriving (Eq, Show)
221+data PostPolicyError
222+ = PPEKeyNotSpecified
223+ | PPEBucketNotSpecified
224+ | PPEConditionKeyEmpty
225+ | PPERangeInvalid
226+ deriving stock (Show, Eq)
182227
183228-- | Set the bucket name that the upload should use.
184229ppCondBucket :: Bucket -> PostPolicyCondition
@@ -186,8 +231,10 @@ ppCondBucket = PPCEquals "bucket"
186231
187232-- | Set the content length range constraint with minimum and maximum
188233-- byte count values.
189−ppCondContentLengthRange :: Int64 -> Int64
190− -> PostPolicyCondition
234+ppCondContentLengthRange ::
235+ Int64 ->
236+ Int64 ->
237+ PostPolicyCondition
191238ppCondContentLengthRange = PPCRange "content-length-range"
192239
193240-- | Set the content-type header for the upload.
@@ -210,83 +257,99 @@ ppCondSuccessActionStatus n =
210257
211258-- | This function creates a PostPolicy after validating its
212259-- arguments.
213−newPostPolicy :: UTCTime -> [PostPolicyCondition]
214− -> Either PostPolicyError PostPolicy
260+newPostPolicy ::
261+ UTCTime ->
262+ [PostPolicyCondition] ->
263+ Either PostPolicyError PostPolicy
215264newPostPolicy expirationTime conds
216265 -- object name condition must be present
217266 | not $ any (keyEquals "key") conds =
218267 Left PPEKeyNotSpecified
219−
220268 -- bucket name condition must be present
221269 | not $ any (keyEquals "bucket") conds =
222270 Left PPEBucketNotSpecified
223−
224271 -- a condition with an empty key is invalid
225272 | any (keyEquals "") conds || any isEmptyRangeKey conds =
226273 Left PPEConditionKeyEmpty
227−
228274 -- invalid range check
229275 | any isInvalidRange conds =
230276 Left PPERangeInvalid
231−
232277 -- all good!
233278 | otherwise =
234279 return $ PostPolicy expirationTime conds
235−
236280 where
237281 keyEquals k' (PPCStartsWith k _) = k == k'
238− keyEquals k' (PPCEquals k _) = k == k'
239− keyEquals _ _ = False
240−
282+ keyEquals k' (PPCEquals k _) = k == k'
283+ keyEquals _ _ = False
241284 isEmptyRangeKey (PPCRange k _ _) = k == ""
242− isEmptyRangeKey _ = False
243−
285+ isEmptyRangeKey _ = False
244286 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
245− isInvalidRange _ = False
287+ isInvalidRange _ = False
246288
247289-- | Convert Post Policy to a string (e.g. for printing).
248290showPostPolicy :: PostPolicy -> ByteString
249−showPostPolicy = toS . Json.encode
291+showPostPolicy = toStrictBS . Json.encode
250292
251293-- | Generate a presigned URL and POST policy to upload files via a
252294-- browser. On success, this function returns a URL and POST
253295-- form-data.
254−presignedPostPolicy :: PostPolicy
255− -> Minio (ByteString, Map.Map Text ByteString)
296+presignedPostPolicy ::
297+ PostPolicy ->
298+ Minio (ByteString, H.HashMap Text ByteString)
256299presignedPostPolicy p = do
257300 ci <- asks mcConnInfo
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 "/"
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 "/"
291354
292355 return (url, formData)
src/Network/Minio/PutObject.hs+85−69
@@ -15,29 +15,24 @@
1515--
1616
1717module Network.Minio.PutObject
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
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
2928import qualified Data.Conduit.Combinators as CC
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−
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
4136
4237-- | A data-type to represent the source data for an object. A
4338-- file-path or a producer-conduit may be provided.
@@ -50,37 +45,45 @@ import Network.Minio.Utils
5045-- the input - if it is not provided, upload will continue until the
5146-- stream ends or the object reaches `maxObjectSize` size.
5247data ObjectData m
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.
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)
6057
6158-- | Put an object from ObjectData. This high-level API handles
6259-- objects of all sizes, and even if the object size is unknown.
63−putObjectInternal :: Bucket -> Object -> PutObjectOptions
64− -> ObjectData Minio -> Minio ETag
60+putObjectInternal ::
61+ Bucket ->
62+ Object ->
63+ PutObjectOptions ->
64+ ObjectData Minio ->
65+ Minio ETag
6566putObjectInternal b o opts (ODStream src sizeMay) = do
6667 case sizeMay of
6768 -- unable to get size, so assume non-seekable file
6869 Nothing -> sequentialMultipartUpload b o opts Nothing src
69−
7070 -- got file size, so check for single/multipart upload
7171 Just size ->
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−
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
7878putObjectInternal b o opts (ODFile fp sizeMay) = do
7979 hResE <- withNewHandle fp $ \h ->
80− liftM2 (,) (isHandleSeekable h) (getFileSize h)
80+ liftA2 (,) (isHandleSeekable h) (getFileSize h)
8181
82− (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return
83− hResE
82+ (isSeekable, handleSizeMay) <-
83+ either
84+ (const $ return (False, Nothing))
85+ return
86+ hResE
8487
8588 -- prefer given size to queried size.
8689 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
@@ -88,18 +91,25 @@ putObjectInternal b o opts (ODFile fp sizeMay) = do
8891 case finalSizeMay of
8992 -- unable to get size, so assume non-seekable file
9093 Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
91−
9294 -- got file size, so check for single/multipart upload
9395 Just size ->
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
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
103113parallelMultipartUpload b o opts filePath size = do
104114 -- get a new upload id.
105115 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -109,15 +119,17 @@ parallelMultipartUpload b o opts filePath size = do
109119 let threads = fromMaybe 10 $ pooNumThreads opts
110120
111121 -- perform upload with 'threads' threads
112− uploadedPartsE <- limitedMapConcurrently (fromIntegral threads)
113− (uploadPart uploadId) partSizeInfo
122+ uploadedPartsE <-
123+ limitedMapConcurrently
124+ (fromIntegral threads)
125+ (uploadPart uploadId)
126+ partSizeInfo
114127
115128 -- if there were any errors, rethrow exception.
116129 mapM_ throwIO $ lefts uploadedPartsE
117130
118131 -- if we get here, all parts were successfully uploaded.
119132 completeMultipartUpload b o uploadId $ rights uploadedPartsE
120−
121133 where
122134 uploadPart uploadId (partNum, offset, sz) =
123135 withNewHandle filePath $ \h -> do
@@ -125,10 +137,13 @@ parallelMultipartUpload b o opts filePath size = do
125137 putObjectPart b o uploadId partNum [] payload
126138
127139-- | Upload multipart object from conduit source sequentially
128−sequentialMultipartUpload :: Bucket -> Object -> PutObjectOptions
129− -> Maybe Int64
130− -> C.ConduitM () ByteString Minio ()
131− -> Minio ETag
140+sequentialMultipartUpload ::
141+ Bucket ->
142+ Object ->
143+ PutObjectOptions ->
144+ Maybe Int64 ->
145+ C.ConduitM () ByteString Minio () ->
146+ Minio ETag
132147sequentialMultipartUpload b o opts sizeMay src = do
133148 -- get a new upload id.
134149 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -136,22 +151,23 @@ sequentialMultipartUpload b o opts sizeMay src = do
136151 -- upload parts in loop
137152 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
138153 (pnums, _, sizes) = List.unzip3 partSizes
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
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
145161
146162 -- complete multipart upload
147163 completeMultipartUpload b o uploadId uploadedParts
148−
149164 where
150165 uploadPart' _ [] = return ()
151− uploadPart' uid (pn:pns) = do
166+ uploadPart' uid (pn : pns) = do
152167 payloadMay <- C.await
153168 case payloadMay of
154169 Nothing -> return ()
155− Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload
156− C.yield pinfo
157− uploadPart' uid pns
170+ Just payload -> do
171+ pinfo <- lift $ putObjectPart b o uid pn [] payload
172+ C.yield pinfo
173+ uploadPart' uid pns
src/Network/Minio/S3API.hs+486−314
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,133 +14,185 @@
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.
1725module Network.Minio.S3API
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
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))
112125
113126-- | Fetch all buckets from the service.
114127getService :: Minio [BucketInfo]
115128getService = do
116− resp <- executeRequest $ defaultS3ReqInfo {
117− riNeedsLocation = False
118− }
129+ resp <-
130+ executeRequest $
131+ defaultS3ReqInfo
132+ { riNeedsLocation = False
133+ }
119134 parseListBuckets $ NC.responseBody resp
120135
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 ())
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
125158getObject' bucket object queryParams headers = do
126159 resp <- mkStreamRequest reqInfo
127− return (NC.responseHeaders resp, NC.responseBody resp)
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+ }
128171 where
129− reqInfo = defaultS3ReqInfo { riBucket = Just bucket
130− , riObject = Just object
131− , riQueryParams = queryParams
132− , riHeaders = headers
133− }
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+ }
134184
135185-- | Creates a bucket via a PUT bucket call.
136186putBucket :: Bucket -> Region -> Minio ()
137187putBucket bucket location = do
138188 ns <- asks getSvcNamespace
139− void $ executeRequest $
140− defaultS3ReqInfo { riMethod = HT.methodPut
141− , riBucket = Just bucket
142− , riPayload = PayloadBS $ mkCreateBucketConfig ns location
143− , riNeedsLocation = False
189+ void $
190+ executeRequest $
191+ defaultS3ReqInfo
192+ { riMethod = HT.methodPut,
193+ riBucket = Just bucket,
194+ riPayload = PayloadBS $ mkCreateBucketConfig ns location,
195+ riNeedsLocation = False
144196 }
145197
146198-- | Single PUT object size.
@@ -156,321 +208,432 @@ putObjectSingle' bucket object headers bs = do
156208 let size = fromIntegral (BS.length bs)
157209 -- check length is within single PUT object size.
158210 when (size > maxSinglePutObjectSizeBytes) $
159− throwIO $ MErrVSinglePUTSizeExceeded size
211+ throwIO $
212+ MErrVSinglePUTSizeExceeded size
160213
161214 let payload = mkStreamingPayload $ PayloadBS bs
162− resp <- executeRequest $
163− defaultS3ReqInfo { riMethod = HT.methodPut
164− , riBucket = Just bucket
165− , riObject = Just object
166− , riHeaders = headers
167− , riPayload = payload
168− }
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+ }
169224
170225 let rheaders = NC.responseHeaders resp
171226 etag = getETagHeader rheaders
172227 maybe
173228 (throwIO MErrVETagHeaderNotFound)
174− return etag
229+ return
230+ etag
175231
176232-- | PUT an object into the service. This function performs a single
177233-- PUT object call, and so can only transfer objects upto 5GiB.
178−putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
179− -> Int64 -> Minio ETag
234+putObjectSingle ::
235+ Bucket ->
236+ Object ->
237+ [HT.Header] ->
238+ Handle ->
239+ Int64 ->
240+ Int64 ->
241+ Minio ETag
180242putObjectSingle bucket object headers h offset size = do
181243 -- check length is within single PUT object size.
182244 when (size > maxSinglePutObjectSizeBytes) $
183− throwIO $ MErrVSinglePUTSizeExceeded size
245+ throwIO $
246+ MErrVSinglePUTSizeExceeded size
184247
185248 -- content-length header is automatically set by library.
186249 let payload = mkStreamingPayload $ PayloadH h offset size
187− resp <- executeRequest $
188− defaultS3ReqInfo { riMethod = HT.methodPut
189− , riBucket = Just bucket
190− , riObject = Just object
191− , riHeaders = headers
192− , riPayload = payload
193− }
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+ }
194259
195260 let rheaders = NC.responseHeaders resp
196261 etag = getETagHeader rheaders
197262 maybe
198263 (throwIO MErrVETagHeaderNotFound)
199− return etag
264+ return
265+ etag
200266
201267-- | List objects in a bucket matching prefix up to delimiter,
202268-- starting from nextMarker.
203−listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
204− -> Minio ListObjectsV1Result
269+listObjectsV1' ::
270+ Bucket ->
271+ Maybe Text ->
272+ Maybe Text ->
273+ Maybe Text ->
274+ Maybe Int ->
275+ Minio ListObjectsV1Result
205276listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
206− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
207− , riBucket = Just bucket
208− , riQueryParams = mkOptionalParams params
209− }
277+ resp <-
278+ executeRequest $
279+ defaultS3ReqInfo
280+ { riMethod = HT.methodGet,
281+ riBucket = Just bucket,
282+ riQueryParams = mkOptionalParams params
283+ }
210284 parseListObjectsV1Response $ NC.responseBody resp
211285 where
212− params = [
213− ("marker", nextMarker)
214− , ("prefix", prefix)
215− , ("delimiter", delimiter)
216− , ("max-keys", show <$> maxKeys)
286+ params =
287+ [ ("marker", nextMarker),
288+ ("prefix", prefix),
289+ ("delimiter", delimiter),
290+ ("max-keys", show <$> maxKeys)
217291 ]
218292
219293-- | List objects in a bucket matching prefix up to delimiter,
220294-- starting from nextToken.
221−listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
222− -> Minio ListObjectsResult
295+listObjects' ::
296+ Bucket ->
297+ Maybe Text ->
298+ Maybe Text ->
299+ Maybe Text ->
300+ Maybe Int ->
301+ Minio ListObjectsResult
223302listObjects' bucket prefix nextToken delimiter maxKeys = do
224− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
225− , riBucket = Just bucket
226− , riQueryParams = mkOptionalParams params
227− }
303+ resp <-
304+ executeRequest $
305+ defaultS3ReqInfo
306+ { riMethod = HT.methodGet,
307+ riBucket = Just bucket,
308+ riQueryParams = mkOptionalParams params
309+ }
228310 parseListObjectsResponse $ NC.responseBody resp
229311 where
230− params = [
231− ("list-type", Just "2")
232− , ("continuation_token", nextToken)
233− , ("prefix", prefix)
234− , ("delimiter", delimiter)
235− , ("max-keys", show <$> maxKeys)
312+ params =
313+ [ ("list-type", Just "2"),
314+ ("continuation_token", nextToken),
315+ ("prefix", prefix),
316+ ("delimiter", delimiter),
317+ ("max-keys", show <$> maxKeys)
236318 ]
237319
238320-- | DELETE a bucket from the service.
239321deleteBucket :: Bucket -> Minio ()
240−deleteBucket bucket = void $
241− executeRequest $
242− defaultS3ReqInfo { riMethod = HT.methodDelete
243− , riBucket = Just bucket
322+deleteBucket bucket =
323+ void $
324+ executeRequest $
325+ defaultS3ReqInfo
326+ { riMethod = HT.methodDelete,
327+ riBucket = Just bucket
244328 }
245329
246330-- | DELETE an object from the service.
247331deleteObject :: Bucket -> Object -> Minio ()
248−deleteObject bucket object = void $
249− executeRequest $
250− defaultS3ReqInfo { riMethod = HT.methodDelete
251− , riBucket = Just bucket
252− , riObject = Just object
332+deleteObject bucket object =
333+ void $
334+ executeRequest $
335+ defaultS3ReqInfo
336+ { riMethod = HT.methodDelete,
337+ riBucket = Just bucket,
338+ riObject = Just object
253339 }
254340
255341-- | Create a new multipart upload.
256342newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
257343newMultipartUpload bucket object headers = do
258− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPost
259− , riBucket = Just bucket
260− , riObject = Just object
261− , riQueryParams = [("uploads", Nothing)]
262− , riHeaders = headers
263− }
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+ }
264353 parseNewMultipartUpload $ NC.responseBody resp
265354
266355-- | PUT a part of an object as part of a multipart upload.
267−putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
268− -> Payload -> Minio PartTuple
356+putObjectPart ::
357+ Bucket ->
358+ Object ->
359+ UploadId ->
360+ PartNumber ->
361+ [HT.Header] ->
362+ Payload ->
363+ Minio PartTuple
269364putObjectPart bucket object uploadId partNumber headers payload = do
270365 -- transform payload to conduit to enable streaming signature
271366 let payload' = mkStreamingPayload payload
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− }
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+ }
280377 let rheaders = NC.responseHeaders resp
281378 etag = getETagHeader rheaders
282379 maybe
283380 (throwIO MErrVETagHeaderNotFound)
284− (return . (partNumber, )) etag
381+ (return . (partNumber,))
382+ etag
285383 where
286− params = [
287− ("uploadId", Just uploadId)
288− , ("partNumber", Just $ show partNumber)
384+ params =
385+ [ ("uploadId", Just uploadId),
386+ ("partNumber", Just $ show partNumber)
289387 ]
290388
291389srcInfoToHeaders :: SourceInfo -> [HT.Header]
292−srcInfoToHeaders srcInfo = ("x-amz-copy-source",
293− toS $ T.concat ["/", srcBucket srcInfo,
294− "/", srcObject srcInfo]
295− ) : rangeHdr ++ zip names values
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
296402 where
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
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)
306419 toByteRange :: (Int64, Int64) -> HT.ByteRange
307420 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
308421
309422-- | Performs server-side copy of an object or part of an object as an
310423-- upload part of an ongoing multi-part upload.
311−copyObjectPart :: DestinationInfo -> SourceInfo -> UploadId
312− -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
424+copyObjectPart ::
425+ DestinationInfo ->
426+ SourceInfo ->
427+ UploadId ->
428+ PartNumber ->
429+ [HT.Header] ->
430+ Minio (ETag, UTCTime)
313431copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
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− }
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+ }
321441
322442 parseCopyObjectResponse $ NC.responseBody resp
323443 where
324− params = [
325− ("uploadId", Just uploadId)
326− , ("partNumber", Just $ show partNumber)
444+ params =
445+ [ ("uploadId", Just uploadId),
446+ ("partNumber", Just $ show partNumber)
327447 ]
328448
329449-- | Performs server-side copy of an object that is upto 5GiB in
330450-- size. If the object is greater than 5GiB, this function throws the
331451-- error returned by the server.
332−copyObjectSingle :: Bucket -> Object -> SourceInfo -> [HT.Header]
333− -> Minio (ETag, UTCTime)
452+copyObjectSingle ::
453+ Bucket ->
454+ Object ->
455+ SourceInfo ->
456+ [HT.Header] ->
457+ Minio (ETag, UTCTime)
334458copyObjectSingle bucket object srcInfo headers = do
335459 -- validate that srcRange is Nothing for this API.
336460 when (isJust $ srcRange srcInfo) $
337461 throwIO MErrVCopyObjSingleNoRangeAccepted
338− resp <- executeRequest $
339− defaultS3ReqInfo { riMethod = HT.methodPut
340− , riBucket = Just bucket
341− , riObject = Just object
342− , riHeaders = headers ++ srcInfoToHeaders srcInfo
343− }
462+ resp <-
463+ executeRequest $
464+ defaultS3ReqInfo
465+ { riMethod = HT.methodPut,
466+ riBucket = Just bucket,
467+ riObject = Just object,
468+ riHeaders = headers ++ srcInfoToHeaders srcInfo
469+ }
344470 parseCopyObjectResponse $ NC.responseBody resp
345471
346472-- | Complete a multipart upload.
347−completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
348− -> Minio ETag
473+completeMultipartUpload ::
474+ Bucket ->
475+ Object ->
476+ UploadId ->
477+ [PartTuple] ->
478+ Minio ETag
349479completeMultipartUpload bucket object uploadId partTuple = do
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− }
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+ }
358491 parseCompleteMultipartUploadResponse $ NC.responseBody resp
359492 where
360493 params = [("uploadId", Just uploadId)]
361494
362495-- | Abort a multipart upload.
363496abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
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− }
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+ }
370506 where
371507 params = [("uploadId", Just uploadId)]
372508
373509-- | List incomplete multipart uploads.
374−listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
375− -> Maybe Text -> Maybe Int -> Minio ListUploadsResult
510+listIncompleteUploads' ::
511+ Bucket ->
512+ Maybe Text ->
513+ Maybe Text ->
514+ Maybe Text ->
515+ Maybe Text ->
516+ Maybe Int ->
517+ Minio ListUploadsResult
376518listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
377− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
378− , riBucket = Just bucket
379− , riQueryParams = params
380− }
519+ resp <-
520+ executeRequest $
521+ defaultS3ReqInfo
522+ { riMethod = HT.methodGet,
523+ riBucket = Just bucket,
524+ riQueryParams = params
525+ }
381526 parseListUploadsResponse $ NC.responseBody resp
382527 where
383528 -- build query params
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−
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+ ]
392538
393539-- | List parts of an ongoing multipart upload.
394−listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
395− -> Maybe Text -> Minio ListPartsResult
540+listIncompleteParts' ::
541+ Bucket ->
542+ Object ->
543+ UploadId ->
544+ Maybe Text ->
545+ Maybe Text ->
546+ Minio ListPartsResult
396547listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
397− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
398− , riBucket = Just bucket
399− , riObject = Just object
400− , riQueryParams = mkOptionalParams params
401− }
548+ resp <-
549+ executeRequest $
550+ defaultS3ReqInfo
551+ { riMethod = HT.methodGet,
552+ riBucket = Just bucket,
553+ riObject = Just object,
554+ riQueryParams = mkOptionalParams params
555+ }
402556 parseListPartsResponse $ NC.responseBody resp
403557 where
404558 -- build optional query params
405− params = [
406− ("uploadId", Just uploadId)
407− , ("part-number-marker", partNumMarker)
408− , ("max-parts", maxParts)
559+ params =
560+ [ ("uploadId", Just uploadId),
561+ ("part-number-marker", partNumMarker),
562+ ("max-parts", maxParts)
409563 ]
410564
411565-- | Get metadata of an object.
412566headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
413567headObject bucket object reqHeaders = do
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−
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+ }
427581 maybe (throwIO MErrVInvalidObjectInfoResponse) return $
428− ObjectInfo <$> Just object <*> modTime <*> etag <*> size <*> Just metadata
429−
582+ parseGetObjectHeaders object $
583+ NC.responseHeaders resp
430584
431585-- | Query the object store if a given bucket exists.
432586headBucket :: Bucket -> Minio Bool
433−headBucket bucket = headBucketEx `catches`
434− [ Handler handleNoSuchBucket
435− , Handler handleStatus404
436− ]
437−
587+headBucket bucket =
588+ headBucketEx
589+ `catches` [ Handler handleNoSuchBucket,
590+ Handler handleStatus404
591+ ]
438592 where
439593 handleNoSuchBucket :: ServiceErr -> Minio Bool
440− handleNoSuchBucket e | e == NoSuchBucket = return False
441− | otherwise = throwIO e
442−
594+ handleNoSuchBucket e
595+ | e == NoSuchBucket = return False
596+ | otherwise = throwIO e
443597 handleStatus404 :: NC.HttpException -> Minio Bool
444598 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
445599 if NC.responseStatus res == status404
446− then return False
447− else throwIO e
600+ then return False
601+ else throwIO e
448602 handleStatus404 e = throwIO e
449−
450603 headBucketEx = do
451− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
452− , riBucket = Just bucket
453− }
604+ resp <-
605+ executeRequest $
606+ defaultS3ReqInfo
607+ { riMethod = HT.methodHead,
608+ riBucket = Just bucket
609+ }
454610 return $ NC.responseStatus resp == HT.ok200
455611
456612-- | Set the notification configuration on a bucket.
457613putBucketNotification :: Bucket -> Notification -> Minio ()
458614putBucketNotification bucket ncfg = do
459615 ns <- asks getSvcNamespace
460− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
461− , riBucket = Just bucket
462− , riQueryParams = [("notification", Nothing)]
463− , riPayload = PayloadBS $
464− mkPutNotificationRequest ns ncfg
465− }
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+ }
466626
467627-- | Retrieve the notification configuration on a bucket.
468628getBucketNotification :: Bucket -> Minio Notification
469629getBucketNotification bucket = do
470− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
471− , riBucket = Just bucket
472− , riQueryParams = [("notification", Nothing)]
473− }
630+ resp <-
631+ executeRequest $
632+ defaultS3ReqInfo
633+ { riMethod = HT.methodGet,
634+ riBucket = Just bucket,
635+ riQueryParams = [("notification", Nothing)]
636+ }
474637 parseNotification $ NC.responseBody resp
475638
476639-- | Remove all notifications configured on a bucket.
@@ -480,11 +643,14 @@ removeAllBucketNotification = flip putBucketNotification defaultNotification
480643-- | Fetch the policy if any on a bucket.
481644getBucketPolicy :: Bucket -> Minio Text
482645getBucketPolicy bucket = do
483− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
484− , riBucket = Just bucket
485− , riQueryParams = [("policy", Nothing)]
486− }
487− return $ toS $ NC.responseBody resp
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
488654
489655-- | Set a new policy on a bucket.
490656-- As a special condition if the policy is empty
@@ -496,18 +662,24 @@ setBucketPolicy bucket policy = do
496662 else putBucketPolicy bucket policy
497663
498664-- | Save a new policy on a bucket.
499−putBucketPolicy :: Bucket -> Text -> Minio()
665+putBucketPolicy :: Bucket -> Text -> Minio ()
500666putBucketPolicy bucket policy = do
501− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
502− , riBucket = Just bucket
503− , riQueryParams = [("policy", Nothing)]
504− , riPayload = PayloadBS $ encodeUtf8 policy
505− }
667+ void $
668+ executeRequest $
669+ defaultS3ReqInfo
670+ { riMethod = HT.methodPut,
671+ riBucket = Just bucket,
672+ riQueryParams = [("policy", Nothing)],
673+ riPayload = PayloadBS $ encodeUtf8 policy
674+ }
506675
507676-- | Delete any policy set on a bucket.
508−deleteBucketPolicy :: Bucket -> Minio()
677+deleteBucketPolicy :: Bucket -> Minio ()
509678deleteBucketPolicy bucket = do
510− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
511− , riBucket = Just bucket
512− , riQueryParams = [("policy", Nothing)]
513− }
679+ void $
680+ executeRequest $
681+ defaultS3ReqInfo
682+ { riMethod = HT.methodDelete,
683+ riBucket = Just bucket,
684+ riQueryParams = [("policy", Nothing)]
685+ }
src/Network/Minio/SelectAPI.hs+233−241
@@ -15,116 +15,103 @@
1515--
1616
1717module Network.Minio.SelectAPI
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)
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)
128115
129116instance Exception EventStreamException
130117
@@ -132,171 +119,176 @@ instance Exception EventStreamException
132119chunkSize :: Int
133120chunkSize = 32 * 1024
134121
135−parseBinary :: Bin.Binary a => ByteString -> IO a
122+parseBinary :: (Bin.Binary a) => ByteString -> IO a
136123parseBinary b = do
137− case Bin.decodeOrFail $ LB.fromStrict b of
138− Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
139− Right (_, _, r) -> return r
124+ case Bin.decodeOrFail $ LB.fromStrict b of
125+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
126+ Right (_, _, r) -> return r
140127
141128bytesToHeaderName :: Text -> IO MsgHeaderName
142129bytesToHeaderName t = case t of
143− ":message-type" -> return MessageType
144− ":event-type" -> return EventType
145− ":content-type" -> return ContentType
146− ":error-code" -> return ErrorCode
130+ ":message-type" -> return MessageType
131+ ":event-type" -> return EventType
132+ ":content-type" -> return ContentType
133+ ":error-code" -> return ErrorCode
147134 ":error-message" -> return ErrorMessage
148− _ -> throwIO ESEInvalidHeaderType
135+ _ -> throwIO ESEInvalidHeaderType
149136
150−parseHeaders :: MonadUnliftIO m
151− => Word32 -> C.ConduitM ByteString a m [MessageHeader]
137+parseHeaders ::
138+ (MonadUnliftIO m) =>
139+ Word32 ->
140+ C.ConduitM ByteString a m [MessageHeader]
152141parseHeaders 0 = return []
153142parseHeaders hdrLen = do
154− bs1 <- readNBytes 1
155− n :: Word8 <- liftIO $ parseBinary bs1
143+ bs1 <- readNBytes 1
144+ n :: Word8 <- liftIO $ parseBinary bs1
156145
157− headerKeyBytes <- readNBytes $ fromIntegral n
158− let headerKey = decodeUtf8Lenient headerKeyBytes
159− headerName <- liftIO $ bytesToHeaderName headerKey
146+ headerKeyBytes <- readNBytes $ fromIntegral n
147+ let headerKey = decodeUtf8Lenient headerKeyBytes
148+ headerName <- liftIO $ bytesToHeaderName headerKey
160149
161− bs2 <- readNBytes 1
162− headerValueType :: Word8 <- liftIO $ parseBinary bs2
163− when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
150+ bs2 <- readNBytes 1
151+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
152+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
164153
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
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
171160
172− ms <- parseHeaders (hdrLen - k)
173− return (m:ms)
161+ ms <- parseHeaders (hdrLen - k)
162+ return (m : ms)
174163
175164-- readNBytes returns N bytes read from the string and throws an
176165-- exception if N bytes are not present on the stream.
177−readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
166+readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
178167readNBytes n = do
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 ()
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 ()
186176crcCheck = do
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
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
192182
193− -- we do not yield the checksum
194− C.yield $ B.take 8 b
183+ -- we do not yield the checksum
184+ C.yield $ B.take 8 b
195185
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
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
200190
201− bs <- readNBytes 4
202− expectedCrc :: Word32 <- liftIO $ parseBinary bs
191+ bs <- readNBytes 4
192+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
203193
204− when (finalCrc /= expectedCrc) $
205− throwIO ESEMessageCRCFailed
194+ when (finalCrc /= expectedCrc) $
195+ throwIO ESEMessageCRCFailed
206196
207− -- we unconditionally recurse - downstream figures out when to
208− -- quit reading the stream
209− crcCheck
197+ -- we unconditionally recurse - downstream figures out when to
198+ -- quit reading the stream
199+ crcCheck
210200 where
211201 accumulateYield n checkSum = do
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 ()
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 ()
222212handleMessage = do
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−
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
261248 where
262249 passThrough 0 = return ()
263250 passThrough n = do
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 ()
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 ()
272259selectProtoConduit = crcCheck .| handleMessage
273260
274261-- | selectObjectContent calls the SelectRequest on the given
275262-- object. It returns a Conduit of event messages that can be consumed
276263-- by the client.
277−selectObjectContent :: Bucket -> Object -> SelectRequest
278− -> Minio (C.ConduitT () EventMessage Minio ())
264+selectObjectContent ::
265+ Bucket ->
266+ Object ->
267+ SelectRequest ->
268+ Minio (C.ConduitT () EventMessage Minio ())
279269selectObjectContent b o r = do
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
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
290282
291283-- | A helper conduit that returns only the record payload bytes.
292−getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
284+getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
293285getPayloadBytes = do
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 ()
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 ()
src/Network/Minio/Sign/V4.hs+432−329
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,88 +13,94 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
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
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)
3952
4053-- these headers are not included in the string to sign when signing a
4154-- request
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 ""
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)
8483
8584mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
8685mkAuthHeader accessKey scope signedHeaderKeys sign =
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)
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"
98104
99105-- | Given SignParams and request details, including request method,
100106-- request path, headers, query params and payload hash, generates an
@@ -108,124 +114,212 @@ mkAuthHeader accessKey scope signedHeaderKeys sign =
108114-- is being created. The expiry is interpreted as an integer number of
109115-- seconds. The output will be the list of query-parameters to add to
110116-- the request.
111−signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
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]
112162signV4 !sp !req =
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− ]
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+ ]
178206
207+-- Folds header name, trims whitespace in header values, skips ignored headers
208+-- and sorts headers.
179209getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
180210getHeadersToSign !h =
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
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
186276mkCanonicalRequest !isStreaming !sp !req !headersForSign =
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)
193−
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 =
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 =
202293 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
213− ]
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+ ]
214305
215306mkStringToSign :: 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]
307+mkStringToSign ts !scope !canonicalRequest =
308+ B.intercalate
309+ "\n"
310+ [ "AWS4-HMAC-SHA256",
311+ awsTimeFormatBS ts,
312+ scope,
313+ hashSHA256 canonicalRequest
314+ ]
315+
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]
229323
230324computeSignature :: ByteString -> ByteString -> ByteString
231325computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -233,159 +327,168 @@ computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
233327-- | Takes a validated Post Policy JSON bytestring, the signing time,
234328-- and ConnInfo and returns form-data for the POST upload containing
235329-- just the signature and the encoded post-policy.
236−signV4PostPolicy :: ByteString -> SignParams
237− -> Map.Map Text ByteString
330+signV4PostPolicy ::
331+ ByteString ->
332+ SignParams ->
333+ Map.HashMap Text ByteString
238334signV4PostPolicy !postPolicyJSON !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− ]
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)
248343
249344chunkSizeConstant :: Int
250345chunkSizeConstant = 64 * 1024
251346
252347-- base16Len computes the number of bytes required to represent @n (> 0)@ in
253348-- hexadecimal.
254−base16Len :: Integral a => a -> Int
255−base16Len n | n == 0 = 0
256− | otherwise = 1 + base16Len (n `div` 16)
349+base16Len :: (Integral a) => a -> Int
350+base16Len n
351+ | n == 0 = 0
352+ | otherwise = 1 + base16Len (n `div` 16)
257353
258354signedStreamLength :: Int64 -> Int64
259355signedStreamLength dataLen =
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)
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)
277382signV4Stream !payloadLength !sp !req =
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
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")
346392 ]
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)
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)
351438 when (B.length bs /= n) $
352− throwIO MErrVStreamingBodyUnexpectedEOF
439+ throwIO MErrVStreamingBodyUnexpectedEOF
353440 return bs
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− }
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'
src/Network/Minio/Utils.hs+150−123
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,71 +16,77 @@
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.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)
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)
5150allocateReadFile fp = do
5251 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
53− either (\(e :: IOException) -> throwIO e) (return . (rk,)) hdlE
52+ either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
5453 where
5554 openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
5655 cleanup = either (const $ return ()) IO.hClose
5756
5857-- | Queries the file size from the handle. Catches any file operation
5958-- exceptions and returns Nothing instead.
60−getFileSize :: (MonadUnliftIO m, R.MonadResource m)
61− => Handle -> m (Maybe Int64)
59+getFileSize ::
60+ (MonadUnliftIO m) =>
61+ Handle ->
62+ m (Maybe Int64)
6263getFileSize h = do
6364 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
6465 case resE of
65− Left (_ :: IOException) -> return Nothing
66− Right s -> return $ Just s
66+ Left (_ :: U.IOException) -> return Nothing
67+ Right s -> return $ Just s
6768
6869-- | Queries if handle is seekable. Catches any file operation
6970-- exceptions and return False instead.
70−isHandleSeekable :: (R.MonadResource m, MonadUnliftIO m)
71− => Handle -> m Bool
71+isHandleSeekable ::
72+ (R.MonadResource m) =>
73+ Handle ->
74+ m Bool
7275isHandleSeekable h = do
7376 resE <- liftIO $ try $ IO.hIsSeekable h
7477 case resE of
75− Left (_ :: IOException) -> return False
76− Right v -> return v
78+ Left (_ :: U.IOException) -> return False
79+ Right v -> return v
7780
7881-- | Helper function that opens a handle to the filepath and performs
7982-- the given action on it. Exceptions of type MError are caught and
8083-- returned - both during file handle allocation and when the action
8184-- is run.
82−withNewHandle :: (MonadUnliftIO m, R.MonadResource m)
83− => FilePath -> (Handle -> m a) -> m (Either IOException a)
85+withNewHandle ::
86+ (MonadUnliftIO m, R.MonadResource m) =>
87+ FilePath ->
88+ (Handle -> m a) ->
89+ m (Either U.IOException a)
8490withNewHandle fp fileAction = do
8591 -- opening a handle can throw MError exception.
8692 handleE <- try $ allocateReadFile fp
@@ -94,19 +100,61 @@ withNewHandle fp fileAction = do
94100 return resE
95101
96102mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
97−mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
103+mkHeaderFromPairs = map (first mk)
98104
99105lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
100−lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
106+lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
101107
102108getETagHeader :: [HT.Header] -> Maybe Text
103109getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
104110
105111getMetadata :: [HT.Header] -> [(Text, Text)]
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)
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_
110158
111159getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
112160getLastModifiedHeader hs = do
@@ -116,19 +164,21 @@ getLastModifiedHeader hs = do
116164getContentLength :: [HT.Header] -> Maybe Int64
117165getContentLength hs = do
118166 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
119− fst <$> hush (decimal nbs)
120−
167+ fst <$> either (const Nothing) Just (decimal nbs)
121168
122169decodeUtf8Lenient :: ByteString -> Text
123170decodeUtf8Lenient = decodeUtf8With lenientDecode
124171
125172isSuccessStatus :: HT.Status -> Bool
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)
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)
132182httpLbs req mgr = do
133183 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
134184 resp <- either throwIO return respE
@@ -140,21 +190,26 @@ httpLbs req mgr = do
140190 Just "application/json" -> do
141191 sErr <- parseErrResponseJSON $ NC.responseBody resp
142192 throwIO sErr
143−
144− _ -> throwIO $ NC.HttpExceptionRequest req $
145− NC.StatusCodeException (void resp) (show resp)
193+ _ ->
194+ throwIO $
195+ NC.HttpExceptionRequest req $
196+ NC.StatusCodeException (void resp) (showBS resp)
146197
147198 return resp
148199 where
149− tryHttpEx :: IO (NC.Response LByteString)
150− -> IO (Either NC.HttpException (NC.Response LByteString))
200+ tryHttpEx ::
201+ IO (NC.Response LByteString) ->
202+ IO (Either NC.HttpException (NC.Response LByteString))
151203 tryHttpEx = try
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 ()))
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 ()))
158213http req mgr = do
159214 respE <- tryHttpEx $ NC.http req mgr
160215 resp <- either throwIO return respE
@@ -164,25 +219,31 @@ http req mgr = do
164219 respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
165220 sErr <- parseErrResponse respBody
166221 throwIO sErr
167−
168222 _ -> do
169223 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
170− throwIO $ NC.HttpExceptionRequest req $
171− NC.StatusCodeException (void resp) content
172−
224+ throwIO $
225+ NC.HttpExceptionRequest req $
226+ NC.StatusCodeException (void resp) content
173227
174228 return resp
175229 where
176− tryHttpEx :: (MonadUnliftIO m) => m a
177− -> m (Either NC.HttpException a)
230+ tryHttpEx ::
231+ (MonadUnliftIO m) =>
232+ m a ->
233+ m (Either NC.HttpException a)
178234 tryHttpEx = try
179− contentTypeMay resp = lookupHeader Hdr.hContentType $
180− NC.responseHeaders resp
235+ contentTypeMay resp =
236+ lookupHeader Hdr.hContentType $
237+ NC.responseHeaders resp
181238
182239-- Similar to mapConcurrently but limits the number of threads that
183240-- can run using a quantity semaphore.
184−limitedMapConcurrently :: MonadUnliftIO m
185− => Int -> (t -> m a) -> [t] -> m [a]
241+limitedMapConcurrently ::
242+ (MonadUnliftIO m) =>
243+ Int ->
244+ (t -> m a) ->
245+ [t] ->
246+ m [a]
186247limitedMapConcurrently 0 _ _ = return []
187248limitedMapConcurrently count act args = do
188249 t' <- U.newTVarIO count
@@ -191,17 +252,15 @@ limitedMapConcurrently count act args = do
191252 where
192253 wThread t arg =
193254 U.bracket_ (waitSem t) (signalSem t) $ act arg
194−
195255 -- quantity semaphore implementation using TVar
196256 waitSem t = U.atomically $ do
197257 v <- U.readTVar t
198258 if v > 0
199− then U.writeTVar t (v-1)
200− else U.retrySTM
201−
259+ then U.writeTVar t (v - 1)
260+ else U.retrySTM
202261 signalSem t = U.atomically $ do
203262 v <- U.readTVar t
204− U.writeTVar t (v+1)
263+ U.writeTVar t (v + 1)
205264
206265-- helper function to 'drop' empty optional parameter.
207266mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -210,7 +269,7 @@ mkQuery k mv = (k,) <$> mv
210269-- helper function to build query parameters that are optional.
211270-- don't use it with mandatory query params with empty value.
212271mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
213−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
272+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
214273
215274-- | Conduit that rechunks bytestrings into the given chunk
216275-- lengths. Stops after given chunk lengths are yielded. Stops if
@@ -218,41 +277,9 @@ mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
218277-- received. Does not throw any errors.
219278chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
220279chunkBSConduit [] = 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
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 ()
src/Network/Minio/XmlCommon.hsnew file+65
@@ -0,0 +1,65 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.XmlCommon where
18+
19+import qualified Data.Text as T
20+import Data.Text.Read (decimal)
21+import Data.Time (UTCTime)
22+import Data.Time.Format.ISO8601 (iso8601ParseM)
23+import Lib.Prelude (throwIO)
24+import Network.Minio.Errors
25+import Text.XML (Name (Name), def, parseLBS)
26+import Text.XML.Cursor (Axis, Cursor, content, element, fromDocument, laxElement, ($/), (&/))
27+
28+s3Name :: Text -> Text -> Name
29+s3Name ns s = Name s (Just ns) Nothing
30+
31+uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
32+uncurry4 f (a, b, c, d) = f a b c d
33+
34+uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
35+uncurry6 f (a, b, c, d, e, g) = f a b c d e g
36+
37+-- | Parse time strings from XML
38+parseS3XMLTime :: (MonadIO m) => Text -> m UTCTime
39+parseS3XMLTime t =
40+ maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
41+ iso8601ParseM $
42+ toString t
43+
44+parseDecimal :: (MonadIO m, Integral a) => Text -> m a
45+parseDecimal numStr =
46+ either (throwIO . MErrVXmlParse . show) return $
47+ fst <$> decimal numStr
48+
49+parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
50+parseDecimals numStr = forM numStr parseDecimal
51+
52+s3Elem :: Text -> Text -> Axis
53+s3Elem ns = element . s3Name ns
54+
55+parseRoot :: (MonadIO m) => LByteString -> m Cursor
56+parseRoot =
57+ either (throwIO . MErrVXmlParse . show) (return . fromDocument)
58+ . parseLBS def
59+
60+parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
61+parseErrResponse xmldata = do
62+ r <- parseRoot xmldata
63+ let code = T.concat $ r $/ laxElement "Code" &/ content
64+ message = T.concat $ r $/ laxElement "Message" &/ content
65+ return $ toServiceErr code message
src/Network/Minio/XmlGenerator.hs+165−99
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,89 +15,112 @@
1515--
1616
1717module Network.Minio.XmlGenerator
18− ( mkCreateBucketConfig
19− , mkCompleteMultipartUploadRequest
20− , mkPutNotificationRequest
21− , mkSelectRequest
22− ) where
23−
18+ ( mkCreateBucketConfig,
19+ mkCompleteMultipartUploadRequest,
20+ mkPutNotificationRequest,
21+ mkSelectRequest,
22+ )
23+where
2424
2525import qualified Data.ByteString.Lazy as LBS
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−
26+import qualified Data.Text as T
27+import Network.Minio.Data
28+import Network.Minio.XmlCommon
29+import Text.XML
3430
3531-- | Create a bucketConfig request body XML
3632mkCreateBucketConfig :: Text -> Region -> ByteString
3733mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
3834 where
39− s3Element n = Element (s3Name ns n) mempty
40− root = s3Element "CreateBucketConfiguration"
41− [ NodeElement $ s3Element "LocationConstraint"
42− [ NodeContent location]
35+ s3Element n = Element (s3Name ns n) mempty
36+ root =
37+ s3Element
38+ "CreateBucketConfiguration"
39+ [ NodeElement $
40+ s3Element
41+ "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 = 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− ]
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+ ]
5969 cmur = Document (Prologue [] Nothing []) root []
6070
6171-- Simplified XML representation without element attributes.
62−data XNode = XNode Text [XNode]
63− | XLeaf Text Text
64− deriving (Eq, Show)
72+data XNode
73+ = XNode Text [XNode]
74+ | XLeaf Text Text
75+ deriving stock (Eq, Show)
6576
6677toXML :: Text -> XNode -> ByteString
67−toXML ns node = LBS.toStrict $ renderLBS def $
68− Document (Prologue [] Nothing []) (xmlNode node) []
78+toXML ns node =
79+ LBS.toStrict $
80+ renderLBS def $
81+ Document (Prologue [] Nothing []) (xmlNode node) []
6982 where
7083 xmlNode :: XNode -> Element
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]
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]
7592
7693class ToXNode a where
7794 toXNode :: a -> XNode
7895
7996instance ToXNode Event where
80− toXNode = XLeaf "Event" . show
97+ toXNode = XLeaf "Event" . toText
8198
8299instance ToXNode Notification where
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
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
87105
88106toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
89−toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
90− XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events ++
91− [toXNode fRule]
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]
92112
93113instance ToXNode Filter where
94114 toXNode (Filter (FilterKey (FilterRules rules))) =
95115 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
96116
97117getFRXNode :: FilterRule -> XNode
98−getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
99− , XLeaf "Value" v
100− ]
118+getFRXNode (FilterRule n v) =
119+ XNode
120+ "FilterRule"
121+ [ XLeaf "Name" n,
122+ XLeaf "Value" v
123+ ]
101124
102125mkPutNotificationRequest :: Text -> Notification -> ByteString
103126mkPutNotificationRequest ns = toXML ns . toXNode
@@ -106,60 +129,103 @@ mkSelectRequest :: SelectRequest -> ByteString
106129mkSelectRequest r = LBS.toStrict $ renderLBS def sr
107130 where
108131 sr = Document (Prologue [] Nothing []) root []
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− ]
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+ ]
138188 comprTypeNode Nothing = []
139−
140189 kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
141− formatNode (InputFormatCSV (CSVProp h)) =
142− Element "CSV" mempty
143− (map NodeElement $ map kvElement $ H.toList h)
190+ formatNode (InputFormatCSV c) =
191+ Element
192+ "CSV"
193+ mempty
194+ (map (NodeElement . kvElement) (csvPropsList c))
144195 formatNode (InputFormatJSON p) =
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− ]
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+ ]
154209 formatNode InputFormatParquet = Element "Parquet" mempty []
155−
156210 outputSerializationNodes (OutputSerializationJSON j) =
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−
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+ ]
163224 rdElem Nothing = []
164− rdElem (Just t) = [NodeElement $ Element "RecordDelimiter" mempty
165− [NodeContent t]]
225+ rdElem (Just t) =
226+ [ NodeElement $
227+ Element
228+ "RecordDelimiter"
229+ mempty
230+ [NodeContent t]
231+ ]
src/Network/Minio/XmlParser.hs+107−152
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,75 +15,38 @@
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− ) 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+ )
31+where
3132
3233import qualified Data.ByteString.Lazy as LB
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
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)
7741
7842-- | Parse the response XML of a list buckets call.
7943parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
8044parseListBuckets xmldata = do
8145 r <- parseRoot xmldata
8246 ns <- asks getSvcNamespace
83− let
84− s3Elem' = s3Elem ns
85− names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
86− timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
47+ let s3Elem' = s3Elem ns
48+ names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
49+ timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
8750
8851 times <- mapM parseS3XMLTime timeStrings
8952 return $ zipWith BucketInfo names times
@@ -116,40 +79,38 @@ parseCopyObjectResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =
11679parseCopyObjectResponse xmldata = do
11780 r <- parseRoot xmldata
11881 ns <- asks getSvcNamespace
119− let
120− s3Elem' = s3Elem ns
121− mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
82+ let s3Elem' = s3Elem ns
83+ mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
12284
12385 mtime <- parseS3XMLTime mtimeStr
12486 return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
12587
12688-- | Parse the response XML of a list objects v1 call.
127−parseListObjectsV1Response :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
128− => LByteString -> m ListObjectsV1Result
89+parseListObjectsV1Response ::
90+ (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
91+ LByteString ->
92+ m ListObjectsV1Result
12993parseListObjectsV1Response xmldata = do
13094 r <- parseRoot xmldata
13195 ns <- asks getSvcNamespace
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
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
147107
148108 modTimes <- mapM parseS3XMLTime modTimeStr
149109 sizes <- parseDecimals sizeStr
150110
151− let
152− objects = map (uncurry5 ObjectInfo) $ zip5 keys modTimes etags sizes (repeat Map.empty)
111+ let objects =
112+ map (uncurry6 ObjectInfo) $
113+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
153114
154115 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
155116
@@ -158,27 +119,24 @@ parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
158119parseListObjectsResponse xmldata = do
159120 r <- parseRoot xmldata
160121 ns <- asks getSvcNamespace
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
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
176133
177134 modTimes <- mapM parseS3XMLTime modTimeStr
178135 sizes <- parseDecimals sizeStr
179136
180− let
181− objects = map (uncurry5 ObjectInfo) $ zip5 keys modTimes etags sizes (repeat Map.empty)
137+ let objects =
138+ map (uncurry6 ObjectInfo) $
139+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
182140
183141 return $ ListObjectsResult hasMore nextToken objects prefixes
184142
@@ -187,20 +145,18 @@ parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
187145parseListUploadsResponse xmldata = do
188146 r <- parseRoot xmldata
189147 ns <- asks getSvcNamespace
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
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
199156
200157 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
201158
202− let
203− uploads = zip3 uploadKeys uploadIds uploadInitTimes
159+ let uploads = zip3 uploadKeys uploadIds uploadInitTimes
204160
205161 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
206162
@@ -208,34 +164,25 @@ parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
208164parseListPartsResponse xmldata = do
209165 r <- parseRoot xmldata
210166 ns <- asks getSvcNamespace
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
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
219174
220175 partModTimes <- mapM parseS3XMLTime partModTimeStr
221176 partSizes <- parseDecimals partSizeStr
222177 partNumbers <- parseDecimals partNumberStr
223178 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
224179
225− let
226− partInfos = map (uncurry4 ObjectPartInfo) $
227− zip4 partNumbers partETags partSizes partModTimes
180+ let partInfos =
181+ map (uncurry4 ObjectPartInfo) $
182+ zip4 partNumbers partETags partSizes partModTimes
228183
229184 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
230185
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−
239186parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
240187parseNotification xmldata = do
241188 r <- parseRoot xmldata
@@ -244,32 +191,40 @@ parseNotification xmldata = do
244191 qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
245192 tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
246193 lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
247− Notification <$> (mapM (parseNode ns "Queue") qcfg)
248− <*> (mapM (parseNode ns "Topic") tcfg)
249− <*> (mapM (parseNode ns "CloudFunction") lcfg)
194+ Notification
195+ <$> mapM (parseNode ns "Queue") qcfg
196+ <*> mapM (parseNode ns "Topic") tcfg
197+ <*> mapM (parseNode ns "CloudFunction") lcfg
250198 where
251−
252199 getFilterRule ns c =
253200 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
254201 value = T.concat $ c $/ s3Elem ns "Value" &/ content
255− in FilterRule name value
256−
202+ in FilterRule name value
257203 parseNode ns arnName nodeData = do
258204 let c = fromNode nodeData
259− id = T.concat $ c $/ s3Elem ns "Id" &/ content
205+ itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
260206 arn = T.concat $ c $/ s3Elem ns arnName &/ content
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
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
268222parseSelectProgress xmldata = do
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
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
stack.yaml+4−4
@@ -15,7 +15,7 @@
1515# resolver:
1616# name: custom-snapshot
1717# location: "./custom-snapshot.yaml"
18−resolver: lts-13.1
18+resolver: lts-22.19
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-13.1
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: []
42+extra-deps:
43+ - crypton-connection-0.3.2
4344
4445# Override default flag values for local packages and extra-deps
4546flags: {}
4647
4748# Extra package databases containing global packages
4849extra-package-dbs: []
49−
5050# Control whether we use the GHC we find on the path
5151# system-ghc: true
5252#
stack.yaml.locknew file+19
@@ -0,0 +1,19 @@
1+# This file was autogenerated by Stack.
2+# You should not edit this file by hand.
3+# For more information, please see the documentation at:
4+# https://docs.haskellstack.org/en/stable/lock_files
5+
6+packages:
7+- completed:
8+ hackage: crypton-connection-0.3.2@sha256:c7937edc25ab022bcf167703f2ec5ab73b62908e545bb587d2aa42b33cd6f6cc,1581
9+ pantry-tree:
10+ sha256: f986ad29b008cbe5732606e9cde1897191c486a2f1f169a4cb75fd915bce397c
11+ size: 394
12+ original:
13+ hackage: crypton-connection-0.3.2
14+snapshots:
15+- completed:
16+ sha256: e5cac927cf7ccbd52aa41476baa68b88c564ee6ddc3bc573dbf4210069287fe7
17+ size: 713340
18+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/19.yaml
19+ original: lts-22.19
test/LiveServer.hs+807−461
@@ -1,6 +1,5 @@
1−{-# LANGUAGE OverloadedStrings #-}
21--
3−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
43--
54-- Licensed under the Apache License, Version 2.0 (the "License");
65-- you may not use this file except in compliance with the License.
@@ -15,36 +14,34 @@
1514-- limitations under the License.
1615--
1716
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
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
3429import qualified Network.HTTP.Client.MultipartFormData as Form
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
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
4845
4946main :: IO ()
5047main = defaultMain tests
@@ -52,41 +49,107 @@ main = defaultMain tests
5249tests :: TestTree
5350tests = testGroup "Tests" [liveServerUnitTests]
5451
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+
5597-- conduit that generates random binary stream of given length
56−randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
57−randomDataSrc s' = genBS s'
98+randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
99+randomDataSrc = genBS
58100 where
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−
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)
63107 genBS s = do
64108 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
65109 let byteArr64 = BS.pack w8s
66110 if s < oneMiB
67111 then yield $ concatIt byteArr64 s
68− else do yield $ concatIt byteArr64 oneMiB
69− genBS (s - oneMiB)
112+ else do
113+ yield $ concatIt byteArr64 oneMiB
114+ genBS (s - oneMiB)
70115
71−mkRandFile :: R.MonadResource m => Int64 -> m FilePath
116+mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
72117mkRandFile size = do
73− dir <- liftIO $ getTemporaryDirectory
118+ dir <- liftIO getTemporaryDirectory
74119 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
75120
76121funTestBucketPrefix :: Text
77122funTestBucketPrefix = "miniohstest-"
78123
79−funTestWithBucket :: TestName
80− -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree
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
81147funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
82148 -- generate a random name for the bucket
83149 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
84150 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
85151 liftStep = liftIO . step
86− connInfo <- ( bool minioPlayCI
87− ( setCreds (Credentials "minio" "minio123") "http://localhost:9000" )
88− . isJust
89− ) <$> lookupEnv "MINIO_LOCAL"
152+ connInfo <- loadTestServerConnInfo
90153 ret <- runMinio connInfo $ do
91154 liftStep $ "Creating bucket for test - " ++ t
92155 foundBucket <- bucketExists b
@@ -96,24 +159,40 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
96159 deleteBucket b
97160 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
98161
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+
99173liveServerUnitTests :: TestTree
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− ]
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+ ]
117196
118197basicTests :: TestTree
119198basicTests = funTestWithBucket "Basic tests" $
@@ -122,83 +201,109 @@ basicTests = funTestWithBucket "Basic tests" $
122201 buckets <- getService
123202 unless (length (filter (== bucket) $ map biName buckets) == 1) $
124203 liftIO $
125− assertFailure ("The bucket " ++ show bucket ++
126− " was expected to exist.")
204+ assertFailure
205+ ( "The bucket "
206+ ++ show bucket
207+ ++ " was expected to exist."
208+ )
127209
128210 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
129211 mbE <- try $ makeBucket bucket Nothing
130212 case mbE of
131213 Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
132− _ -> return ()
214+ _ -> return ()
133215
134216 step "makeBucket with an invalid bucket name and check for appropriate exception."
135217 invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
136218 case invalidMBE of
137219 Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
138− _ -> return ()
220+ _ -> return ()
139221
140222 step "getLocation works"
141223 region <- getLocation bucket
142224 liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
143225
144226 step "singlepart putObject works"
145− fPutObject bucket "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
227+ testFilepath <- mkRandFile 200
228+ fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
146229
147230 step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
148− fpE <- try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
231+ fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
149232 case fpE of
150233 Left exn -> liftIO $ exn @?= NoSuchBucket
151− _ -> return ()
234+ _ -> return ()
152235
153236 outFile <- mkRandFile 0
154237 step "simple fGetObject works"
155− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions
238+ fGetObject bucket "test-file" outFile defaultGetObjectOptions
156239
157240 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
158241 step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
159− resE <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
160− gooIfUnmodifiedSince = (Just unmodifiedTime)
161− }
242+ resE <-
243+ try $
244+ fGetObject
245+ bucket
246+ "test-file"
247+ outFile
248+ defaultGetObjectOptions
249+ { gooIfUnmodifiedSince = Just unmodifiedTime
250+ }
162251 case resE of
163252 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
164− _ -> return ()
253+ _ -> return ()
165254
166255 step "fGetObject an object with no matching etag, check for exception"
167− resE1 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
168− gooIfMatch = (Just "invalid-etag")
169− }
256+ resE1 <-
257+ try $
258+ fGetObject
259+ bucket
260+ "test-file"
261+ outFile
262+ defaultGetObjectOptions
263+ { gooIfMatch = Just "invalid-etag"
264+ }
170265 case resE1 of
171266 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
172− _ -> return ()
267+ _ -> return ()
173268
174269 step "fGetObject an object with no valid range, check for exception"
175− resE2 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
176− gooRange = (Just $ HT.ByteRangeFromTo 100 200)
177− }
270+ resE2 <-
271+ try $
272+ fGetObject
273+ bucket
274+ "test-file"
275+ outFile
276+ defaultGetObjectOptions
277+ { gooRange = Just $ HT.ByteRangeFromTo 100 300
278+ }
178279 case resE2 of
179− Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
180− _ -> return ()
280+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
281+ _ -> return ()
181282
182283 step "fGetObject on object with a valid range"
183− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
184− gooRange = (Just $ HT.ByteRangeFrom 1)
185− }
284+ fGetObject
285+ bucket
286+ "test-file"
287+ outFile
288+ defaultGetObjectOptions
289+ { gooRange = Just $ HT.ByteRangeFrom 1
290+ }
186291
187292 step "fGetObject a non-existent object and check for NoSuchKey exception"
188293 resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
189294 case resE3 of
190295 Left exn -> liftIO $ exn @?= NoSuchKey
191− _ -> return ()
296+ _ -> return ()
192297
193298 step "create new multipart upload works"
194299 uid <- newMultipartUpload bucket "newmpupload" []
195− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
300+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
196301
197302 step "abort a new multipart upload works"
198303 abortMultipartUpload bucket "newmpupload" uid
199304
200305 step "delete object works"
201− deleteObject bucket "lsb-release"
306+ deleteObject bucket "test-file"
202307
203308 step "statObject test"
204309 let object = "sample"
@@ -208,257 +313,310 @@ basicTests = funTestWithBucket "Basic tests" $
208313
209314 step "get metadata of the object"
210315 res <- statObject bucket object defaultGetObjectOptions
211− liftIO $ (oiSize res) @?= 0
316+ liftIO $ oiSize res @?= 0
212317
213318 step "delete object"
214319 deleteObject bucket object
215320
216321lowLevelMultipartTest :: TestTree
217322lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
218− \step bucket -> do
219− -- low-level multipart operation tests.
220− let object = "newmpupload"
221− mb15 = 15 * 1024 * 1024
323+ \step bucket -> do
324+ -- low-level multipart operation tests.
325+ let object = "newmpupload"
326+ mb15 = 15 * 1024 * 1024
222327
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.")
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."
227332
228− randFile <- mkRandFile mb15
333+ randFile <- mkRandFile mb15
229334
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
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
233339
234− step "complete multipart"
235− void $ completeMultipartUpload bucket object uid [partInfo]
340+ step "complete multipart"
341+ void $ completeMultipartUpload bucket object uid [partInfo]
236342
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"
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"
243351
244− step "Cleanup actions"
245− removeObject bucket object
352+ step "Cleanup actions"
353+ removeObject bucket object
246354
247355putObjectSizeTest :: TestTree
248356putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
249357 \step bucket -> do
250− -- putObject test (conduit source, size specified)
251− let obj = "msingle"
252− mb1 = 1 * 1024 * 1024
358+ -- putObject test (conduit source, size specified)
359+ let obj = "msingle"
360+ mb1 = 1 * 1024 * 1024
253361
254− step "Prepare for putObject with from source with size."
255− rFile <- mkRandFile mb1
362+ step "Prepare for putObject with from source with size."
363+ rFile <- mkRandFile mb1
256364
257− step "Upload single file."
258− putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
365+ step "Upload single file."
366+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
259367
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"
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"
266376
267− step "Cleanup actions"
268− deleteObject bucket obj
377+ step "Cleanup actions"
378+ deleteObject bucket obj
269379
270380putObjectNoSizeTest :: TestTree
271381putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
272382 \step bucket -> do
273− -- putObject test (conduit source, no size specified)
274− let obj = "mpart"
275− mb70 = 70 * 1024 * 1024
383+ -- putObject test (conduit source, no size specified)
384+ let obj = "mpart"
385+ mb70 = 70 * 1024 * 1024
276386
277− step "Prepare for putObject with from source without providing size."
278− rFile <- mkRandFile mb70
387+ step "Prepare for putObject with from source without providing size."
388+ rFile <- mkRandFile mb70
279389
280− step "Upload multipart file."
281− putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
390+ step "Upload multipart file."
391+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
282392
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"
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"
289401
290− step "Cleanup actions"
291− deleteObject bucket obj
402+ step "Cleanup actions"
403+ deleteObject bucket obj
292404
293405highLevelListingTest :: TestTree
294406highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
295407 \step bucket -> do
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 $
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 $
315433 extractObjectsAndDirsFromList items
316434
317− step "High-level recursive listing of objects"
318− objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
435+ step "High-level recursive listing of objects"
436+ objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
319437
320− liftIO $ assertEqual "Objects match failed!"
321− (Just $ sort expectedObjects) $
322− extractObjectsFromList objects
438+ liftIO
439+ $ assertEqual
440+ "Objects match failed!"
441+ (Just $ sort expectedObjects)
442+ $ extractObjectsFromList objects
323443
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 $
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 $
327448 extractObjectsAndDirsFromList itemsV1
328449
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"
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
384504 uid <- newMultipartUpload bucket object []
385505 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
386506
387− step "put object parts 1..10"
388− inputFile <- mkRandFile mb5
389− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
390− forM_ [1..10] $ \pnum ->
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 ->
391529 putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
392530
393− step "fetch list parts"
394− incompleteParts <- C.runConduit $ listIncompleteParts bucket object uid
395− C..| sinkList
396− liftIO $ length incompleteParts @?= 10
531+ step "fetch list parts"
532+ incompleteParts <-
533+ C.runConduit $
534+ listIncompleteParts bucket object uid
535+ C..| sinkList
536+ liftIO $ length incompleteParts @?= 10
397537
398− step "cleanup"
399− abortMultipartUpload bucket object uid
538+ step "cleanup"
539+ abortMultipartUpload bucket object uid
400540
401541listingTest :: TestTree
402542listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
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.")
451−
452− step "put object parts 1..10"
453− inputFile <- mkRandFile mb5
454− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
455− forM_ [1..10] $ \pnum ->
456− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
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."
457587
458− step "fetch list parts"
459− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
460− liftIO $ (length $ lprParts listPartsResult) @?= 10
461− abortMultipartUpload bucket object uid
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
462620
463621presignedUrlFunTest :: TestTree
464622presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -470,29 +628,47 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
470628 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
471629
472630 step "PUT object presigned URL - makePresignedUrl"
473− putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
474− (Just obj) (Just "us-east-1") [] []
475−
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
476642 let size1 = 1000 :: Int64
477643 inputFile <- mkRandFile size1
478644
479645 -- attempt to upload using the presigned URL
480646 putResp <- putR size1 inputFile mgr putUrl
481− liftIO $ (NC.responseStatus putResp == HT.status200) @?
482− "presigned PUT failed"
647+ liftIO $
648+ (NC.responseStatus putResp == HT.status200)
649+ @? "presigned PUT failed"
483650
484651 step "GET object presigned URL - makePresignedUrl"
485− getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
486− (Just obj) (Just "us-east-1") [] []
652+ getUrl <-
653+ makePresignedUrl
654+ 3600
655+ HT.methodGet
656+ (Just bucket)
657+ (Just obj)
658+ (Just "us-east-1")
659+ []
660+ []
487661
488662 getResp <- getR mgr getUrl
489− liftIO $ (NC.responseStatus getResp == HT.status200) @?
490− "presigned GET failed"
663+ liftIO $
664+ (NC.responseStatus getResp == HT.status200)
665+ @? "presigned GET failed"
491666
492667 -- read content from file to compare with response above
493668 bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
494− liftIO $ (bs == NC.responseBody getResp) @?
495− "presigned put and get got mismatched data"
669+ liftIO $
670+ (bs == NC.responseBody getResp)
671+ @? "presigned put and get got mismatched data"
496672
497673 step "PUT object presigned - presignedPutObjectURL"
498674 putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
@@ -501,66 +677,71 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
501677 testFile <- mkRandFile size2
502678
503679 putResp2 <- putR size2 testFile mgr putUrl2
504− liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
505− "presigned PUT failed (presignedPutObjectUrl)"
680+ liftIO $
681+ (NC.responseStatus putResp2 == HT.status200)
682+ @? "presigned PUT failed (presignedPutObjectUrl)"
506683
507684 step "HEAD object presigned URL - presignedHeadObjectUrl"
508685 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
509686
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)"
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)"
514693
515694 -- check that header info is accurate
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"
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"
519698
520699 step "GET object presigned URL - presignedGetObjectUrl"
521700 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
522701
523702 getResp2 <- getR mgr getUrl2
524− liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
525− "presigned GET failed (presignedGetObjectUrl)"
703+ liftIO $
704+ (NC.responseStatus getResp2 == HT.status200)
705+ @? "presigned GET failed (presignedGetObjectUrl)"
526706
527707 -- read content from file to compare with response above
528708 bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
529− liftIO $ (bs2 == NC.responseBody getResp2) @?
530− "presigned put and get got mismatched data (presigned*Url)"
531−
709+ liftIO $
710+ (bs2 == NC.responseBody getResp2)
711+ @? "presigned put and get got mismatched data (presigned*Url)"
532712
533713 mapM_ (removeObject bucket) [obj, obj2]
534714 where
535715 putR size filePath mgr url = do
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}
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+ }
540724 NC.httpLbs req' mgr
541−
542725 getR mgr url = do
543− let req = NC.parseRequest_ $ toS url
726+ let req = NC.parseRequest_ $ decodeUtf8 url
544727 NC.httpLbs req mgr
545728
546729presignedPostPolicyFunTest :: TestTree
547−presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
730+presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
548731 \step bucket -> do
549−
550732 step "presignedPostPolicy basic test"
551− now <- liftIO $ Time.getCurrentTime
733+ now <- liftIO Time.getCurrentTime
552734
553735 let key = "presignedPostPolicyTest/myfile"
554− policyConds = [ ppCondBucket bucket
555− , ppCondKey key
556− , ppCondContentLengthRange 1 1000
557− , ppCondContentType "application/octet-stream"
558− , ppCondSuccessActionStatus 200
559− ]
560−
736+ policyConds =
737+ [ ppCondBucket bucket,
738+ ppCondKey key,
739+ ppCondContentLengthRange 1 1000,
740+ ppCondContentType "application/octet-stream",
741+ ppCondSuccessActionStatus 200
742+ ]
561743 expirationTime = Time.addUTCTime 3600 now
562744 postPolicyE = newPostPolicy expirationTime policyConds
563−
564745 size = 1000 :: Int64
565746
566747 inputFile <- mkRandFile size
@@ -571,43 +752,43 @@ presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
571752 (url, formData) <- presignedPostPolicy postPolicy
572753 -- liftIO (print url) >> liftIO (print formData)
573754 result <- liftIO $ postForm url formData inputFile
574− liftIO $ (NC.responseStatus result == HT.status200) @?
575− "presigned POST failed"
755+ liftIO $
756+ (NC.responseStatus result == HT.status200)
757+ @? "presigned POST failed"
576758
577759 mapM_ (removeObject bucket) [key]
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
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
588770
589771bucketPolicyFunTest :: TestTree
590772bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
591773 \step bucket -> do
592−
593774 step "bucketPolicy basic test - no policy exception"
594775 resE <- try $ getBucketPolicy bucket
595776 case resE of
596777 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
597− _ -> return ()
778+ _ -> return ()
598779
599780 resE' <- try $ setBucketPolicy bucket T.empty
600781 case resE' of
601782 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
602− _ -> return ()
783+ _ -> return ()
603784
604785 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/*\"]}]}"
605786
606787 step "try a malformed policy, expect error"
607788 resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
608789 case resE'' of
609− Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "Policy has invalid resource."
610− _ -> return ()
790+ Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
791+ _ -> return ()
611792
612793 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 <> "/*\"]}]}"
613794
@@ -622,13 +803,22 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
622803 step "verify bucket policy: (2) get `myobject` anonymously"
623804 connInfo <- asks mcConnInfo
624805 let proto = bool "http://" "https://" $ connectIsSecure connInfo
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))
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))
629819 case respE of
630− Left err -> liftIO $ assertFailure $ show err
631− Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")
820+ Left err -> liftIO $ assertFailure $ show err
821+ Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
632822
633823 deleteObject bucket obj
634824
@@ -638,25 +828,28 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
638828multipartTest :: TestTree
639829multipartTest = funTestWithBucket "Multipart Tests" $
640830 \step bucket -> do
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"
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"
645837
646− void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
838+ -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
647839
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"
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"
654847
655− step "Cleanup actions"
656− removeObject bucket obj
848+ -- step "Cleanup actions"
849+ -- removeObject bucket obj
657850
658− step "cleanup"
659− removeObject bucket "big"
851+ -- step "cleanup"
852+ -- removeObject bucket "big"
660853
661854 step "Prepare for removeIncompleteUpload"
662855 -- low-level multipart operation tests.
@@ -670,15 +863,17 @@ multipartTest = funTestWithBucket "Multipart Tests" $
670863 randFile <- mkRandFile kb5
671864
672865 step "upload 2 parts"
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
866+ forM_ [1, 2] $ \partNum -> do
867+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
868+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
676869
677870 step "remove ongoing upload"
678871 removeIncompleteUpload bucket object
679− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
680− C..| sinkList
681− liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
872+ uploads <-
873+ C.runConduit $
874+ listIncompleteUploads bucket (Just object) False
875+ C..| sinkList
876+ liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
682877
683878putObjectContentTypeTest :: TestTree
684879putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
@@ -689,28 +884,39 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
689884
690885 step "create server object with content-type"
691886 inputFile <- mkRandFile size1
692− fPutObject bucket object inputFile defaultPutObjectOptions {
693− pooContentType = Just "application/javascript"
694− }
887+ fPutObject
888+ bucket
889+ object
890+ inputFile
891+ defaultPutObjectOptions
892+ { pooContentType = Just "application/javascript"
893+ }
695894
696895 -- retrieve obj info to check
697896 oi <- headObject bucket object []
698897 let m = oiMetadata oi
699898
700899 step "Validate content-type"
701− liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (Map.lookup "Content-Type" m)
900+ liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
702901
703902 step "upload object with content-encoding set to identity"
704− fPutObject bucket object inputFile defaultPutObjectOptions {
705− pooContentEncoding = Just "identity"
706− }
903+ fPutObject
904+ bucket
905+ object
906+ inputFile
907+ defaultPutObjectOptions
908+ { pooContentEncoding = Just "identity"
909+ }
707910
708911 oiCE <- headObject bucket object []
709912 let m' = oiMetadata oiCE
710913
711914 step "Validate content-encoding"
712− liftIO $ assertEqual "Content-Encoding did not match" (Just "identity")
713− (Map.lookup "Content-Encoding" m')
915+ liftIO $
916+ assertEqual
917+ "Content-Encoding did not match"
918+ (Just "identity")
919+ (H.lookup "Content-Encoding" m')
714920
715921 step "Cleanup actions"
716922
@@ -725,19 +931,103 @@ putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage test
725931
726932 step "create server object with content-language"
727933 inputFile <- mkRandFile size1
728− fPutObject bucket object inputFile defaultPutObjectOptions {
729− pooContentLanguage = Just "en-US"
730− }
934+ fPutObject
935+ bucket
936+ object
937+ inputFile
938+ defaultPutObjectOptions
939+ { pooContentLanguage = Just "en-US"
940+ }
731941
732942 -- retrieve obj info to check
733943 oi <- headObject bucket object []
734944 let m = oiMetadata oi
735945
736946 step "Validate content-language"
737− liftIO $ assertEqual "content-language did not match" (Just "en-US")
738− (Map.lookup "Content-Language" m)
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+
739990 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!"
7401029
1030+ step "Cleanup actions"
7411031 removeObject bucket object
7421032
7431033putObjectStorageClassTest :: TestTree
@@ -755,13 +1045,21 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
7551045 inputFile' <- mkRandFile size1
7561046 inputFile'' <- mkRandFile size0
7571047
758− fPutObject bucket object inputFile defaultPutObjectOptions {
759− pooStorageClass = Just "STANDARD"
760− }
761−
762− fPutObject bucket object' inputFile' defaultPutObjectOptions {
763− pooStorageClass = Just "REDUCED_REDUNDANCY"
764− }
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+ }
7651063
7661064 removeObject bucket object
7671065
@@ -770,15 +1068,24 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
7701068 let m' = oiMetadata oi'
7711069
7721070 step "Validate x-amz-storage-class rrs"
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− }
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+ }
7791086 case fpE of
7801087 Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
781− _ -> return ()
1088+ _ -> return ()
7821089
7831090 step "Cleanup actions"
7841091
@@ -797,7 +1104,7 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
7971104 fPutObject bucket object inputFile defaultPutObjectOptions
7981105
7991106 step "copy object"
800− let srcInfo = defaultSourceInfo { srcBucket = bucket, srcObject = object}
1107+ let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
8011108 (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
8021109
8031110 -- retrieve obj info to check
@@ -808,8 +1115,9 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
8081115
8091116 let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
8101117
811− liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
812− "Copied object did not match expected."
1118+ liftIO $
1119+ (s == size1 && e == etag && isMTimeDiffOk)
1120+ @? "Copied object did not match expected."
8131121
8141122 step "cleanup actions"
8151123 removeObject bucket object
@@ -830,12 +1138,18 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
8301138 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
8311139
8321140 step "put object parts 1-3"
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) []
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+ []
8391153 return (fromIntegral p, etag')
8401154
8411155 step "complete multipart"
@@ -881,11 +1195,13 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
8811195 fPutObject bucket src inputFile' defaultPutObjectOptions
8821196
8831197 step "copy last 10MiB of object"
884− copyObject defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj } defaultSourceInfo {
885− srcBucket = bucket
886− , srcObject = src
887− , srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
888− }
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+ }
8891205
8901206 step "verify uploaded object"
8911207 cSize <- oiSize <$> headObject bucket copyObj []
@@ -896,42 +1212,72 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
8961212
8971213getNPutSSECTest :: TestTree
8981214getNPutSSECTest =
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"
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)
test/Network/Minio/API/Test.hs+75−61
@@ -15,88 +15,102 @@
1515--
1616
1717module Network.Minio.API.Test
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
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
3331
3432assertBool' :: Bool -> Assertion
3533assertBool' = assertBool "Test failed!"
3634
3735bucketNameValidityTests :: TestTree
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− ]
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+ ]
5151
5252objectNameValidityTests :: TestTree
53−objectNameValidityTests = testGroup "Object Name Validity Tests"
54− [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""
55− , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
56− ]
53+objectNameValidityTests =
54+ testGroup
55+ "Object Name Validity Tests"
56+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName "",
57+ testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
58+ ]
5759
5860parseServerInfoJSONTest :: TestTree
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
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
6270 where
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− ]
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+ ]
6776 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−
6977 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−
7178 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\":[]}}}]"
7279
7380parseHealStatusTest :: TestTree
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−
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
7890 where
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−
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+ ]
8496 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−
8697 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−
8898 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}]}"
8999
90100parseHealStartRespTest :: TestTree
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−
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
95110 where
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−
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+ ]
100115 hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
101−
102116 missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
test/Network/Minio/JsonParser/Test.hs+26−27
@@ -15,26 +15,26 @@
1515--
1616
1717module Network.Minio.JsonParser.Test
18− (
19− jsonParserTests
20− ) where
18+ ( jsonParserTests,
19+ )
20+where
2121
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
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)
3028
3129jsonParserTests :: TestTree
32−jsonParserTests = testGroup "JSON Parser Tests"
33− [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34− ]
30+jsonParserTests =
31+ testGroup
32+ "JSON Parser Tests"
33+ [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34+ ]
3535
3636tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
37−tryValidationErr act = try act
37+tryValidationErr = try
3838
3939assertValidationErr :: MErrV -> Assertion
4040assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
@@ -44,21 +44,20 @@ testParseErrResponseJSON = do
4444 -- 1. Test parsing of an invalid error json.
4545 parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
4646 when (isRight parseResE) $
47− assertFailure $ "Parsing should have failed => " ++ show parseResE
47+ assertFailure $
48+ "Parsing should have failed => " ++ show parseResE
4849
4950 forM_ cases $ \(jsondata, sErr) -> do
5051 parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
5152 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− ,
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− )
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+ )
6463 ]
test/Network/Minio/TestHelpers.hs+8−8
@@ -15,18 +15,18 @@
1515--
1616
1717module Network.Minio.TestHelpers
18− ( runTestNS
19− ) where
18+ ( runTestNS,
19+ )
20+where
2021
21−import Network.Minio.Data
22+import Network.Minio.Data
2223
23−import Lib.Prelude
24−
25−newtype TestNS = TestNS { testNamespace :: Text }
24+newtype TestNS = TestNS {testNamespace :: Text}
2625
2726instance HasSvcNamespace TestNS where
2827 getSvcNamespace = testNamespace
2928
3029runTestNS :: ReaderT TestNS m a -> m a
31−runTestNS = flip runReaderT $
32− TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
30+runTestNS =
31+ flip runReaderT $
32+ TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs+15−17
@@ -15,33 +15,31 @@
1515--
1616
1717module Network.Minio.Utils.Test
18− (
19− limitedMapConcurrentlyTests
20− ) where
18+ ( limitedMapConcurrentlyTests,
19+ )
20+where
2121
22−import Test.Tasty
23−import Test.Tasty.HUnit
24−
25−import Lib.Prelude
26−
27−import Network.Minio.Utils
22+import Network.Minio.Utils
23+import Test.Tasty
24+import Test.Tasty.HUnit
2825
2926limitedMapConcurrentlyTests :: TestTree
30−limitedMapConcurrentlyTests = testGroup "limitedMapConcurrently Tests"
31− [ testCase "Test with various thread counts" testLMC
32− ]
27+limitedMapConcurrentlyTests =
28+ testGroup
29+ "limitedMapConcurrently Tests"
30+ [ testCase "Test with various thread counts" testLMC
31+ ]
3332
3433testLMC :: Assertion
3534testLMC = do
3635 let maxNum = 50
3736 -- test with thread count of 1 to 2*maxNum
38− forM_ [1..(2*maxNum)] $ \threads -> do
39− res <- limitedMapConcurrently threads compute [1..maxNum]
37+ forM_ [1 .. (2 * maxNum)] $ \threads -> do
38+ res <- limitedMapConcurrently threads compute [1 .. maxNum]
4039 sum res @?= overallResultCheck maxNum
4140 where
4241 -- simple function to run in each thread
4342 compute :: Int -> IO Int
44− compute n = return $ sum [1..n]
45−
43+ compute n = return $ sum [1 .. n]
4644 -- function to check overall result
47− overallResultCheck n = sum $ map (\t -> (t * (t+1)) `div` 2) [1..n]
45+ overallResultCheck n = sum $ map (\t -> (t * (t + 1)) `div` 2) [1 .. n]
test/Network/Minio/XmlGenerator/Test.hs+137−98
@@ -13,30 +13,33 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−
1716{-# LANGUAGE QuasiQuotes #-}
18−module Network.Minio.XmlGenerator.Test
19− ( xmlGeneratorTests
20− ) where
21−
22−import Test.Tasty
23−import Test.Tasty.HUnit
24−import Text.RawString.QQ (r)
2517
26−import Lib.Prelude
18+module Network.Minio.XmlGenerator.Test
19+ ( xmlGeneratorTests,
20+ )
21+where
2722
28−import Network.Minio.Data
29−import Network.Minio.TestHelpers
30−import Network.Minio.XmlGenerator
31−import Network.Minio.XmlParser (parseNotification)
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)
3233
3334xmlGeneratorTests :: TestTree
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− ]
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+ ]
4043
4144testMkCreateBucketConfig :: Assertion
4245testMkCreateBucketConfig = do
@@ -44,100 +47,136 @@ testMkCreateBucketConfig = do
4447 assertEqual "CreateBucketConfiguration xml should match: " expected $
4548 mkCreateBucketConfig ns "EU"
4649 where
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>"
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>"
5155
5256testMkCompleteMultipartUploadRequest :: Assertion
5357testMkCompleteMultipartUploadRequest =
5458 assertEqual "completeMultipartUpload xml should match: " expected $
55− mkCompleteMultipartUploadRequest [(1, "abc")]
59+ mkCompleteMultipartUploadRequest [(1, "abc")]
5660 where
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−
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>"
6468
6569testMkPutNotificationRequest :: Assertion
6670testMkPutNotificationRequest =
6771 forM_ cases $ \val -> do
6872 let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
69− result = toS $ mkPutNotificationRequest ns val
73+ result = fromStrictBS $ mkPutNotificationRequest ns val
7074 ntf <- runExceptT $ runTestNS $ parseNotification result
71− either (\_ -> assertFailure "XML Parse Error!")
72− (@?= val) ntf
75+ either
76+ (\_ -> assertFailure "XML Parse Error!")
77+ (@?= val)
78+ ntf
7379 where
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− ]
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+ ]
101121
102122testMkSelectRequest :: Assertion
103123testMkSelectRequest = mapM_ assertFn cases
104124 where
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 "\""
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 "\""
119144 )
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>
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>
130165</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
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− ]
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+ ]
test/Network/Minio/XmlParser/Test.hs+298−276
@@ -13,48 +13,49 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−
1716{-# LANGUAGE QuasiQuotes #-}
18−module Network.Minio.XmlParser.Test
19− ( xmlParserTests
20− ) where
21−
22−import qualified Data.Map as Map
23−import Data.Time (fromGregorian)
24−import Test.Tasty
25−import Test.Tasty.HUnit
26−import Text.RawString.QQ (r)
27−import UnliftIO (MonadUnliftIO)
2817
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
18+module Network.Minio.XmlParser.Test
19+ ( xmlParserTests,
20+ )
21+where
22+
23+import qualified Data.HashMap.Strict as H
24+import Data.Time (fromGregorian)
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)
3534
3635xmlParserTests :: TestTree
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− ]
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+ ]
4950
5051tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
51−tryValidationErr act = try act
52+tryValidationErr = try
5253
5354assertValidtionErr :: MErrV -> Assertion
5455assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
5556
5657eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
57−eitherValidationErr (Left e) _ = assertValidtionErr e
58+eitherValidationErr (Left e) _ = assertValidtionErr e
5859eitherValidationErr (Right a) f = f a
5960
6061testParseLocation :: Assertion
@@ -62,224 +63,224 @@ testParseLocation = do
6263 -- 1. Test parsing of an invalid location constraint xml.
6364 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
6465 when (isRight parseResE) $
65− assertFailure $ "Parsing should have failed => " ++ show parseResE
66+ assertFailure $
67+ "Parsing should have failed => " ++ show parseResE
6668
6769 forM_ cases $ \(xmldata, expectedLocation) -> do
6870 parseLocE <- tryValidationErr $ parseLocation xmldata
6971 either assertValidtionErr (@?= expectedLocation) parseLocE
7072 where
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− )
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+ )
8283 ]
8384
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
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
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
133132
134133 parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
135134 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
136135
137136testParseListObjectsV1Result :: Assertion
138137testParseListObjectsV1Result = do
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
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
160158
161159 parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
162160 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
163161
164162testParseListIncompleteUploads :: Assertion
165163testParseListIncompleteUploads = do
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/"]
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/"]
202200
203201 parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
204202 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
205203
206−
207204testParseCompleteMultipartUploadResponse :: Assertion
208205testParseCompleteMultipartUploadResponse = do
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\""
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\""
218215
219216 parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
220217 eitherValidationErr parsedETagE (@?= expectedETag)
221218
222219testParseListPartsResponse :: Assertion
223220testParseListPartsResponse = do
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
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
262258
263259 parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
264260 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
265261
266262testParseCopyObjectResponse :: Assertion
267263testParseCopyObjectResponse = do
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))]
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+ ]
283284
284285 forM_ cases $ \(xmldata, (etag, modTime)) -> do
285286 parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
@@ -287,73 +288,89 @@ testParseCopyObjectResponse = do
287288
288289testParseNotification :: Assertion
289290testParseNotification = do
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
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
301303 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
302304 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
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− ]
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+ ]
357374
358375 forM_ cases $ \(xmldata, val) -> do
359376 result <- runExceptT $ runTestNS $ parseNotification xmldata
@@ -362,20 +379,25 @@ testParseNotification = do
362379-- | Tests parsing of both progress and stats
363380testParseSelectProgress :: Assertion
364381testParseSelectProgress = do
365− let cases = [ ([r|<?xml version="1.0" encoding="UTF-8"?>
382+ let cases =
383+ [ ( [r|<?xml version="1.0" encoding="UTF-8"?>
366384<Progress>
367385 <BytesScanned>512</BytesScanned>
368386 <BytesProcessed>1024</BytesProcessed>
369387 <BytesReturned>1024</BytesReturned>
370−</Progress>|] , Progress 512 1024 1024)
371− , ([r|<?xml version="1.0" encoding="UTF-8"?>
388+</Progress>|],
389+ Progress 512 1024 1024
390+ ),
391+ ( [r|<?xml version="1.0" encoding="UTF-8"?>
372392<Stats>
373393 <BytesScanned>512</BytesScanned>
374394 <BytesProcessed>1024</BytesProcessed>
375395 <BytesReturned>1024</BytesReturned>
376−</Stats>|], Progress 512 1024 1024)
377− ]
378−
379− forM_ cases $ \(xmldata, progress) -> do
380− result <- runExceptT $ parseSelectProgress xmldata
381− eitherValidationErr result (@?= progress)
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)
test/Spec.hs+83−82
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 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,21 +14,17 @@
1414-- limitations under the License.
1515--
1616
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
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
3228
3329main :: IO ()
3430main = defaultMain tests
@@ -51,82 +47,87 @@ properties = testGroup "Properties" [qcProps] -- [scProps]
5147-- ]
5248
5349qcProps :: TestTree
54−qcProps = testGroup "(checked by QuickCheck)"
55− [ QC.testProperty "selectPartSizes:" $
56− \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
57−
50+qcProps =
51+ testGroup
52+ "(checked by QuickCheck)"
53+ [ QC.testProperty "selectPartSizes:" $
54+ \n ->
55+ let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
5856 -- check that pns increments from 1.
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−
57+ isPNumsAscendingFrom1 = all (uncurry (==)) $ zip pns [1 ..]
58+ consPairs [] = []
59+ consPairs [_] = []
60+ consPairs (a : (b : c)) = (a, b) : consPairs (b : c)
6561 -- check `offs` is monotonically increasing.
66− isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs
67−
62+ isOffsetsAsc = all (uncurry (<)) $ consPairs offs
6863 -- check sizes sums to n.
6964 isSumSizeOk = sum sizes == n
70−
7165 -- check sizes are constant except last
7266 isSizesConstantExceptLast =
73− all (\(a, b) -> a == b) (consPairs $ L.init sizes)
74−
67+ all (uncurry (==)) (consPairs $ L.init sizes)
7568 -- check each part except last is at least minPartSize;
7669 -- last part may be 0 only if it is the only part.
7770 nparts = length sizes
7871 isMinPartSizeOk =
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
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
121118 Nothing -> B.length bs /= 32
122− ]
119+ ]
123120
124121unitTests :: TestTree
125−unitTests = testGroup "Unit tests" [ xmlGeneratorTests, xmlParserTests
126− , bucketNameValidityTests
127− , objectNameValidityTests
128− , parseServerInfoJSONTest
129− , parseHealStatusTest
130− , parseHealStartRespTest
131− , limitedMapConcurrentlyTests
132− ]
122+unitTests =
123+ testGroup
124+ "Unit tests"
125+ [ xmlGeneratorTests,
126+ xmlParserTests,
127+ bucketNameValidityTests,
128+ objectNameValidityTests,
129+ parseServerInfoJSONTest,
130+ parseHealStatusTest,
131+ parseHealStartRespTest,
132+ limitedMapConcurrentlyTests
133+ ]
test/cert/private.keynew file+28
@@ -0,0 +1,28 @@
1+-----BEGIN PRIVATE KEY-----
2+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC3G9IiC+adjf0p
3+i/2KYc+4dizeuzUFN7wraSdhiOMdQgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwI
4+rPJ61dRGQSuN12l+mzngFJQjE0sysZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPY
5+fhcUcq03onMGq44yOfE6mIhoe0Y9wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq
6+5KjHL8WW2vGg7G9edpYdxINA/A2fdLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L
7+9yRqItqddriRxJFwOXb5OPW8xx2WGaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A8
8+4TD/RXMbAgMBAAECggEBAJ7r1oUWLyGvinn0tijUm6RNbMQjVvEgXoCO008jr3pF
9+PqxVpgEMrOa/4tmwFBus0jcCNF4t3r2zhddBw3I5A/O1vEdvHnBz6NdDBQ8sP6fP
10+1fF50iEe1Y2MBibQkXFxxVMG2QRB1Gt5nuvXA9ELdqtCovK3EsMk5ukkWb/UvjH5
11+8hcmQsaSqvzFEF4wJSY2mkeGSGIJTphPhhuA22xbhaBMInQyhZu8EHsn0h6s/Wgy
12+C4Cp2+4qZTKaaf6x3/ZjJ8CuKiSX+ZsJKjOEv8sqx7j/Y7QFOmJPewInKDhwazr/
13+xIK+N0KXPbUzeSEz6ZvExNDTxtR5ZlQP2UrRDg28yQECgYEA4Is1O2BvKVzNFOkj
14+bTVz25a/bb0Xrcfgi0Y9rdfLzlNdItFjAkxLTVRSW2Hv9ICl0RDDAG+wTlktXRdh
15+rfvDjwG2CvLQo1VEdMWTTkKVg03SwMEy2hFiWV69lENFGSaY8Y6unZDbia5HQinA
16+EgSS4sCojS+a2jtzG5FVVHJDKlkCgYEA0MKhMhD4SUhr2y1idPBrmLxuW5mVozuW
17+8bYaBeSzmfS0BRsN4fP9JGODPBPDdNbfGfGC9ezWLgD/lmCgjIEyBOq8EmqWSsiS
18+Kihds1+Z7hXtbzGsFGAFJJTIh7blBCsK5QFuyuih2UG0fL9z6K/dy+UUJkzrYqph
19+vSfKixyM8pMCgYEAmUPLsNyw4325aeV8TeWnUCJERaZFDFQa21W1cfyS2yEhuEtN
20+llr3JzBACqn9vFk3VU1onNqfb8sE4L696KCpKeqUFEMK0AG6eS4Gzus53Gb5TKJS
21+kHA/PhshsZp9Bp7G1FJ8s4YVo5N2hh2zQVkn3Wh9Y+kzfHQJrK51nO9lEvkCgYBi
22+BuKWle1gzAcJdnhDHRoJMIJJtQbVDYhFnBMALXJAmu1lcFzGe0GlMq1PKqCfXr6I
23+eiXawQmZtJJP1LPPBmOsd2U06KQGHcS00xucvQmVCOrjSdnZ/3SqxsqbH8DOgj+t
24+ZUzXLwHA+N99rJEK9Hob4kfh7ECjpgobPnIXfKKazQKBgQChAuiXHtf/Qq18hY3u
25+x48zFWjGgfd6GpOBZYkXOwGdCJgnYjZbE26LZEnYbwPh8ZUA2vp7mgHRJkD5e3Fj
26+ERuJLCw86WqyYZmLEuBciYGjCZqR5nbavfwsziWD00jeNruds2ZwKxRfFm4V7o2S
27+WLd/RUatd2Uu9f3B2J78OUdnxg==
28+-----END PRIVATE KEY-----
test/cert/public.crtnew file+19
@@ -0,0 +1,19 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDCzCCAfOgAwIBAgIUaIUOMI78LCu+r1zl0mmFHK8n5/AwDQYJKoZIhvcNAQEL
3+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE5MTAyNDE5NTMxOVoYDzIxMTkw
4+OTMwMTk1MzE5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
5+AQUAA4IBDwAwggEKAoIBAQC3G9IiC+adjf0pi/2KYc+4dizeuzUFN7wraSdhiOMd
6+QgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwIrPJ61dRGQSuN12l+mzngFJQjE0sy
7+sZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPYfhcUcq03onMGq44yOfE6mIhoe0Y9
8+wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq5KjHL8WW2vGg7G9edpYdxINA/A2f
9+dLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L9yRqItqddriRxJFwOXb5OPW8xx2W
10+GaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A84TD/RXMbAgMBAAGjUzBRMB0GA1Ud
11+DgQWBBSEWXQ2JRD+OK7/KTmlD+OW16pGmzAfBgNVHSMEGDAWgBSEWXQ2JRD+OK7/
12+KTmlD+OW16pGmzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCF
13+0zYRaabB3X0jzGI9/Lr3Phrb90GvoL1DFLRuiOuTlDkz0vrm/HrZskwHCgMNrkCj
14+OTD9Vpas4D1QZBbQbRzfnf3OOoG4bgmcCwLFZl3dy27yIDAhrmbUP++g9l1Jmy4v
15+vBR/M4lt2scQ8LcZYEPqhEaE5EzFQEjtaxDcKdWDNKY9W1NUzSIABhF9eHiAUNdH
16+AFNJlYeBlCHxcWIeqgon184Dqp/CsvKtz3z3Ni+rlwPM/zuJCFHh1VF+z++0LJjG
17+roBCV0Tro4XyiEz9yp7Cb5kQYMaj1KL9TqBG0tZx0pmv7y+lXc4TT6DEllXz6USy
18+rbIba9/uUet3BqeIMTqj
19+-----END CERTIFICATE-----