git.iko.soy/typeable/minio-hs
on master help sign in
70 files changed+6685−4777
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml+266 .stylish-haskell.yaml−233 .travis.yml−61 CHANGELOG.md+31 CNAME+1 README.md+50−29 Setup.hs−18 examples/AssumeRole.hs+47 examples/BucketExists.hs+4−7 examples/CopyObject.hs+12−14 examples/FileUploader.hs+23−24 examples/GetConfig.hs+6−6 examples/GetObject.hs+5−10 examples/HeadObject.hs+8−11 examples/Heal.hs+11−8 examples/ListBuckets.hs+3−5 examples/ListIncompleteUploads.hs+17−19 examples/ListObjects.hs+13−15 examples/MakeBucket.hs+6−9 examples/PresignedGetObject.hs+32−28 examples/PresignedPostPolicy.hs+42−39 examples/PresignedPutObject.hs+20−21 examples/PutObject.hs+11−14 examples/RemoveBucket.hs+3−8 examples/RemoveIncompleteUpload.hs+7−10 examples/RemoveObject.hs+7−8 examples/SelectObject.hs+24−26 examples/ServerInfo.hs+7−7 examples/ServiceSendRestart.hs+6−7 examples/ServiceSendStop.hs+6−7 examples/ServiceStatus.hs+7−7 examples/SetConfig.hs+3−4 minio-hs.cabal+73−17 src/Lib/Prelude.hs+33−11 src/Network/Minio.hs+249−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+697−559 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+481−319 src/Network/Minio/SelectAPI.hs+233−238 src/Network/Minio/Sign/V4.hs+430−327 src/Network/Minio/Utils.hs+140−127 src/Network/Minio/XmlCommon.hs+65 src/Network/Minio/XmlGenerator.hs+165−99 src/Network/Minio/XmlParser.hs+107−154 stack.yaml+4−4 stack.yaml.lock+19 test/LiveServer.hs+755−481 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−61
@@ -1,61 +0,0 @@
1−sudo: true
2−language: haskell
3−
4−git:
5− depth: 5
6−
7−cabal: "3.0"
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.4.4
20− - ghc: 8.6.5
21− - ghc: 8.8.1
22−
23− # Stack
24− - ghc: 8.6.5
25− env: STACK_YAML="$TRAVIS_BUILD_DIR/stack.yaml"
26−
27−before_install:
28− - sudo apt-get install devscripts
29− - mkdir /tmp/minio /tmp/certs
30− - (cd /tmp/minio; wget https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
31− - (cd /tmp/certs; openssl req -newkey rsa:2048 -nodes -keyout private.key -x509 -days 36500 -out public.crt -subj "/C=US/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=localhost/emailAddress=email@example.com")
32− - sudo cp /tmp/certs/public.crt /usr/local/share/ca-certificates/
33− - sudo update-ca-certificates
34− - MINIO_ACCESS_KEY=minio MINIO_SECRET_KEY=minio123 /tmp/minio/minio server --quiet --certs-dir /tmp/certs data 2>&1 > minio.log &
35−
36−install:
37− - |
38− if [ -z "$STACK_YAML" ]; then
39− ghc --version
40− cabal --version
41− cabal new-update
42− cabal new-build --enable-tests --enable-benchmarks -fexamples
43− else
44− # install stack
45− curl -sSL https://get.haskellstack.org/ | sh
46−
47− # build project with stack
48− stack --version
49− stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --flag minio-hs:examples
50− fi
51−
52−script:
53− - |
54− if [ -z "$STACK_YAML" ]; then
55− MINIO_LOCAL=1 MINIO_SECURE=1 cabal new-test --enable-tests -flive-test
56− else
57− MINIO_LOCAL=1 MINIO_SECURE=1 stack test --system-ghc --flag minio-hs:live-test
58− fi
59−
60−notifications:
61− email: false
CHANGELOG.md+31
@@ -1,6 +1,37 @@
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+
435## Version 1.5.2
536
637* Fix region `us-west-2` for AWS S3 (#139)
CNAMEnew file+1
@@ -0,0 +1 @@
1+minio-hs.min.io
README.md+50−29
@@ -1,10 +1,8 @@
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
@@ -12,20 +10,35 @@ The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.
1210
1311Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
1412
15−### Try it out directly with `ghci`
13+### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
14+
15+#### For a cabal based environment
16+
17+Download the library source and change to the extracted directory:
18+
19+``` sh
20+$ cabal get minio-hs
21+$ cd minio-hs-1.6.0/ # directory name could be different
22+```
23+
24+Then load the `ghci` REPL environment with the library and browse the available APIs:
25+
26+``` sh
27+$ cabal repl
28+ghci> :browse Network.Minio
29+```
30+
31+#### For a stack based environment
1632
1733From your home folder or any non-haskell project directory, just run:
1834
1935```sh
20−
2136stack install minio-hs
22−
2337```
2438
2539Then start an interpreter session and browse the available APIs with:
2640
2741```sh
28−
2942$ stack ghci
3043> :browse Network.Minio
3144```
@@ -134,44 +147,52 @@ main = do
134147
135148### Development
136149
137−To setup:
150+#### Download the source
138151
139152```sh
140−git clone https://github.com/minio/minio-hs.git
141−
142−cd minio-hs/
153+$ git clone https://github.com/minio/minio-hs.git
154+$ cd minio-hs/
155+```
143156
144−stack install
145−```
157+#### Build the package:
146158
147−Tests can be run with:
159+With `cabal`:
148160
149161```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+```
150166
151−stack test
167+With `stack`:
152168
169+``` sh
170+$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
153171```
172+#### Running tests:
154173
155−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`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
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).
156175
157−To run the live server tests, set a build flag as shown below:
176+With `cabal`:
158177
159178```sh
179+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
180+$ cabal test
181+```
160182
161−stack test --flag minio-hs:live-test
162−
163−# OR against a local MinIO server with:
164−
165−MINIO_LOCAL=1 stack test --flag minio-hs:live-test
183+With `stack`:
166184
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
167188```
168189
169−The configured CI system always runs both test-suites for every change.
190+This will run all the test suites.
170191
171−Documentation can be locally built with:
192+#### Building documentation:
172193
173194```sh
174−
175−stack haddock
176−
195+$ cabal haddock
196+$ # OR
197+$ stack haddock
177198```
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
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+4−7
@@ -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
2726-- 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+12−14
@@ -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 UnliftIO.Exception (catch, throwIO)
21+import Network.Minio
22+import UnliftIO.Exception (catch, throwIO)
2423
2524-- | The following example uses minio's play server at
2625-- 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 = "test"
31+ let bucket = "test"
3632 object = "obj"
3733 objectCopy = "obj-copy"
3834 localFile = "/etc/lsb-release"
3935
4036 res1 <- runMinio minioPlayCI $ do
4137 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
42− catch (makeBucket bucket Nothing) (
43− \e -> case e of
44− BucketAlreadyOwnedByYou -> return ()
45− _ -> throwIO e
38+ catch
39+ (makeBucket bucket Nothing)
40+ ( \e -> case e of
41+ BucketAlreadyOwnedByYou -> return ()
42+ _ -> throwIO e
4643 )
4744
4845 -- 2. Upload a file to bucket/object.
4946 fPutObject bucket object localFile defaultPutObjectOptions
5047
5148 -- 3. Copy bucket/object to bucket/objectCopy.
52− copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
53− defaultSourceInfo { srcBucket = bucket , srcObject = object }
49+ copyObject
50+ defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
51+ defaultSourceInfo {srcBucket = bucket, srcObject = object}
5452
5553 case res1 of
56− Left e -> putStrLn $ "copyObject failed." ++ show e
54+ Left e -> putStrLn $ "copyObject failed." ++ show e
5755 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+23−24
@@ -16,40 +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
3430-- 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
4643cmdParser :: ParserInfo FilePath
47−cmdParser = info
48− (helper <*> fileNameArgs)
49− (fullDesc
50− <> progDesc "FileUploader"
51− <> header
52− "FileUploader - a simple file-uploader program using minio-hs")
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+ )
5352
5453main :: IO ()
5554main = do
@@ -64,12 +63,12 @@ main = do
6463 bErr <- try $ makeBucket bucket Nothing
6564 case bErr of
6665 Left BucketAlreadyOwnedByYou -> return ()
67− Left e -> throwIO e
68− Right _ -> return ()
66+ Left e -> throwIO e
67+ Right _ -> return ()
6968
7069 -- Upload filepath to bucket; object is derived from filepath.
7170 fPutObject bucket object filepath defaultPutObjectOptions
7271
7372 case res of
74− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
73+ Left e -> putStrLn $ "file upload failed due to " ++ show e
7574 Right () -> putStrLn "file upload succeeded."
examples/GetConfig.hs+6−6
@@ -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 minioPlayCI $
26+ res <-
27+ runMinio
28+ minioPlayCI
2929 getConfig
3030 print res
examples/GetObject.hs+5−10
@@ -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
2927-- 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
4136 src <- getObject bucket object defaultGetObjectOptions
4237 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+8−11
@@ -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
2726-- 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+11−8
@@ -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
2826 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+3−5
@@ -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
2726-- 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+17−19
@@ -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
2726-- 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+13−15
@@ -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
2826-- 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+6−9
@@ -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
2725-- 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+32−28
@@ -16,39 +16,37 @@
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
3029-- 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..."
5048 putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
51− liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
49+ liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
5250
5351 -- Extract Etag of uploaded object
5452 oi <- statObject bucket object defaultGetObjectOptions
@@ -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
7174 Right (headers, _, url) -> do
72−
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+42−39
@@ -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.HashMap.Strict as H
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
3029-- 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 = "photos/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 object
51− -- set the bucket name condition
52− , ppCondBucket 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 $ H.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+20−21
@@ -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
2726-- 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+11−14
@@ -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
2826-- 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) defaultPutObjectOptions
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 defaultPutObjectOptions
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/RemoveBucket.hs+3−8
@@ -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
2725-- 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+7−10
@@ -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
2625-- 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+7−8
@@ -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+24−26
@@ -16,34 +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)
2520
26−import Prelude
21+import qualified Conduit as C
22+import Control.Monad (unless)
23+import Network.Minio
24+import Prelude
2725
2826main :: IO ()
2927main = do
30− let bucket = "selectbucket"
31− object = "1.csv"
32− content = "Name,Place,Temperature\n"
33− <> "James,San Jose,76\n"
34− <> "Alicia,San Leandro,88\n"
35− <> "Mark,San Carlos,90\n"
36−
37− res <- runMinio minioPlayCI $ do
38−
39− exists <- bucketExists bucket
40− when (not exists) $
41− makeBucket bucket Nothing
42−
43− C.liftIO $ putStrLn "Uploading csv object"
44− putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
45−
46− let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
47− res <- selectObjectContent bucket object sr
48− C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
49− 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+7−7
@@ -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 minioPlayCI $
29− getServerInfo
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ getServerInfo
3030 print res
examples/ServiceSendRestart.hs+6−7
@@ -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 minioPlayCI $
29− serviceSendAction ServiceActionRestart
26+ res <-
27+ runMinio minioPlayCI $
28+ serviceSendAction ServiceActionRestart
3029 print res
examples/ServiceSendStop.hs+6−7
@@ -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 minioPlayCI $
29− serviceSendAction ServiceActionStop
26+ res <-
27+ runMinio minioPlayCI $
28+ serviceSendAction ServiceActionStop
3029 print res
examples/ServiceStatus.hs+7−7
@@ -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 minioPlayCI $
29− serviceStatus
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ serviceStatus
3030 print res
examples/SetConfig.hs+3−4
@@ -16,12 +16,11 @@
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
minio-hs.cabal+73−17
@@ -1,6 +1,6 @@
1−cabal-version: 2.2
1+cabal-version: 2.4
22name: minio-hs
3−version: 1.5.2
3+version: 1.7.0
44synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
55 storage.
66description: The MinIO Haskell client library provides simple APIs to
@@ -14,29 +14,70 @@ maintainer: dev@min.io
1414category: Network, AWS, Object Storage
1515build-type: Simple
1616stability: Experimental
17−extra-source-files:
17+extra-doc-files:
1818 CHANGELOG.md
1919 CONTRIBUTING.md
2020 docs/API.md
21− examples/*.hs
2221 README.md
22+extra-source-files:
23+ examples/*.hs
2324 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
2431
32+source-repository head
33+ type: git
34+ location: https://github.com/minio/minio-hs.git
35+
36+Flag dev
37+ Description: Build package in development mode
38+ Default: False
39+ Manual: True
2540
2641common base-settings
2742 ghc-options: -Wall
43+ -Wcompat
44+ -Widentities
45+ -Wincomplete-uni-patterns
46+ -Wincomplete-record-updates
47+ -haddock
48+ if impl(ghc >= 8.0)
49+ ghc-options: -Wredundant-constraints
50+ if impl(ghc >= 8.2)
51+ ghc-options: -fhide-source-paths
52+ if impl(ghc >= 8.4)
53+ ghc-options: -Wpartial-fields
54+ -- -Wmissing-export-lists
55+ if impl(ghc >= 8.8)
56+ ghc-options: -Wmissing-deriving-strategies
57+ -Werror=missing-deriving-strategies
58+ -- if impl(ghc >= 8.10)
59+ -- ghc-options: -Wunused-packages -- disabled due to bug related to mixin config
60+ if impl(ghc >= 9.0)
61+ ghc-options: -Winvalid-haddock
62+ if impl(ghc >= 9.2)
63+ ghc-options: -Wredundant-bang-patterns
64+ if flag(dev)
65+ ghc-options: -Werror
66+
2867 default-language: Haskell2010
68+
2969 default-extensions: BangPatterns
70+ , DerivingStrategies
3071 , FlexibleContexts
3172 , FlexibleInstances
73+ , LambdaCase
3274 , MultiParamTypeClasses
3375 , MultiWayIf
34− , NoImplicitPrelude
3576 , OverloadedStrings
3677 , RankNTypes
3778 , ScopedTypeVariables
38− , TypeFamilies
3979 , TupleSections
80+
4081 other-modules: Lib.Prelude
4182 , Network.Minio.API
4283 , Network.Minio.APICommon
@@ -54,22 +95,31 @@ common base-settings
5495 , Network.Minio.Utils
5596 , Network.Minio.XmlGenerator
5697 , Network.Minio.XmlParser
98+ , Network.Minio.XmlCommon
5799 , 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+
58108 build-depends: base >= 4.7 && < 5
59− , protolude >= 0.2 && < 0.3
60− , aeson >= 1.2
109+ , relude >= 0.7 && < 2
110+ , aeson >= 1.2 && < 3
61111 , base64-bytestring >= 1.0
62112 , binary >= 0.8.5.0
63113 , bytestring >= 0.10
64114 , case-insensitive >= 1.2
65115 , conduit >= 1.3
66116 , conduit-extra >= 1.3
67− , connection
117+ , crypton-connection
68118 , cryptonite >= 0.25
69119 , cryptonite-conduit >= 0.2
120+ , data-default-class
70121 , digest >= 0.0.1
71122 , directory
72− , exceptions
73123 , filepath >= 1.4
74124 , http-client >= 0.5
75125 , http-client-tls
@@ -77,14 +127,15 @@ common base-settings
77127 , http-types >= 0.12
78128 , ini
79129 , memory >= 0.14
80− , raw-strings-qq >= 1
130+ , network-uri
81131 , resourcet >= 1.2
82132 , retry
83133 , text >= 1.2
84− , time >= 1.8
134+ , time >= 1.9
135+ , time-units ^>= 1.0.0
85136 , transformers >= 0.5
86− , unliftio >= 0.2
87− , unliftio-core >= 0.1
137+ , unliftio >= 0.2 && < 0.3
138+ , unliftio-core >= 0.2 && < 0.3
88139 , unordered-containers >= 0.2
89140 , xml-conduit >= 1.8
90141
@@ -115,7 +166,9 @@ test-suite minio-hs-live-server-test
115166 , Network.Minio.Utils.Test
116167 , Network.Minio.XmlGenerator.Test
117168 , Network.Minio.XmlParser.Test
169+ , Network.Minio.Credentials
118170 build-depends: minio-hs
171+ , raw-strings-qq
119172 , tasty
120173 , tasty-hunit
121174 , tasty-quickcheck
@@ -130,6 +183,7 @@ test-suite minio-hs-test
130183 hs-source-dirs: test, src
131184 main-is: Spec.hs
132185 build-depends: minio-hs
186+ , raw-strings-qq
133187 , QuickCheck
134188 , tasty
135189 , tasty-hunit
@@ -146,6 +200,7 @@ test-suite minio-hs-test
146200 , Network.Minio.Utils.Test
147201 , Network.Minio.XmlGenerator.Test
148202 , Network.Minio.XmlParser.Test
203+ , Network.Minio.Credentials
149204
150205Flag examples
151206 Description: Build the examples
@@ -292,6 +347,7 @@ executable SetConfig
292347 scope: private
293348 main-is: SetConfig.hs
294349
295−source-repository head
296− type: git
297− location: https://github.com/minio/minio-hs
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+249−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,224 +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--
2323-- Types and functions to conveniently access S3 compatible object
2424-- storage servers like MinIO.
25−
2625module Network.Minio
27− (
28− -- * Credentials
29− Credentials (..)
30−
31− -- ** Credential providers
32− -- | Run actions that retrieve 'Credentials' from the environment or
33− -- files or other custom sources.
34− , Provider
35− , fromAWSConfigFile
36− , fromAWSEnv
37− , fromMinioEnv
38− , findFirst
39−
40− -- * Connecting to object storage
41− , ConnectInfo
42− , setRegion
43− , setCreds
44− , setCredsFrom
45− , isConnectInfoSecure
46− , disableTLSCertValidation
47− , MinioConn
48− , mkMinioConn
49−
50− -- ** Connection helpers
51− -- | These are helpers to construct 'ConnectInfo' values for common
52− -- cases.
53− , minioPlayCI
54− , awsCI
55− , gcsCI
56−
57− -- * Minio Monad
58− ----------------
59− -- | The Minio Monad provides connection-reuse, bucket-location
60− -- caching, resource management and simpler error handling
61− -- functionality. All actions on object storage are performed within
62− -- this Monad.
63− , Minio
64− , runMinioWith
65− , runMinio
66− , runMinioResWith
67− , runMinioRes
68−
69− -- * Bucket Operations
70−
71− -- ** Creation, removal and querying
72− , Bucket
73− , makeBucket
74− , removeBucket
75− , bucketExists
76− , Region
77− , getLocation
78−
79− -- ** Listing buckets
80− , BucketInfo(..)
81− , listBuckets
82−
83− -- ** Listing objects
84− , listObjects
85− , listObjectsV1
86− , ListItem(..)
87−
88− , ObjectInfo
89− , oiObject
90− , oiModTime
91− , oiETag
92− , oiSize
93− , oiUserMetadata
94− , oiMetadata
95−
96− -- ** Listing incomplete uploads
97− , listIncompleteUploads
98− , UploadId
99− , UploadInfo(..)
100− , listIncompleteParts
101− , ObjectPartInfo(..)
102−
103− -- ** Bucket Notifications
104− , getBucketNotification
105− , putBucketNotification
106− , removeAllBucketNotification
107− , Notification(..)
108− , defaultNotification
109− , NotificationConfig(..)
110− , Arn
111− , Event(..)
112− , Filter(..)
113− , defaultFilter
114− , FilterKey(..)
115− , defaultFilterKey
116− , FilterRules(..)
117− , defaultFilterRules
118− , FilterRule(..)
119−
120− -- * Object Operations
121− , Object
122−
123− -- ** File-based operations
124− , fGetObject
125− , fPutObject
126−
127− -- ** Conduit-based streaming operations
128− , putObject
129− , PutObjectOptions
130− , defaultPutObjectOptions
131− , pooContentType
132− , pooContentEncoding
133− , pooContentDisposition
134− , pooContentLanguage
135− , pooCacheControl
136− , pooStorageClass
137− , pooUserMetadata
138− , pooNumThreads
139− , pooSSE
140−
141− , getObject
142− , GetObjectOptions
143− , defaultGetObjectOptions
144− , gooRange
145− , gooIfMatch
146− , gooIfNoneMatch
147− , gooIfModifiedSince
148− , gooIfUnmodifiedSince
149− , gooSSECKey
150− , GetObjectResponse
151− , gorObjectInfo
152− , gorObjectStream
153−
154− -- ** Server-side object copying
155− , copyObject
156− , SourceInfo
157− , defaultSourceInfo
158− , srcBucket
159− , srcObject
160− , srcRange
161− , srcIfMatch
162− , srcIfNoneMatch
163− , srcIfModifiedSince
164− , srcIfUnmodifiedSince
165− , DestinationInfo
166− , defaultDestinationInfo
167− , dstBucket
168− , dstObject
169−
170− -- ** Querying object info
171− , statObject
172−
173− -- ** Object removal operations
174− , removeObject
175− , removeIncompleteUpload
176−
177− -- ** Select Object Content with SQL
178− , module Network.Minio.SelectAPI
179−
180− -- * Server-Side Encryption Helpers
181− , mkSSECKey
182− , SSECKey
183− , SSE(..)
184−
185− -- * Presigned Operations
186− , presignedPutObjectUrl
187− , presignedGetObjectUrl
188− , presignedHeadObjectUrl
189− , UrlExpiry
190−
191− -- ** POST (browser) upload helpers
192− -- | Please see
193− -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
194− -- for detailed information.
195− , newPostPolicy
196− , presignedPostPolicy
197− , showPostPolicy
198− , PostPolicy
199− , PostPolicyError(..)
200−
201− -- *** Post Policy condition helpers
202− , PostPolicyCondition
203− , ppCondBucket
204− , ppCondContentLengthRange
205− , ppCondContentType
206− , ppCondKey
207− , ppCondKeyStartsWith
208− , ppCondSuccessActionStatus
209−
210− -- * Error handling
211− -- | Data types representing various errors that may occur while
212− -- working with an object storage service.
213− , MinioErr(..)
214− , MErrV(..)
215− , ServiceErr(..)
216−
217−) 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
218233
219234{-
220235This module exports the high-level MinIO API for object storage.
221236-}
222237
223−import qualified Data.Conduit as C
224−import qualified Data.Conduit.Binary as CB
238+import qualified Data.Conduit as C
239+import qualified Data.Conduit.Binary as CB
225240import qualified Data.Conduit.Combinators as CC
226−
227−import Lib.Prelude
228−
229−import Network.Minio.CopyObject
230−import Network.Minio.Data
231−import Network.Minio.Errors
232−import Network.Minio.ListOps
233−import Network.Minio.PutObject
234−import Network.Minio.S3API
235−import Network.Minio.SelectAPI
236−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
237250
238251-- | Lists buckets.
239252listBuckets :: Minio [BucketInfo]
@@ -248,8 +261,12 @@ fGetObject bucket object fp opts = do
248261 C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
249262
250263-- | Upload the given file to the given object.
251−fPutObject :: Bucket -> Object -> FilePath
252− -> PutObjectOptions -> Minio ()
264+fPutObject ::
265+ Bucket ->
266+ Object ->
267+ FilePath ->
268+ PutObjectOptions ->
269+ Minio ()
253270fPutObject bucket object f opts =
254271 void $ putObjectInternal bucket object opts $ ODFile f Nothing
255272
@@ -257,8 +274,13 @@ fPutObject bucket object f opts =
257274-- known; this helps the library select optimal part sizes to perform
258275-- a multipart upload. If not specified, it is assumed that the object
259276-- can be potentially 5TiB and selects multipart sizes appropriately.
260−putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio ()
261− -> Maybe Int64 -> PutObjectOptions -> Minio ()
277+putObject ::
278+ Bucket ->
279+ Object ->
280+ C.ConduitM () ByteString Minio () ->
281+ Maybe Int64 ->
282+ PutObjectOptions ->
283+ Minio ()
262284putObject bucket object src sizeMay opts =
263285 void $ putObjectInternal bucket object opts $ ODStream src sizeMay
264286
@@ -268,18 +290,25 @@ putObject bucket object src sizeMay opts =
268290-- copy operation if the new object is to be greater than 5GiB in
269291-- size.
270292copyObject :: DestinationInfo -> SourceInfo -> Minio ()
271−copyObject dstInfo srcInfo = void $ copyObjectInternal (dstBucket dstInfo)
272− (dstObject dstInfo) srcInfo
293+copyObject dstInfo srcInfo =
294+ void $
295+ copyObjectInternal
296+ (dstBucket dstInfo)
297+ (dstObject dstInfo)
298+ srcInfo
273299
274300-- | Remove an object from the object store.
275301removeObject :: Bucket -> Object -> Minio ()
276302removeObject = deleteObject
277303
278304-- | Get an object from the object store.
279−getObject :: Bucket -> Object -> GetObjectOptions
280− -> Minio GetObjectResponse
305+getObject ::
306+ Bucket ->
307+ Object ->
308+ GetObjectOptions ->
309+ Minio GetObjectResponse
281310getObject bucket object opts =
282− getObject' bucket object [] $ gooToHeaders opts
311+ getObject' bucket object [] $ gooToHeaders opts
283312
284313-- | Get an object's metadata from the object store. It accepts the
285314-- same options as GetObject.
@@ -309,6 +338,8 @@ bucketExists = headBucket
309338-- | Removes an ongoing multipart upload of an object.
310339removeIncompleteUpload :: Bucket -> Object -> Minio ()
311340removeIncompleteUpload bucket object = do
312− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
313− C..| CC.sinkList
341+ uploads <-
342+ C.runConduit $
343+ listIncompleteUploads bucket (Just object) False
344+ C..| CC.sinkList
314345 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.HashMap.Strict as H
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 (H.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+697−559
@@ -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,45 +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 qualified Control.Concurrent.MVar as M
25−import Control.Monad.IO.Unlift (UnliftIO (..), askUnliftIO,
26− withUnliftIO)
27−import Control.Monad.Trans.Resource
28−import qualified Data.Aeson as A
29−import qualified Data.ByteArray as BA
30−import qualified Data.ByteString as B
31−import Data.CaseInsensitive (mk)
32−import qualified Data.HashMap.Strict as H
33−import qualified Data.Ini as Ini
34−import Data.String (IsString (..))
35−import qualified Data.Text as T
36−import qualified Data.Text.Encoding as TE
37−import Data.Time (defaultTimeLocale, formatTime)
38−import GHC.Show (Show (show))
39−import qualified Network.Connection as Conn
40−import Network.HTTP.Client (defaultManagerSettings)
41−import qualified Network.HTTP.Client.TLS as TLS
42−import qualified Network.HTTP.Conduit as NC
43−import Network.HTTP.Types (ByteRange, Header, Method, Query,
44− hRange)
45−import qualified Network.HTTP.Types as HT
46−import Network.Minio.Errors
47−import System.Directory (doesFileExist, getHomeDirectory)
48−import qualified System.Environment as Env
49−import System.FilePath.Posix (combine)
50−import Text.XML
51−import qualified UnliftIO as U
52−
53−import Lib.Prelude
54−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
5574
5675-- | max obj size is 5TiB
5776maxObjectSize :: Int64
@@ -71,125 +90,157 @@ maxMultipartParts = 10000
7190-- TODO: Add a type which provides typed constants for region. this
7291-- type should have a IsString instance to infer the appropriate
7392-- constant.
93+
7494-- | awsRegionMap - library constant
7595awsRegionMap :: H.HashMap Text Text
76−awsRegionMap = H.fromList [
77− ("us-east-1", "s3.amazonaws.com")
78− , ("us-east-2", "s3-us-east-2.amazonaws.com")
79− , ("us-west-1", "s3-us-west-1.amazonaws.com")
80− , ("us-west-2", "s3-us-west-2.amazonaws.com")
81− , ("ca-central-1", "s3-ca-central-1.amazonaws.com")
82− , ("ap-south-1", "s3-ap-south-1.amazonaws.com")
83− , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
84− , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
85− , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
86− , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
87− , ("eu-west-1", "s3-eu-west-1.amazonaws.com")
88− , ("eu-west-2", "s3-eu-west-2.amazonaws.com")
89− , ("eu-central-1", "s3-eu-central-1.amazonaws.com")
90− , ("sa-east-1", "s3-sa-east-1.amazonaws.com")
91− ]
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+ ]
92129
93130-- | Connection Info data type. To create a 'ConnectInfo' value,
94131-- enable the @OverloadedStrings@ language extension and use the
95132-- `IsString` instance to provide a URL, for example:
96133--
97134-- > let c :: ConnectInfo = "https://play.min.io"
98−data ConnectInfo =
99− ConnectInfo { connectHost :: Text
100− , connectPort :: Int
101− , connectAccessKey :: Text
102− , connectSecretKey :: Text
103− , connectIsSecure :: Bool
104− , connectRegion :: Region
105− , connectAutoDiscoverRegion :: Bool
106− , connectDisableTLSCertValidation :: Bool
107− } deriving (Eq, Show)
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)
108147
109148instance IsString ConnectInfo where
110− fromString str =
111− let req = NC.parseRequest_ str
112− in ConnectInfo
113− { connectHost = TE.decodeUtf8 $ NC.host req
114− , connectPort = NC.port req
115− , connectAccessKey = ""
116− , connectSecretKey = ""
117− , connectIsSecure = NC.secure req
118− , connectRegion = ""
119− , connectAutoDiscoverRegion = True
120− , connectDisableTLSCertValidation = False
121− }
122−
123−-- | Contains access key and secret key to access object storage.
124−data Credentials = Credentials { cAccessKey :: Text
125− , cSecretKey :: Text
126− } deriving (Eq, Show)
127−
128−-- | A Provider is an action that may return Credentials. Providers
129−-- may be chained together using 'findFirst'.
130−type Provider = IO (Maybe Credentials)
131−
132−-- | Combines the given list of providers, by calling each one in
133−-- order until Credentials are found.
134−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)
135170findFirst [] = return Nothing
136−findFirst (f:fs) = do c <- f
137− maybe (findFirst fs) (return . Just) c
171+findFirst (f : fs) = do
172+ c <- f
173+ maybe (findFirst fs) (return . Just) c
138174
139−-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
140−fromAWSConfigFile :: Provider
175+-- | This action returns a 'CredentialValue' populated from
176+-- @~\/.aws\/credentials@
177+fromAWSConfigFile :: CredentialLoader
141178fromAWSConfigFile = do
142− credsE <- runExceptT $ do
143− homeDir <- lift $ getHomeDirectory
144− let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
145− fileExists <- lift $ doesFileExist awsCredsFile
146− bool (throwE "FileNotFound") (return ()) fileExists
147− ini <- ExceptT $ Ini.readIniFile awsCredsFile
148− akey <- ExceptT $ return
149− $ Ini.lookupValue "default" "aws_access_key_id" ini
150− skey <- ExceptT $ return
151− $ Ini.lookupValue "default" "aws_secret_access_key" ini
152− return $ Credentials akey skey
153− return $ hush credsE
154−
155−-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
156−-- @AWS_SECRET_ACCESS_KEY@ environment variables.
157−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
158199fromAWSEnv = runMaybeT $ do
159− akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
160− skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
161− 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
162203
163−-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
164−-- @MINIO_SECRET_KEY@ environment variables.
165−fromMinioEnv :: Provider
204+-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
205+-- and @MINIO_SECRET_KEY@ environment variables.
206+fromMinioEnv :: CredentialLoader
166207fromMinioEnv = runMaybeT $ do
167− akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
168− skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
169− return $ Credentials (T.pack akey) (T.pack skey)
170−
171−-- | setCredsFrom retrieves access credentials from the first
172−-- `Provider` form the given list that succeeds and sets it in the
173−-- `ConnectInfo`.
174−setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo
175−setCredsFrom ps ci = do pMay <- findFirst ps
176− maybe
177− (throwIO MErrVMissingCredentials)
178− (return . (flip setCreds ci))
179− pMay
180−
181−-- | setCreds sets the given `Credentials` in the `ConnectInfo`.
182−setCreds :: Credentials -> ConnectInfo -> ConnectInfo
183−setCreds (Credentials accessKey secretKey) connInfo =
184− connInfo { connectAccessKey = accessKey
185− , connectSecretKey = secretKey
186− }
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}
187236
188237-- | Set the S3 region parameter in the `ConnectInfo`
189238setRegion :: Region -> ConnectInfo -> ConnectInfo
190−setRegion r connInfo = connInfo { connectRegion = r
191− , connectAutoDiscoverRegion = False
192− }
239+setRegion r connInfo =
240+ connInfo
241+ { connectRegion = r,
242+ connectAutoDiscoverRegion = False
243+ }
193244
194245-- | Check if the connection to object storage server is secure
195246-- (i.e. uses TLS)
@@ -201,16 +252,10 @@ isConnectInfoSecure = connectIsSecure
201252-- certificates. Note that this option has no effect, if you provide
202253-- your own Manager in `mkMinioConn`.
203254disableTLSCertValidation :: ConnectInfo -> ConnectInfo
204−disableTLSCertValidation c = c { connectDisableTLSCertValidation = True }
255+disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
205256
206257getHostAddr :: ConnectInfo -> ByteString
207−getHostAddr ci = if | port == 80 || port == 443 -> toS host
208− | otherwise -> toS $
209− T.concat [ host, ":" , Lib.Prelude.show port]
210− where
211− port = connectPort ci
212− host = connectHost ci
213−
258+getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
214259
215260-- | Default Google Compute Storage ConnectInfo. Works only for
216261-- "Simple Migration" use-case with interoperability mode enabled on
@@ -219,23 +264,25 @@ getHostAddr ci = if | port == 80 || port == 443 -> toS host
219264--
220265-- Credentials should be supplied before use.
221266gcsCI :: ConnectInfo
222−gcsCI = setRegion "us"
223− "https://storage.googleapis.com"
224−
267+gcsCI =
268+ setRegion
269+ "us"
270+ "https://storage.googleapis.com"
225271
226272-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
227273-- should be supplied before use.
228274awsCI :: ConnectInfo
229275awsCI = "https://s3.amazonaws.com"
230276
231−
232277-- | <https://play.min.io MinIO Play Server>
233278-- ConnectInfo. Credentials are already filled in.
234279minioPlayCI :: ConnectInfo
235−minioPlayCI = let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
236− in setCreds playCreds
237− $ setRegion "us-east-1"
238− "https://play.min.io"
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"
239286
240287-- |
241288-- Represents a bucket in the object store
@@ -254,135 +301,147 @@ type ETag = Text
254301-- | Data type to represent an object encryption key. Create one using
255302-- the `mkSSECKey` function.
256303newtype SSECKey = SSECKey BA.ScrubbedBytes
257− deriving (Eq, Show)
304+ deriving stock (Eq, Show)
258305
259306-- | Validates that the given ByteString is 32 bytes long and creates
260307-- an encryption key.
261−mkSSECKey :: MonadThrow m => ByteString -> m SSECKey
262−mkSSECKey keyBytes | B.length keyBytes /= 32 =
263− throwM MErrVInvalidEncryptionKeyLength
264− | otherwise =
265− 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
266314
267315-- | Data type to represent Server-Side-Encryption settings
268316data SSE where
269− -- | Specifies SSE S3 encryption - server manages encryption keys
270− SSE :: SSE
271− -- | Specifies that KMS service should be used. The first argument
272− -- to the constructor is the Key Id to be used by the server (if
273− -- not specified, the default KMS key id is used). The second
274− -- argument is the optional KMS context that must have a
275− -- `A.ToJSON` instance - please refer to the AWS S3 documentation
276− -- for detailed information.
277− SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
278− -- | Specifies server-side encryption with customer provided
279− -- key. The argument is the encryption key to be used.
280− 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
281329
282330toPutObjectHeaders :: SSE -> [HT.Header]
283331toPutObjectHeaders sseArg =
284− let sseHeader = "x-amz-server-side-encryption"
285− sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
286− sseKmsContextHeader = sseHeader <> "-context"
287− ssecAlgo = sseHeader <> "-customer-algorithm"
288− ssecKey = sseHeader <> "-customer-key"
289− ssecKeyMD5 = ssecKey <> "-MD5"
290−
291− in case sseArg of
292− SSE -> [(sseHeader, "AES256")]
293−
294− SSEKMS keyIdMay ctxMay ->
295− [(sseHeader, "aws:kms")] ++
296− maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay ++
297− maybe [] (\k -> [(sseKmsContextHeader, toS $ A.encode k)]) ctxMay
298−
299− SSEC (SSECKey sb) ->
300− [(ssecAlgo, "AES256"),
301− (ssecKey, encodeToBase64 sb),
302− (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+ ]
303349
304350-- | Data type for options in PutObject call. Start with the empty
305351-- `defaultPutObjectOptions` and use various the various poo*
306352-- accessors.
307−data PutObjectOptions = PutObjectOptions {
308− -- | Set a standard MIME type describing the format of the object.
309− pooContentType :: Maybe Text
310− -- | Set what content encodings have been applied to the object and thus
311− -- what decoding mechanisms must be applied to obtain the media-type
312− -- referenced by the Content-Type header field.
313− , pooContentEncoding :: Maybe Text
314− -- | Set presentational information for the object.
315− , pooContentDisposition :: Maybe Text
316− -- | Set to specify caching behavior for the object along the
317− -- request/reply chain.
318− , pooCacheControl :: Maybe Text
319− -- | Set to describe the language(s) intended for the audience.
320− , pooContentLanguage :: Maybe Text
321− -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
322− -- performance needs, storage class is @STANDARD@ by default (i.e
323− -- when Nothing is passed).
324− , pooStorageClass :: Maybe Text
325− -- | Set user defined metadata to store with the object.
326− , pooUserMetadata :: [(Text, Text)]
327− -- | Set number of worker threads used to upload an object.
328− , pooNumThreads :: Maybe Word
329− -- | Set object encryption parameters for the request.
330− , 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
331377 }
332378
333379-- | Provide default `PutObjectOptions`.
334380defaultPutObjectOptions :: PutObjectOptions
335381defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
336382
337−-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
338−-- stripped and a Just is returned.
339−userMetadataHeaderNameMaybe :: Text -> Maybe Text
340−userMetadataHeaderNameMaybe k =
341− let prefix = T.toCaseFold "X-Amz-Meta-"
342− n = T.length prefix
343− in if T.toCaseFold (T.take n k) == prefix
344− then Just (T.drop n k)
345− else Nothing
346−
347−addXAmzMetaPrefix :: Text -> Text
348−addXAmzMetaPrefix s | isJust (userMetadataHeaderNameMaybe s) = s
349− | otherwise = "X-Amz-Meta-" <> s
350−
351−mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
352−mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix $ x, encodeUtf8 y))
353−
354383pooToHeaders :: PutObjectOptions -> [HT.Header]
355−pooToHeaders poo = userMetadata
356− ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
357− ++ maybe [] toPutObjectHeaders (pooSSE poo)
384+pooToHeaders poo =
385+ userMetadata
386+ ++ mapMaybe tupToMaybe (zip names values)
387+ ++ maybe [] toPutObjectHeaders (pooSSE poo)
358388 where
359− tupToMaybe (k, Just v) = Just (k, v)
389+ tupToMaybe (k, Just v) = Just (k, v)
360390 tupToMaybe (_, Nothing) = Nothing
361−
362391 userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
363−
364− names = ["content-type",
365− "content-encoding",
366− "content-disposition",
367− "content-language",
368− "cache-control",
369− "x-amz-storage-class"]
370− values = map (fmap encodeUtf8 . (poo &))
371− [pooContentType, pooContentEncoding,
372− pooContentDisposition, pooContentLanguage,
373− pooCacheControl, pooStorageClass]
374−
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+ ]
375410
376411-- |
377412-- BucketInfo returned for list buckets call
378−data BucketInfo = BucketInfo {
379− biName :: Bucket
380− , biCreationDate :: UTCTime
381− } deriving (Show, Eq)
413+data BucketInfo = BucketInfo
414+ { biName :: Bucket,
415+ biCreationDate :: UTCTime
416+ }
417+ deriving stock (Show, Eq)
382418
383419-- | A type alias to represent a part-number for multipart upload
384420type PartNumber = Int16
385421
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+
386445-- | A type alias to represent an upload-id for multipart upload
387446type UploadId = Text
388447
@@ -391,95 +450,116 @@ type PartTuple = (PartNumber, ETag)
391450
392451-- | Represents result from a listing of object parts of an ongoing
393452-- multipart upload.
394−data ListPartsResult = ListPartsResult {
395− lprHasMore :: Bool
396− , lprNextPart :: Maybe Int
397− , lprParts :: [ObjectPartInfo]
398− } deriving (Show, Eq)
453+data ListPartsResult = ListPartsResult
454+ { lprHasMore :: Bool,
455+ lprNextPart :: Maybe Int,
456+ lprParts :: [ObjectPartInfo]
457+ }
458+ deriving stock (Show, Eq)
399459
400460-- | Represents information about an object part in an ongoing
401461-- multipart upload.
402−data ObjectPartInfo = ObjectPartInfo {
403− opiNumber :: PartNumber
404− , opiETag :: ETag
405− , opiSize :: Int64
406− , opiModTime :: UTCTime
407− } 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)
408469
409470-- | Represents result from a listing of incomplete uploads to a
410471-- bucket.
411−data ListUploadsResult = ListUploadsResult {
412− lurHasMore :: Bool
413− , lurNextKey :: Maybe Text
414− , lurNextUpload :: Maybe Text
415− , lurUploads :: [(Object, UploadId, UTCTime)]
416− , lurCPrefixes :: [Text]
417− } 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)
418480
419481-- | Represents information about a multipart upload.
420−data UploadInfo = UploadInfo {
421− uiKey :: Object
422− , uiUploadId :: UploadId
423− , uiInitTime :: UTCTime
424− , uiSize :: Int64
425− } 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)
426489
427490-- | Represents result from a listing of objects in a bucket.
428−data ListObjectsResult = ListObjectsResult {
429− lorHasMore :: Bool
430− , lorNextToken :: Maybe Text
431− , lorObjects :: [ObjectInfo]
432− , lorCPrefixes :: [Text]
433− } 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)
434498
435499-- | Represents result from a listing of objects version 1 in a bucket.
436−data ListObjectsV1Result = ListObjectsV1Result {
437− lorHasMore' :: Bool
438− , lorNextMarker :: Maybe Text
439− , lorObjects' :: [ObjectInfo]
440− , lorCPrefixes' :: [Text]
441− } 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)
442507
443508-- | Represents information about an object.
444509data ObjectInfo = ObjectInfo
445− { oiObject :: Object -- ^ Object key
446− , oiModTime :: UTCTime -- ^ Modification time of the object
447− , oiETag :: ETag -- ^ ETag of the object
448− , oiSize :: Int64 -- ^ Size of the object in bytes
449− , oiUserMetadata :: H.HashMap Text Text -- ^ A map of user-metadata
450− -- pairs stored with an
451− -- object (keys will not
452− -- have the @X-Amz-Meta-@
453− -- prefix).
454− , oiMetadata :: H.HashMap Text Text -- ^ A map of metadata
455− -- key-value pairs (not
456− -- including the
457− -- user-metadata pairs)
458− } 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)
459531
460532-- | Represents source object in server-side copy object
461533data SourceInfo = SourceInfo
462− { srcBucket :: Text -- ^ Bucket containing the source object
463− , srcObject :: Text -- ^ Source object key
464− , srcRange :: Maybe (Int64, Int64) -- ^ Source object
465− -- byte-range
466− -- (inclusive)
467− , srcIfMatch :: Maybe Text -- ^ ETag condition on source -
468− -- object is copied only if the
469− -- source object's ETag matches
470− -- this value.
471− , srcIfNoneMatch :: Maybe Text -- ^ ETag not match condition
472− -- on source - object is copied
473− -- if ETag does not match this
474− -- value.
475− , srcIfModifiedSince :: Maybe UTCTime -- ^ Copy source object only
476− -- if the source has been
477− -- modified since this time.
478− , srcIfUnmodifiedSince :: Maybe UTCTime -- ^ Copy source object only
479− -- if the source has been
480− -- un-modified since this
481− -- given time.
482− } 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)
483563
484564-- | Provide a default for `SourceInfo`
485565defaultSourceInfo :: SourceInfo
@@ -487,9 +567,12 @@ defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing
487567
488568-- | Represents destination object in server-side copy object
489569data DestinationInfo = DestinationInfo
490− { dstBucket :: Text -- ^ Destination bucket
491− , dstObject :: Text -- ^ Destination object key
492− } deriving (Show, Eq)
570+ { -- | Destination bucket
571+ dstBucket :: Text,
572+ -- | Destination object key
573+ dstObject :: Text
574+ }
575+ deriving stock (Show, Eq)
493576
494577-- | Provide a default for `DestinationInfo`
495578defaultDestinationInfo :: DestinationInfo
@@ -498,96 +581,106 @@ defaultDestinationInfo = DestinationInfo "" ""
498581-- | Data type for options when getting an object from the
499582-- service. Start with the empty `defaultGetObjectOptions` and modify
500583-- it using the goo* functions.
501−data GetObjectOptions = GetObjectOptions {
502− -- | 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,
503586 -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
504− gooRange :: Maybe ByteRange
587+ gooRange :: Maybe ByteRange,
505588 -- | Set matching ETag condition, GetObject which matches the following
506589 -- ETag.
507− , gooIfMatch :: Maybe ETag
590+ gooIfMatch :: Maybe ETag,
508591 -- | Set matching ETag none condition, GetObject which does not match
509592 -- the following ETag.
510− , gooIfNoneMatch :: Maybe ETag
593+ gooIfNoneMatch :: Maybe ETag,
511594 -- | Set object unmodified condition, GetObject unmodified since given time.
512− , gooIfUnmodifiedSince :: Maybe UTCTime
595+ gooIfUnmodifiedSince :: Maybe UTCTime,
513596 -- | Set object modified condition, GetObject modified since given time.
514− , gooIfModifiedSince :: Maybe UTCTime
597+ gooIfModifiedSince :: Maybe UTCTime,
515598 -- | Specify SSE-C key
516− , gooSSECKey :: Maybe SSECKey
599+ gooSSECKey :: Maybe SSECKey
517600 }
518601
519602-- | Provide default `GetObjectOptions`.
520603defaultGetObjectOptions :: GetObjectOptions
521604defaultGetObjectOptions =
522− GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
605+ GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
523606
524607gooToHeaders :: GetObjectOptions -> [HT.Header]
525−gooToHeaders goo = rangeHdr ++ zip names values
526− ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
608+gooToHeaders goo =
609+ rangeHdr
610+ ++ zip names values
611+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
527612 where
528− names = ["If-Match",
529− "If-None-Match",
530− "If-Unmodified-Since",
531− "If-Modified-Since"]
532− values = mapMaybe (fmap encodeUtf8 . (goo &))
533− [gooIfMatch, gooIfNoneMatch,
534− fmap formatRFC1123 . gooIfUnmodifiedSince,
535− fmap formatRFC1123 . gooIfModifiedSince]
536− rangeHdr = maybe [] (\a -> [(hRange, HT.renderByteRanges [a])])
537− $ gooRange goo
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
538630
539631-- | Data type returned by 'getObject' representing the object being
540632-- retrieved. Use the @gor*@ functions to access its contents.
541−data GetObjectResponse = GetObjectResponse {
542− -- | ObjectInfo of the object being retrieved.
543− gorObjectInfo :: ObjectInfo
544− -- | A conduit of the bytes of the object.
545− , gorObjectStream :: C.ConduitM () ByteString Minio ()
546− }
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+ }
547639
548640-- | A data-type for events that can occur in the object storage
549641-- server. Reference:
550642-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
551−data Event = ObjectCreated
552− | ObjectCreatedPut
553− | ObjectCreatedPost
554− | ObjectCreatedCopy
555− | ObjectCreatedMultipartUpload
556− | ObjectRemoved
557− | ObjectRemovedDelete
558− | ObjectRemovedDeleteMarkerCreated
559− | ReducedRedundancyLostObject
560− deriving (Eq)
561−
562−instance Show Event where
563− show ObjectCreated = "s3:ObjectCreated:*"
564− show ObjectCreatedPut = "s3:ObjectCreated:Put"
565− show ObjectCreatedPost = "s3:ObjectCreated:Post"
566− show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
567− show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
568− show ObjectRemoved = "s3:ObjectRemoved:*"
569− show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
570− show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
571− 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"
572665
573666textToEvent :: Text -> Maybe Event
574667textToEvent t = case t of
575− "s3:ObjectCreated:*" -> Just ObjectCreated
576− "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
577− "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
578− "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
579− "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
580− "s3:ObjectRemoved:*" -> Just ObjectRemoved
581− "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
582675 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
583− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
584− _ -> Nothing
585−
676+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
677+ _ -> Nothing
586678
587679-- | Filter data type - part of notification configuration
588−data Filter = Filter
680+newtype Filter = Filter
589681 { fFilter :: FilterKey
590− } deriving (Show, Eq)
682+ }
683+ deriving stock (Show, Eq)
591684
592685-- | defaultFilter is empty, used to create a notification
593686-- configuration.
@@ -595,9 +688,10 @@ defaultFilter :: Filter
595688defaultFilter = Filter defaultFilterKey
596689
597690-- | FilterKey contains FilterRules, and is part of a Filter.
598−data FilterKey = FilterKey
691+newtype FilterKey = FilterKey
599692 { fkKey :: FilterRules
600− } deriving (Show, Eq)
693+ }
694+ deriving stock (Show, Eq)
601695
602696-- | defaultFilterKey is empty, used to create notification
603697-- configuration.
@@ -605,16 +699,16 @@ defaultFilterKey :: FilterKey
605699defaultFilterKey = FilterKey defaultFilterRules
606700
607701-- | FilterRules represents a collection of `FilterRule`s.
608−data FilterRules = FilterRules
702+newtype FilterRules = FilterRules
609703 { frFilterRules :: [FilterRule]
610− } deriving (Show, Eq)
704+ }
705+ deriving stock (Show, Eq)
611706
612707-- | defaultFilterRules is empty, used to create notification
613708-- configuration.
614709defaultFilterRules :: FilterRules
615710defaultFilterRules = FilterRules []
616711
617−
618712-- | A filter rule that can act based on the suffix or prefix of an
619713-- object. As an example, let's create two filter rules:
620714--
@@ -625,9 +719,10 @@ defaultFilterRules = FilterRules []
625719-- for objects having a suffix of ".jpg", and the @prefixRule@
626720-- restricts it to objects having a prefix of "images/".
627721data FilterRule = FilterRule
628− { frName :: Text
629− , frValue :: Text
630− } deriving (Show, Eq)
722+ { frName :: Text,
723+ frValue :: Text
724+ }
725+ deriving stock (Show, Eq)
631726
632727-- | Arn is an alias of Text
633728type Arn = Text
@@ -636,11 +731,12 @@ type Arn = Text
636731-- notification system. It could represent a Queue, Topic or Lambda
637732-- Function configuration.
638733data NotificationConfig = NotificationConfig
639− { ncId :: Text
640− , ncArn :: Arn
641− , ncEvents :: [Event]
642− , ncFilter :: Filter
643− } deriving (Show, Eq)
734+ { ncId :: Text,
735+ ncArn :: Arn,
736+ ncEvents :: [Event],
737+ ncFilter :: Filter
738+ }
739+ deriving stock (Show, Eq)
644740
645741-- | A data-type to represent bucket notification configuration. It is
646742-- a collection of queue, topic or lambda function configurations. The
@@ -648,16 +744,16 @@ data NotificationConfig = NotificationConfig
648744-- described at
649745-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
650746data Notification = Notification
651− { nQueueConfigurations :: [NotificationConfig]
652− , nTopicConfigurations :: [NotificationConfig]
653− , nCloudFunctionConfigurations :: [NotificationConfig]
654− } deriving (Eq, Show)
747+ { nQueueConfigurations :: [NotificationConfig],
748+ nTopicConfigurations :: [NotificationConfig],
749+ nCloudFunctionConfigurations :: [NotificationConfig]
750+ }
751+ deriving stock (Show, Eq)
655752
656753-- | The default notification configuration is empty.
657754defaultNotification :: Notification
658755defaultNotification = Notification [] [] []
659756
660−
661757--------------------------------------------------------------------------
662758-- Select API Related Types
663759--------------------------------------------------------------------------
@@ -665,38 +761,42 @@ defaultNotification = Notification [] [] []
665761-- | SelectRequest represents the Select API call. Use the
666762-- `selectRequest` function to create a value of this type.
667763data SelectRequest = SelectRequest
668− { srExpression :: Text
669− , srExpressionType :: ExpressionType
670− , srInputSerialization :: InputSerialization
671− , srOutputSerialization :: OutputSerialization
672− , srRequestProgressEnabled :: Maybe Bool
673− } 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)
674771
675772data ExpressionType = SQL
676− deriving (Eq, Show)
773+ deriving stock (Show, Eq)
677774
678775-- | InputSerialization represents format information of the input
679776-- object being queried. Use one of the smart constructors such as
680777-- `defaultCsvInput` as a starting value, and add compression info
681778-- using `setInputCompressionType`
682779data InputSerialization = InputSerialization
683− { isCompressionType :: Maybe CompressionType
684− , isFormatInfo :: InputFormatInfo
685− } deriving (Eq, Show)
780+ { isCompressionType :: Maybe CompressionType,
781+ isFormatInfo :: InputFormatInfo
782+ }
783+ deriving stock (Show, Eq)
686784
687785-- | Data type representing the compression setting in a Select
688786-- request.
689−data CompressionType = CompressionTypeNone
690− | CompressionTypeGzip
691− | CompressionTypeBzip2
692− deriving (Eq, Show)
787+data CompressionType
788+ = CompressionTypeNone
789+ | CompressionTypeGzip
790+ | CompressionTypeBzip2
791+ deriving stock (Show, Eq)
693792
694793-- | Data type representing input object format information in a
695794-- Select request.
696−data InputFormatInfo = InputFormatCSV CSVInputProp
697− | InputFormatJSON JSONInputProp
698− | InputFormatParquet
699− deriving (Eq, Show)
795+data InputFormatInfo
796+ = InputFormatCSV CSVInputProp
797+ | InputFormatJSON JSONInputProp
798+ | InputFormatParquet
799+ deriving stock (Show, Eq)
700800
701801-- | defaultCsvInput returns InputSerialization with default CSV
702802-- format, and without any compression setting.
@@ -706,14 +806,18 @@ defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
706806-- | linesJsonInput returns InputSerialization with JSON line based
707807-- format with no compression setting.
708808linesJsonInput :: InputSerialization
709−linesJsonInput = InputSerialization Nothing
710− (InputFormatJSON $ JSONInputProp JSONTypeLines)
809+linesJsonInput =
810+ InputSerialization
811+ Nothing
812+ (InputFormatJSON $ JSONInputProp JSONTypeLines)
711813
712814-- | documentJsonInput returns InputSerialization with JSON document
713815-- based format with no compression setting.
714816documentJsonInput :: InputSerialization
715−documentJsonInput = InputSerialization Nothing
716− (InputFormatJSON $ JSONInputProp JSONTypeDocument)
817+documentJsonInput =
818+ InputSerialization
819+ Nothing
820+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)
717821
718822-- | defaultParquetInput returns InputSerialization with Parquet
719823-- format, and no compression setting.
@@ -722,12 +826,14 @@ defaultParquetInput = InputSerialization Nothing InputFormatParquet
722826
723827-- | setInputCompressionType sets the compression type for the input
724828-- of the SelectRequest
725−setInputCompressionType :: CompressionType -> SelectRequest
726− -> SelectRequest
829+setInputCompressionType ::
830+ CompressionType ->
831+ SelectRequest ->
832+ SelectRequest
727833setInputCompressionType c i =
728− let is = srInputSerialization i
729− is' = is { isCompressionType = Just c }
730− in i { srInputSerialization = is' }
834+ let is = srInputSerialization i
835+ is' = is {isCompressionType = Just c}
836+ in i {srInputSerialization = is'}
731837
732838-- | defaultCsvOutput returns OutputSerialization with default CSV
733839-- format.
@@ -744,40 +850,42 @@ defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
744850-- SelectRequest with the SQL query text given by @query@, the input
745851-- serialization settings (compression format and format information)
746852-- @inputSer@ and the output serialization settings @outputSer@.
747−selectRequest :: Text -> InputSerialization -> OutputSerialization
748− -> SelectRequest
853+selectRequest ::
854+ Text ->
855+ InputSerialization ->
856+ OutputSerialization ->
857+ SelectRequest
749858selectRequest sqlQuery inputSer outputSer =
750− SelectRequest { srExpression = sqlQuery
751− , srExpressionType = SQL
752− , srInputSerialization = inputSer
753− , srOutputSerialization = outputSer
754− , srRequestProgressEnabled = Nothing
755− }
859+ SelectRequest
860+ { srExpression = sqlQuery,
861+ srExpressionType = SQL,
862+ srInputSerialization = inputSer,
863+ srOutputSerialization = outputSer,
864+ srRequestProgressEnabled = Nothing
865+ }
756866
757867-- | setRequestProgressEnabled sets the flag for turning on progress
758868-- messages when the Select response is being streamed back to the
759869-- client.
760870setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
761871setRequestProgressEnabled enabled sr =
762− sr { srRequestProgressEnabled = Just enabled }
872+ sr {srRequestProgressEnabled = Just enabled}
763873
764874type CSVInputProp = CSVProp
765875
766876-- | CSVProp represents CSV format properties. It is built up using
767877-- the Monoid instance.
768−data CSVProp = CSVProp (H.HashMap Text Text)
769− deriving (Eq, Show)
878+newtype CSVProp = CSVProp (H.HashMap Text Text)
879+ deriving stock (Show, Eq)
770880
771−#if (__GLASGOW_HASKELL__ >= 804)
772881instance Semigroup CSVProp where
773− (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
774−#endif
882+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
775883
776884instance Monoid CSVProp where
777− mempty = CSVProp mempty
778−#if (__GLASGOW_HASKELL__ < 804)
779− mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
780−#endif
885+ mempty = CSVProp mempty
886+
887+csvPropsList :: CSVProp -> [(Text, Text)]
888+csvPropsList (CSVProp h) = sort $ H.toList h
781889
782890defaultCSVProp :: CSVProp
783891defaultCSVProp = mempty
@@ -801,18 +909,21 @@ quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
801909-- | FileHeaderInfo specifies information about column headers for CSV
802910-- format.
803911data FileHeaderInfo
804− = FileHeaderNone -- ^ No column headers are present
805− | FileHeaderUse -- ^ Headers are present and they should be used
806− | FileHeaderIgnore -- ^ Header are present, but should be ignored
807− 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)
808919
809920-- | Specify the CSV file header info property.
810921fileHeaderInfo :: FileHeaderInfo -> CSVProp
811−fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString
922+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toStr
812923 where
813− toString FileHeaderNone = "NONE"
814− toString FileHeaderUse = "USE"
815− toString FileHeaderIgnore = "IGNORE"
924+ toStr FileHeaderNone = "NONE"
925+ toStr FileHeaderUse = "USE"
926+ toStr FileHeaderIgnore = "IGNORE"
816927
817928-- | Specify the CSV comment character property. Lines starting with
818929-- this character are ignored by the server.
@@ -825,64 +936,69 @@ allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter"
825936
826937-- | Set the CSV format properties in the InputSerialization.
827938setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
828−setInputCSVProps p is = is { isFormatInfo = InputFormatCSV p }
939+setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
829940
830941-- | Set the CSV format properties in the OutputSerialization.
831942outputCSVFromProps :: CSVProp -> OutputSerialization
832−outputCSVFromProps p = OutputSerializationCSV p
943+outputCSVFromProps = OutputSerializationCSV
833944
834−data JSONInputProp = JSONInputProp { jsonipType :: JSONType }
835− deriving (Eq, Show)
945+newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
946+ deriving stock (Show, Eq)
836947
837948data JSONType = JSONTypeDocument | JSONTypeLines
838− deriving (Eq, Show)
839−
949+ deriving stock (Show, Eq)
840950
841951-- | OutputSerialization represents output serialization settings for
842952-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
843953-- a starting point.
844−data OutputSerialization = OutputSerializationJSON JSONOutputProp
845− | OutputSerializationCSV CSVOutputProp
846− deriving (Eq, Show)
954+data OutputSerialization
955+ = OutputSerializationJSON JSONOutputProp
956+ | OutputSerializationCSV CSVOutputProp
957+ deriving stock (Show, Eq)
847958
848959type CSVOutputProp = CSVProp
849960
850961-- | quoteFields is an output serialization parameter
851962quoteFields :: QuoteFields -> CSVProp
852−quoteFields q = CSVProp $ H.singleton "QuoteFields" $
853− case q of
854− QuoteFieldsAsNeeded -> "ASNEEDED"
855− QuoteFieldsAlways -> "ALWAYS"
963+quoteFields q = CSVProp $
964+ H.singleton "QuoteFields" $
965+ case q of
966+ QuoteFieldsAsNeeded -> "ASNEEDED"
967+ QuoteFieldsAlways -> "ALWAYS"
856968
857969-- | Represent the QuoteField setting.
858970data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
859− deriving (Eq, Show)
971+ deriving stock (Show, Eq)
860972
861−data JSONOutputProp = JSONOutputProp { jsonopRecordDelimiter :: Maybe Text }
862− deriving (Eq, Show)
973+newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
974+ deriving stock (Show, Eq)
863975
864976-- | Set the output record delimiter for JSON format
865977outputJSONFromRecordDelimiter :: Text -> OutputSerialization
866978outputJSONFromRecordDelimiter t =
867− OutputSerializationJSON (JSONOutputProp $ Just t)
979+ OutputSerializationJSON (JSONOutputProp $ Just t)
868980
869981-- Response related types
870982
871983-- | An EventMessage represents each kind of message received from the server.
872−data EventMessage = ProgressEventMessage { emProgress :: Progress }
873− | StatsEventMessage { emStats :: Stats }
874− | RequestLevelErrorMessage { emErrorCode :: Text
875− , emErrorMessage :: Text
876− }
877− | RecordPayloadEventMessage { emPayloadBytes :: ByteString }
878− deriving (Eq, Show)
879−
880−data MsgHeaderName = MessageType
881− | EventType
882− | ContentType
883− | ErrorCode
884− | ErrorMessage
885− 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)
8861002
8871003msgHeaderValueType :: Word8
8881004msgHeaderValueType = 7
@@ -890,11 +1006,12 @@ msgHeaderValueType = 7
8901006type MessageHeader = (MsgHeaderName, Text)
8911007
8921008-- | Represent the progress event returned in the Select response.
893−data Progress = Progress { pBytesScanned :: Int64
894− , pBytesProcessed :: Int64
895− , pBytesReturned :: Int64
896− }
897− deriving (Eq, Show)
1009+data Progress = Progress
1010+ { pBytesScanned :: Int64,
1011+ pBytesProcessed :: Int64,
1012+ pBytesReturned :: Int64
1013+ }
1014+ deriving stock (Show, Eq)
8981015
8991016-- | Represent the stats event returned at the end of the Select
9001017-- response.
@@ -907,86 +1024,89 @@ type Stats = Progress
9071024-- | Represents different kinds of payload that are used with S3 API
9081025-- requests.
9091026data Payload
910− = PayloadBS ByteString
911− | PayloadH Handle Int64 Int64 -- file handle, offset and length
912− | 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
9131030
9141031defaultPayload :: Payload
9151032defaultPayload = PayloadBS ""
9161033
917−data AdminReqInfo = AdminReqInfo {
918− ariMethod :: Method
919− , ariPayloadHash :: Maybe ByteString
920− , ariPayload :: Payload
921− , ariPath :: ByteString
922− , ariHeaders :: [Header]
923− , 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
9241041 }
9251042
9261043data S3ReqInfo = S3ReqInfo
927− { riMethod :: Method
928− , riBucket :: Maybe Bucket
929− , riObject :: Maybe Object
930− , riQueryParams :: Query
931− , riHeaders :: [Header]
932− , riPayload :: Payload
933− , riPayloadHash :: Maybe ByteString
934− , riRegion :: Maybe Region
935− , 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
9361054 }
9371055
9381056defaultS3ReqInfo :: S3ReqInfo
939−defaultS3ReqInfo = S3ReqInfo HT.methodGet Nothing Nothing
940− [] [] 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
9411069
9421070getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
9431071getS3Path b o =
944− let segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
945− in
946− B.concat ["/", B.intercalate "/" segments]
947−
948−-- | Time to expire for a presigned URL. It interpreted as a number of
949−-- seconds. The maximum duration that can be specified is 7 days.
950−type UrlExpiry = Int
1072+ let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
1073+ in B.concat ["/", B.intercalate "/" segments]
9511074
9521075type RegionMap = H.HashMap Bucket Region
9531076
9541077-- | The Minio Monad - all computations accessing object storage
9551078-- happens in it.
956−newtype Minio a = Minio {
957− unMinio :: ReaderT MinioConn (ResourceT IO) a
1079+newtype Minio a = Minio
1080+ { unMinio :: ReaderT MinioConn (ResourceT IO) a
9581081 }
959− deriving (
960− Functor
961− , Applicative
962− , Monad
963− , MonadIO
964− , MonadReader MinioConn
965− , MonadResource
1082+ deriving newtype
1083+ ( Functor,
1084+ Applicative,
1085+ Monad,
1086+ MonadIO,
1087+ MonadReader MinioConn,
1088+ MonadResource,
1089+ MonadUnliftIO
9661090 )
9671091
968−instance MonadUnliftIO Minio where
969− askUnliftIO = Minio $ ReaderT $ \r ->
970− withUnliftIO $ \u ->
971− return (UnliftIO (unliftIO u . flip runReaderT r . unMinio))
972−
9731092-- | MinioConn holds connection info and a connection pool to allow
9741093-- for efficient resource re-use.
9751094data MinioConn = MinioConn
976− { mcConnInfo :: ConnectInfo
977− , mcConnManager :: NC.Manager
978− , mcRegionMap :: MVar RegionMap
1095+ { mcConnInfo :: ConnectInfo,
1096+ mcConnManager :: NC.Manager,
1097+ mcRegionMap :: MVar RegionMap
9791098 }
9801099
9811100class HasSvcNamespace env where
9821101 getSvcNamespace :: env -> Text
9831102
9841103instance HasSvcNamespace MinioConn where
985− getSvcNamespace env = let host = connectHost $ mcConnInfo env
986− in if | host == "storage.googleapis.com" ->
987− "http://doc.s3.amazonaws.com/2006-03-01"
988− | otherwise ->
989− "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+ )
9901110
9911111-- | Takes connection information and returns a connection object to
9921112-- be passed to 'runMinio'. The returned value can be kept in the
@@ -994,11 +1114,16 @@ instance HasSvcNamespace MinioConn where
9941114-- object storage is accessed.
9951115connect :: ConnectInfo -> IO MinioConn
9961116connect ci = do
997− let settings | connectIsSecure ci && connectDisableTLSCertValidation ci =
998− let badTlsSettings = Conn.TLSSettingsSimple True False False
999− in TLS.mkManagerSettings badTlsSettings Nothing
1000− | connectIsSecure ci = NC.tlsManagerSettings
1001− | 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
10021127 mgr <- NC.newManager settings
10031128 mkMinioConn ci mgr
10041129
@@ -1012,8 +1137,8 @@ runMinioWith conn m = runResourceT $ runMinioResWith conn m
10121137-- `MinioConn`.
10131138mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
10141139mkMinioConn ci mgr = do
1015− rMapMVar <- M.newMVar H.empty
1016− return $ MinioConn ci mgr rMapMVar
1140+ rMapMVar <- M.newMVar H.empty
1141+ return $ MinioConn ci mgr rMapMVar
10171142
10181143-- | Run the Minio action and return the result or an error.
10191144runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
@@ -1026,12 +1151,12 @@ runMinio ci m = do
10261151runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)
10271152runMinioResWith conn m =
10281153 flip runReaderT conn . unMinio $
1029− fmap Right m `U.catches`
1030− [ U.Handler handlerServiceErr
1031− , U.Handler handlerHE
1032− , U.Handler handlerFE
1033− , U.Handler handlerValidation
1034− ]
1154+ fmap Right m
1155+ `U.catches` [ U.Handler handlerServiceErr,
1156+ U.Handler handlerHE,
1157+ U.Handler handlerFE,
1158+ U.Handler handlerValidation
1159+ ]
10351160 where
10361161 handlerServiceErr = return . Left . MErrService
10371162 handlerHE = return . Left . MErrHTTP
@@ -1045,9 +1170,22 @@ runMinioRes ci m = do
10451170 conn <- liftIO $ connect ci
10461171 runMinioResWith conn m
10471172
1048−s3Name :: Text -> Text -> Name
1049−s3Name ns s = Name s (Just ns) Nothing
1050−
10511173-- | Format as per RFC 1123.
10521174formatRFC1123 :: UTCTime -> T.Text
10531175formatRFC1123 = 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.HashMap.Strict as H
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, H.HashMap 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 = H.map toS $ H.fromList $ catMaybes $
281− mkPair <$> conditions ppWithCreds
282− formData = formFromPolicy `H.union` signData
283−
284− -- compute POST upload URL
285− bucket = H.lookupDefault "" "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+481−319
@@ -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,150 +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 Data.ByteString as BS
95−import qualified Data.Text as T
96−import qualified Network.HTTP.Conduit as NC
97−import qualified Network.HTTP.Types as HT
98−import Network.HTTP.Types.Status (status404)
99−import UnliftIO (Handler (Handler))
100−
101−import Lib.Prelude
102−
103−import Network.Minio.API
104−import Network.Minio.APICommon
105−import Network.Minio.Data
106−import Network.Minio.Errors
107−import Network.Minio.PresignedOperations
108−import Network.Minio.Utils
109−import Network.Minio.XmlGenerator
110−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))
111125
112126-- | Fetch all buckets from the service.
113127getService :: Minio [BucketInfo]
114128getService = do
115− resp <- executeRequest $ defaultS3ReqInfo {
116− riNeedsLocation = False
117− }
129+ resp <-
130+ executeRequest $
131+ defaultS3ReqInfo
132+ { riNeedsLocation = False
133+ }
118134 parseListBuckets $ NC.responseBody resp
119135
120136-- Parse headers from getObject and headObject calls.
121137parseGetObjectHeaders :: Object -> [HT.Header] -> Maybe ObjectInfo
122138parseGetObjectHeaders object headers =
123− let metadataPairs = getMetadata headers
124− userMetadata = getUserMetadataMap metadataPairs
125− metadata = getNonUserMetadataMap metadataPairs
126− in ObjectInfo <$> Just object
127− <*> getLastModifiedHeader headers
128− <*> getETagHeader headers
129− <*> getContentLength headers
130− <*> Just userMetadata
131− <*> Just metadata
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
132149
133150-- | GET an object from the service and return parsed ObjectInfo and a
134151-- conduit source for the object content
135−getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
136− -> Minio GetObjectResponse
152+getObject' ::
153+ Bucket ->
154+ Object ->
155+ HT.Query ->
156+ [HT.Header] ->
157+ Minio GetObjectResponse
137158getObject' bucket object queryParams headers = do
138− resp <- mkStreamRequest reqInfo
139− let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
140− objInfo <- maybe (throwIO MErrVInvalidObjectInfoResponse) return
141− objInfoMaybe
142− return $ GetObjectResponse { gorObjectInfo = objInfo
143− , gorObjectStream = NC.responseBody resp
144− }
159+ resp <- mkStreamRequest reqInfo
160+ let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
161+ objInfo <-
162+ maybe
163+ (throwIO MErrVInvalidObjectInfoResponse)
164+ return
165+ objInfoMaybe
166+ return $
167+ GetObjectResponse
168+ { gorObjectInfo = objInfo,
169+ gorObjectStream = NC.responseBody resp
170+ }
145171 where
146− reqInfo = defaultS3ReqInfo { riBucket = Just bucket
147− , riObject = Just object
148− , riQueryParams = queryParams
149− , riHeaders = headers
150− }
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+ }
151184
152185-- | Creates a bucket via a PUT bucket call.
153186putBucket :: Bucket -> Region -> Minio ()
154187putBucket bucket location = do
155188 ns <- asks getSvcNamespace
156− void $ executeRequest $
157− defaultS3ReqInfo { riMethod = HT.methodPut
158− , riBucket = Just bucket
159− , riPayload = PayloadBS $ mkCreateBucketConfig ns location
160− , 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
161196 }
162197
163198-- | Single PUT object size.
@@ -173,314 +208,432 @@ putObjectSingle' bucket object headers bs = do
173208 let size = fromIntegral (BS.length bs)
174209 -- check length is within single PUT object size.
175210 when (size > maxSinglePutObjectSizeBytes) $
176− throwIO $ MErrVSinglePUTSizeExceeded size
211+ throwIO $
212+ MErrVSinglePUTSizeExceeded size
177213
178214 let payload = mkStreamingPayload $ PayloadBS bs
179− resp <- executeRequest $
180− defaultS3ReqInfo { riMethod = HT.methodPut
181− , riBucket = Just bucket
182− , riObject = Just object
183− , riHeaders = headers
184− , riPayload = payload
185− }
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+ }
186224
187225 let rheaders = NC.responseHeaders resp
188226 etag = getETagHeader rheaders
189227 maybe
190228 (throwIO MErrVETagHeaderNotFound)
191− return etag
229+ return
230+ etag
192231
193232-- | PUT an object into the service. This function performs a single
194233-- PUT object call, and so can only transfer objects upto 5GiB.
195−putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
196− -> Int64 -> Minio ETag
234+putObjectSingle ::
235+ Bucket ->
236+ Object ->
237+ [HT.Header] ->
238+ Handle ->
239+ Int64 ->
240+ Int64 ->
241+ Minio ETag
197242putObjectSingle bucket object headers h offset size = do
198243 -- check length is within single PUT object size.
199244 when (size > maxSinglePutObjectSizeBytes) $
200− throwIO $ MErrVSinglePUTSizeExceeded size
245+ throwIO $
246+ MErrVSinglePUTSizeExceeded size
201247
202248 -- content-length header is automatically set by library.
203249 let payload = mkStreamingPayload $ PayloadH h offset size
204− resp <- executeRequest $
205− defaultS3ReqInfo { riMethod = HT.methodPut
206− , riBucket = Just bucket
207− , riObject = Just object
208− , riHeaders = headers
209− , riPayload = payload
210− }
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+ }
211259
212260 let rheaders = NC.responseHeaders resp
213261 etag = getETagHeader rheaders
214262 maybe
215263 (throwIO MErrVETagHeaderNotFound)
216− return etag
264+ return
265+ etag
217266
218267-- | List objects in a bucket matching prefix up to delimiter,
219268-- starting from nextMarker.
220−listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
221− -> Minio ListObjectsV1Result
269+listObjectsV1' ::
270+ Bucket ->
271+ Maybe Text ->
272+ Maybe Text ->
273+ Maybe Text ->
274+ Maybe Int ->
275+ Minio ListObjectsV1Result
222276listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
223− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
224− , riBucket = Just bucket
225− , riQueryParams = mkOptionalParams params
226− }
277+ resp <-
278+ executeRequest $
279+ defaultS3ReqInfo
280+ { riMethod = HT.methodGet,
281+ riBucket = Just bucket,
282+ riQueryParams = mkOptionalParams params
283+ }
227284 parseListObjectsV1Response $ NC.responseBody resp
228285 where
229− params = [
230− ("marker", nextMarker)
231− , ("prefix", prefix)
232− , ("delimiter", delimiter)
233− , ("max-keys", show <$> maxKeys)
286+ params =
287+ [ ("marker", nextMarker),
288+ ("prefix", prefix),
289+ ("delimiter", delimiter),
290+ ("max-keys", show <$> maxKeys)
234291 ]
235292
236293-- | List objects in a bucket matching prefix up to delimiter,
237294-- starting from nextToken.
238−listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
239− -> Minio ListObjectsResult
295+listObjects' ::
296+ Bucket ->
297+ Maybe Text ->
298+ Maybe Text ->
299+ Maybe Text ->
300+ Maybe Int ->
301+ Minio ListObjectsResult
240302listObjects' bucket prefix nextToken delimiter maxKeys = do
241− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
242− , riBucket = Just bucket
243− , riQueryParams = mkOptionalParams params
244− }
303+ resp <-
304+ executeRequest $
305+ defaultS3ReqInfo
306+ { riMethod = HT.methodGet,
307+ riBucket = Just bucket,
308+ riQueryParams = mkOptionalParams params
309+ }
245310 parseListObjectsResponse $ NC.responseBody resp
246311 where
247− params = [
248− ("list-type", Just "2")
249− , ("continuation_token", nextToken)
250− , ("prefix", prefix)
251− , ("delimiter", delimiter)
252− , ("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)
253318 ]
254319
255320-- | DELETE a bucket from the service.
256321deleteBucket :: Bucket -> Minio ()
257−deleteBucket bucket = void $
258− executeRequest $
259− defaultS3ReqInfo { riMethod = HT.methodDelete
260− , riBucket = Just bucket
322+deleteBucket bucket =
323+ void $
324+ executeRequest $
325+ defaultS3ReqInfo
326+ { riMethod = HT.methodDelete,
327+ riBucket = Just bucket
261328 }
262329
263330-- | DELETE an object from the service.
264331deleteObject :: Bucket -> Object -> Minio ()
265−deleteObject bucket object = void $
266− executeRequest $
267− defaultS3ReqInfo { riMethod = HT.methodDelete
268− , riBucket = Just bucket
269− , 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
270339 }
271340
272341-- | Create a new multipart upload.
273342newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
274343newMultipartUpload bucket object headers = do
275− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPost
276− , riBucket = Just bucket
277− , riObject = Just object
278− , riQueryParams = [("uploads", Nothing)]
279− , riHeaders = headers
280− }
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+ }
281353 parseNewMultipartUpload $ NC.responseBody resp
282354
283355-- | PUT a part of an object as part of a multipart upload.
284−putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
285− -> Payload -> Minio PartTuple
356+putObjectPart ::
357+ Bucket ->
358+ Object ->
359+ UploadId ->
360+ PartNumber ->
361+ [HT.Header] ->
362+ Payload ->
363+ Minio PartTuple
286364putObjectPart bucket object uploadId partNumber headers payload = do
287365 -- transform payload to conduit to enable streaming signature
288366 let payload' = mkStreamingPayload payload
289− resp <- executeRequest $
290− defaultS3ReqInfo { riMethod = HT.methodPut
291− , riBucket = Just bucket
292− , riObject = Just object
293− , riQueryParams = mkOptionalParams params
294− , riHeaders = headers
295− , riPayload = payload'
296− }
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+ }
297377 let rheaders = NC.responseHeaders resp
298378 etag = getETagHeader rheaders
299379 maybe
300380 (throwIO MErrVETagHeaderNotFound)
301− (return . (partNumber, )) etag
381+ (return . (partNumber,))
382+ etag
302383 where
303− params = [
304− ("uploadId", Just uploadId)
305− , ("partNumber", Just $ show partNumber)
384+ params =
385+ [ ("uploadId", Just uploadId),
386+ ("partNumber", Just $ show partNumber)
306387 ]
307388
308389srcInfoToHeaders :: SourceInfo -> [HT.Header]
309−srcInfoToHeaders srcInfo = ("x-amz-copy-source",
310− toS $ T.concat ["/", srcBucket srcInfo,
311− "/", srcObject srcInfo]
312− ) : 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
313402 where
314− names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",
315− "x-amz-copy-source-if-unmodified-since",
316− "x-amz-copy-source-if-modified-since"]
317− values = mapMaybe (fmap encodeUtf8 . (srcInfo &))
318− [srcIfMatch, srcIfNoneMatch,
319− fmap formatRFC1123 . srcIfUnmodifiedSince,
320− fmap formatRFC1123 . srcIfModifiedSince]
321− rangeHdr = maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])])
322− $ 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)
323419 toByteRange :: (Int64, Int64) -> HT.ByteRange
324420 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
325421
326422-- | Performs server-side copy of an object or part of an object as an
327423-- upload part of an ongoing multi-part upload.
328−copyObjectPart :: DestinationInfo -> SourceInfo -> UploadId
329− -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
424+copyObjectPart ::
425+ DestinationInfo ->
426+ SourceInfo ->
427+ UploadId ->
428+ PartNumber ->
429+ [HT.Header] ->
430+ Minio (ETag, UTCTime)
330431copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
331− resp <- executeRequest $
332− defaultS3ReqInfo { riMethod = HT.methodPut
333− , riBucket = Just $ dstBucket dstInfo
334− , riObject = Just $ dstObject dstInfo
335− , riQueryParams = mkOptionalParams params
336− , riHeaders = headers ++ srcInfoToHeaders srcInfo
337− }
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+ }
338441
339442 parseCopyObjectResponse $ NC.responseBody resp
340443 where
341− params = [
342− ("uploadId", Just uploadId)
343− , ("partNumber", Just $ show partNumber)
444+ params =
445+ [ ("uploadId", Just uploadId),
446+ ("partNumber", Just $ show partNumber)
344447 ]
345448
346449-- | Performs server-side copy of an object that is upto 5GiB in
347450-- size. If the object is greater than 5GiB, this function throws the
348451-- error returned by the server.
349−copyObjectSingle :: Bucket -> Object -> SourceInfo -> [HT.Header]
350− -> Minio (ETag, UTCTime)
452+copyObjectSingle ::
453+ Bucket ->
454+ Object ->
455+ SourceInfo ->
456+ [HT.Header] ->
457+ Minio (ETag, UTCTime)
351458copyObjectSingle bucket object srcInfo headers = do
352459 -- validate that srcRange is Nothing for this API.
353460 when (isJust $ srcRange srcInfo) $
354461 throwIO MErrVCopyObjSingleNoRangeAccepted
355− resp <- executeRequest $
356− defaultS3ReqInfo { riMethod = HT.methodPut
357− , riBucket = Just bucket
358− , riObject = Just object
359− , riHeaders = headers ++ srcInfoToHeaders srcInfo
360− }
462+ resp <-
463+ executeRequest $
464+ defaultS3ReqInfo
465+ { riMethod = HT.methodPut,
466+ riBucket = Just bucket,
467+ riObject = Just object,
468+ riHeaders = headers ++ srcInfoToHeaders srcInfo
469+ }
361470 parseCopyObjectResponse $ NC.responseBody resp
362471
363472-- | Complete a multipart upload.
364−completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
365− -> Minio ETag
473+completeMultipartUpload ::
474+ Bucket ->
475+ Object ->
476+ UploadId ->
477+ [PartTuple] ->
478+ Minio ETag
366479completeMultipartUpload bucket object uploadId partTuple = do
367− resp <- executeRequest $
368− defaultS3ReqInfo { riMethod = HT.methodPost
369− , riBucket = Just bucket
370− , riObject = Just object
371− , riQueryParams = mkOptionalParams params
372− , riPayload = PayloadBS $
373− mkCompleteMultipartUploadRequest partTuple
374− }
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+ }
375491 parseCompleteMultipartUploadResponse $ NC.responseBody resp
376492 where
377493 params = [("uploadId", Just uploadId)]
378494
379495-- | Abort a multipart upload.
380496abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
381−abortMultipartUpload bucket object uploadId = void $
382− executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
383− , riBucket = Just bucket
384− , riObject = Just object
385− , riQueryParams = mkOptionalParams params
386− }
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+ }
387506 where
388507 params = [("uploadId", Just uploadId)]
389508
390509-- | List incomplete multipart uploads.
391−listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
392− -> 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
393518listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
394− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
395− , riBucket = Just bucket
396− , riQueryParams = params
397− }
519+ resp <-
520+ executeRequest $
521+ defaultS3ReqInfo
522+ { riMethod = HT.methodGet,
523+ riBucket = Just bucket,
524+ riQueryParams = params
525+ }
398526 parseListUploadsResponse $ NC.responseBody resp
399527 where
400528 -- build query params
401− params = ("uploads", Nothing) : mkOptionalParams
402− [ ("prefix", prefix)
403− , ("delimiter", delimiter)
404− , ("key-marker", keyMarker)
405− , ("upload-id-marker", uploadIdMarker)
406− , ("max-uploads", show <$> maxKeys)
407− ]
408−
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+ ]
409538
410539-- | List parts of an ongoing multipart upload.
411−listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
412− -> Maybe Text -> Minio ListPartsResult
540+listIncompleteParts' ::
541+ Bucket ->
542+ Object ->
543+ UploadId ->
544+ Maybe Text ->
545+ Maybe Text ->
546+ Minio ListPartsResult
413547listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
414− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
415− , riBucket = Just bucket
416− , riObject = Just object
417− , riQueryParams = mkOptionalParams params
418− }
548+ resp <-
549+ executeRequest $
550+ defaultS3ReqInfo
551+ { riMethod = HT.methodGet,
552+ riBucket = Just bucket,
553+ riObject = Just object,
554+ riQueryParams = mkOptionalParams params
555+ }
419556 parseListPartsResponse $ NC.responseBody resp
420557 where
421558 -- build optional query params
422− params = [
423− ("uploadId", Just uploadId)
424− , ("part-number-marker", partNumMarker)
425− , ("max-parts", maxParts)
559+ params =
560+ [ ("uploadId", Just uploadId),
561+ ("part-number-marker", partNumMarker),
562+ ("max-parts", maxParts)
426563 ]
427564
428565-- | Get metadata of an object.
429566headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
430567headObject bucket object reqHeaders = do
431− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
432− , riBucket = Just bucket
433− , riObject = Just object
434− , riHeaders = reqHeaders
435− }
436−
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+ }
437581 maybe (throwIO MErrVInvalidObjectInfoResponse) return $
438− parseGetObjectHeaders object $ NC.responseHeaders resp
439−
582+ parseGetObjectHeaders object $
583+ NC.responseHeaders resp
440584
441585-- | Query the object store if a given bucket exists.
442586headBucket :: Bucket -> Minio Bool
443−headBucket bucket = headBucketEx `catches`
444− [ Handler handleNoSuchBucket
445− , Handler handleStatus404
446− ]
447−
587+headBucket bucket =
588+ headBucketEx
589+ `catches` [ Handler handleNoSuchBucket,
590+ Handler handleStatus404
591+ ]
448592 where
449593 handleNoSuchBucket :: ServiceErr -> Minio Bool
450− handleNoSuchBucket e | e == NoSuchBucket = return False
451− | otherwise = throwIO e
452−
594+ handleNoSuchBucket e
595+ | e == NoSuchBucket = return False
596+ | otherwise = throwIO e
453597 handleStatus404 :: NC.HttpException -> Minio Bool
454598 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
455599 if NC.responseStatus res == status404
456− then return False
457− else throwIO e
600+ then return False
601+ else throwIO e
458602 handleStatus404 e = throwIO e
459−
460603 headBucketEx = do
461− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
462− , riBucket = Just bucket
463− }
604+ resp <-
605+ executeRequest $
606+ defaultS3ReqInfo
607+ { riMethod = HT.methodHead,
608+ riBucket = Just bucket
609+ }
464610 return $ NC.responseStatus resp == HT.ok200
465611
466612-- | Set the notification configuration on a bucket.
467613putBucketNotification :: Bucket -> Notification -> Minio ()
468614putBucketNotification bucket ncfg = do
469615 ns <- asks getSvcNamespace
470− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
471− , riBucket = Just bucket
472− , riQueryParams = [("notification", Nothing)]
473− , riPayload = PayloadBS $
474− mkPutNotificationRequest ns ncfg
475− }
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+ }
476626
477627-- | Retrieve the notification configuration on a bucket.
478628getBucketNotification :: Bucket -> Minio Notification
479629getBucketNotification bucket = do
480− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
481− , riBucket = Just bucket
482− , riQueryParams = [("notification", Nothing)]
483− }
630+ resp <-
631+ executeRequest $
632+ defaultS3ReqInfo
633+ { riMethod = HT.methodGet,
634+ riBucket = Just bucket,
635+ riQueryParams = [("notification", Nothing)]
636+ }
484637 parseNotification $ NC.responseBody resp
485638
486639-- | Remove all notifications configured on a bucket.
@@ -490,11 +643,14 @@ removeAllBucketNotification = flip putBucketNotification defaultNotification
490643-- | Fetch the policy if any on a bucket.
491644getBucketPolicy :: Bucket -> Minio Text
492645getBucketPolicy bucket = do
493− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
494− , riBucket = Just bucket
495− , riQueryParams = [("policy", Nothing)]
496− }
497− 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
498654
499655-- | Set a new policy on a bucket.
500656-- As a special condition if the policy is empty
@@ -506,18 +662,24 @@ setBucketPolicy bucket policy = do
506662 else putBucketPolicy bucket policy
507663
508664-- | Save a new policy on a bucket.
509−putBucketPolicy :: Bucket -> Text -> Minio()
665+putBucketPolicy :: Bucket -> Text -> Minio ()
510666putBucketPolicy bucket policy = do
511− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
512− , riBucket = Just bucket
513− , riQueryParams = [("policy", Nothing)]
514− , riPayload = PayloadBS $ encodeUtf8 policy
515− }
667+ void $
668+ executeRequest $
669+ defaultS3ReqInfo
670+ { riMethod = HT.methodPut,
671+ riBucket = Just bucket,
672+ riQueryParams = [("policy", Nothing)],
673+ riPayload = PayloadBS $ encodeUtf8 policy
674+ }
516675
517676-- | Delete any policy set on a bucket.
518−deleteBucketPolicy :: Bucket -> Minio()
677+deleteBucketPolicy :: Bucket -> Minio ()
519678deleteBucketPolicy bucket = do
520− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
521− , riBucket = Just bucket
522− , riQueryParams = [("policy", Nothing)]
523− }
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−238
@@ -15,113 +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− , InputSerialization
34− , defaultCsvInput
35− , linesJsonInput
36− , documentJsonInput
37− , defaultParquetInput
38− , setInputCSVProps
39−
40− , CompressionType(..)
41− , setInputCompressionType
42−
43− -- *** CSV Format details
44−
45− -- | CSV format options such as delimiters and quote characters are
46− -- specified using using the functions below. Options are combined
47− -- monoidally.
48−
49− , CSVProp
50− , recordDelimiter
51− , fieldDelimiter
52− , quoteCharacter
53− , quoteEscapeCharacter
54− , commentCharacter
55− , allowQuotedRecordDelimiter
56− , FileHeaderInfo(..)
57− , fileHeaderInfo
58− , QuoteFields(..)
59− , quoteFields
60−
61− -- *** Output Serialization
62−
63− , OutputSerialization
64− , defaultCsvOutput
65− , defaultJsonOutput
66− , outputCSVFromProps
67− , outputJSONFromRecordDelimiter
68−
69− -- *** Progress messages
70−
71− , setRequestProgressEnabled
72−
73− -- *** Interpreting Select output
74−
75− -- | The conduit returned by `selectObjectContent` returns values of
76− -- the `EventMessage` data type. This returns the query output
77− -- messages formatted according to the chosen output serialization,
78− -- interleaved with progress messages (if enabled by
79− -- `setRequestProgressEnabled`), and at the end a statistics
80− -- message.
81− --
82− -- If the application is interested in only the payload, then
83− -- `getPayloadBytes` can be used. For example to simply print the
84− -- payload to stdout:
85− --
86− -- > resultConduit <- selectObjectContent bucket object mySelectRequest
87− -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
88− --
89− -- Note that runConduit, the connect operator (.|) and stdoutC are
90− -- all from the "conduit" package.
91−
92− , getPayloadBytes
93− , EventMessage(..)
94− , Progress(..)
95− , Stats
96− ) where
97−
98−import Conduit ((.|))
99−import qualified Conduit as C
100−import qualified Data.Binary as Bin
101−import qualified Data.ByteString as B
102−import qualified Data.ByteString.Lazy as LB
103−import Data.Digest.CRC32 (crc32, crc32Update)
104−import qualified Network.HTTP.Conduit as NC
105−import qualified Network.HTTP.Types as HT
106−import UnliftIO (MonadUnliftIO)
107−
108−import Lib.Prelude
109−
110−import Network.Minio.API
111−import Network.Minio.Data
112−import Network.Minio.Errors
113−import Network.Minio.Utils
114−import Network.Minio.XmlGenerator
115−import Network.Minio.XmlParser
116−
117−data EventStreamException = ESEPreludeCRCFailed
118− | ESEMessageCRCFailed
119− | ESEUnexpectedEndOfStream
120− | ESEDecodeFail [Char]
121− | ESEInvalidHeaderType
122− | ESEInvalidHeaderValueType
123− | ESEInvalidMessageType
124− 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)
125115
126116instance Exception EventStreamException
127117
@@ -129,171 +119,176 @@ instance Exception EventStreamException
129119chunkSize :: Int
130120chunkSize = 32 * 1024
131121
132−parseBinary :: Bin.Binary a => ByteString -> IO a
122+parseBinary :: (Bin.Binary a) => ByteString -> IO a
133123parseBinary b = do
134− case Bin.decodeOrFail $ LB.fromStrict b of
135− Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
136− Right (_, _, r) -> return r
124+ case Bin.decodeOrFail $ LB.fromStrict b of
125+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
126+ Right (_, _, r) -> return r
137127
138128bytesToHeaderName :: Text -> IO MsgHeaderName
139129bytesToHeaderName t = case t of
140− ":message-type" -> return MessageType
141− ":event-type" -> return EventType
142− ":content-type" -> return ContentType
143− ":error-code" -> return ErrorCode
130+ ":message-type" -> return MessageType
131+ ":event-type" -> return EventType
132+ ":content-type" -> return ContentType
133+ ":error-code" -> return ErrorCode
144134 ":error-message" -> return ErrorMessage
145− _ -> throwIO ESEInvalidHeaderType
135+ _ -> throwIO ESEInvalidHeaderType
146136
147−parseHeaders :: MonadUnliftIO m
148− => Word32 -> C.ConduitM ByteString a m [MessageHeader]
137+parseHeaders ::
138+ (MonadUnliftIO m) =>
139+ Word32 ->
140+ C.ConduitM ByteString a m [MessageHeader]
149141parseHeaders 0 = return []
150142parseHeaders hdrLen = do
151− bs1 <- readNBytes 1
152− n :: Word8 <- liftIO $ parseBinary bs1
143+ bs1 <- readNBytes 1
144+ n :: Word8 <- liftIO $ parseBinary bs1
153145
154− headerKeyBytes <- readNBytes $ fromIntegral n
155− let headerKey = decodeUtf8Lenient headerKeyBytes
156− headerName <- liftIO $ bytesToHeaderName headerKey
146+ headerKeyBytes <- readNBytes $ fromIntegral n
147+ let headerKey = decodeUtf8Lenient headerKeyBytes
148+ headerName <- liftIO $ bytesToHeaderName headerKey
157149
158− bs2 <- readNBytes 1
159− headerValueType :: Word8 <- liftIO $ parseBinary bs2
160− when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
150+ bs2 <- readNBytes 1
151+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
152+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
161153
162− bs3 <- readNBytes 2
163− vLen :: Word16 <- liftIO $ parseBinary bs3
164− headerValueBytes <- readNBytes $ fromIntegral vLen
165− let headerValue = decodeUtf8Lenient headerValueBytes
166− m = (headerName, headerValue)
167− 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
168160
169− ms <- parseHeaders (hdrLen - k)
170− return (m:ms)
161+ ms <- parseHeaders (hdrLen - k)
162+ return (m : ms)
171163
172164-- readNBytes returns N bytes read from the string and throws an
173165-- exception if N bytes are not present on the stream.
174−readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
166+readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
175167readNBytes n = do
176− b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
177− if B.length b /= n
178− then throwIO ESEUnexpectedEndOfStream
179− else return b
180−
181−crcCheck :: MonadUnliftIO m
182− => 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 ()
183176crcCheck = do
184− b <- readNBytes 12
185− n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
186− preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
187− when (crc32 (B.take 8 b) /= preludeCRC) $
188− 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
189182
190− -- we do not yield the checksum
191− C.yield $ B.take 8 b
183+ -- we do not yield the checksum
184+ C.yield $ B.take 8 b
192185
193− -- 12 bytes have been read off the current message. Now read the
194− -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
195− let startCrc = crc32 b
196− 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
197190
198− bs <- readNBytes 4
199− expectedCrc :: Word32 <- liftIO $ parseBinary bs
191+ bs <- readNBytes 4
192+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
200193
201− when (finalCrc /= expectedCrc) $
202− throwIO ESEMessageCRCFailed
194+ when (finalCrc /= expectedCrc) $
195+ throwIO ESEMessageCRCFailed
203196
204− -- we unconditionally recurse - downstream figures out when to
205− -- quit reading the stream
206− crcCheck
197+ -- we unconditionally recurse - downstream figures out when to
198+ -- quit reading the stream
199+ crcCheck
207200 where
208201 accumulateYield n checkSum = do
209− let toRead = min n chunkSize
210− b <- readNBytes toRead
211− let c' = crc32Update checkSum b
212− n' = n - B.length b
213− C.yield b
214− if n' > 0
215− then accumulateYield n' c'
216− else return c'
217−
218−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 ()
219212handleMessage = do
220− b1 <- readNBytes 4
221− msgLen :: Word32 <- liftIO $ parseBinary b1
222−
223− b2 <- readNBytes 4
224− hdrLen :: Word32 <- liftIO $ parseBinary b2
225−
226− hs <- parseHeaders hdrLen
227−
228− let payloadLen = msgLen - hdrLen - 16
229− getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)
230− eventHdrValue = getHdrVal EventType hs
231− msgHdrValue = getHdrVal MessageType hs
232− errCode = getHdrVal ErrorCode hs
233− errMsg = getHdrVal ErrorMessage hs
234−
235− case msgHdrValue of
236− Just "event" -> do
237− case eventHdrValue of
238− Just "Records" -> passThrough $ fromIntegral payloadLen
239− Just "Cont" -> return ()
240− Just "Progress" -> do
241− bs <- readNBytes $ fromIntegral payloadLen
242− progress <- parseSelectProgress bs
243− C.yield $ ProgressEventMessage progress
244− Just "Stats" -> do
245− bs <- readNBytes $ fromIntegral payloadLen
246− stats <- parseSelectProgress bs
247− C.yield $ StatsEventMessage stats
248− Just "End" -> return ()
249− _ -> throwIO ESEInvalidMessageType
250− when (eventHdrValue /= Just "End") handleMessage
251−
252− Just "error" -> do
253− let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
254− maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
255−
256− _ -> throwIO ESEInvalidMessageType
257−
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
258248 where
259249 passThrough 0 = return ()
260250 passThrough n = do
261− let c = min n chunkSize
262− b <- readNBytes c
263− C.yield $ RecordPayloadEventMessage b
264− passThrough $ n - B.length b
265−
266−
267−selectProtoConduit :: MonadUnliftIO m
268− => 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 ()
269259selectProtoConduit = crcCheck .| handleMessage
270260
271261-- | selectObjectContent calls the SelectRequest on the given
272262-- object. It returns a Conduit of event messages that can be consumed
273263-- by the client.
274−selectObjectContent :: Bucket -> Object -> SelectRequest
275− -> Minio (C.ConduitT () EventMessage Minio ())
264+selectObjectContent ::
265+ Bucket ->
266+ Object ->
267+ SelectRequest ->
268+ Minio (C.ConduitT () EventMessage Minio ())
276269selectObjectContent b o r = do
277− let reqInfo = defaultS3ReqInfo { riMethod = HT.methodPost
278− , riBucket = Just b
279− , riObject = Just o
280− , riPayload = PayloadBS $ mkSelectRequest r
281− , riNeedsLocation = False
282− , riQueryParams = [("select", Nothing), ("select-type", Just "2")]
283− }
284− --print $ mkSelectRequest r
285− resp <- mkStreamRequest reqInfo
286− 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
287282
288283-- | A helper conduit that returns only the record payload bytes.
289−getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
284+getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
290285getPayloadBytes = do
291− evM <- C.await
292− case evM of
293− Just v -> do
294− case v of
295− RecordPayloadEventMessage b -> C.yield b
296− RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
297− _ -> return ()
298− getPayloadBytes
299− 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+430−327
@@ -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.HashMap.Strict as Map
26−import qualified Data.HashSet 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
4255ignoredHeaders :: Set.HashSet 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 ""
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 =
181211 filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
182− map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
183−
184−mkCanonicalRequest :: Bool -> SignParams -> NC.Request -> [(ByteString, ByteString)]
185− -> ByteString
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.HashMap 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+140−127
@@ -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,70 +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.HashMap.Strict as H
27−import qualified Data.List as List
28−import qualified Data.Text as T
29−import Data.Text.Read (decimal)
30−import Data.Time (defaultTimeLocale, parseTimeM,
31− rfc822DateFormat)
32−import Network.HTTP.Conduit (Response)
33−import qualified Network.HTTP.Conduit as NC
34−import qualified Network.HTTP.Types as HT
35−import qualified Network.HTTP.Types.Header as Hdr
36−import qualified System.IO as IO
37−import qualified UnliftIO as U
38−import qualified UnliftIO.Async as A
39−import qualified UnliftIO.MVar as UM
40−
41−import Lib.Prelude
42−
43−import Network.Minio.Data
44−import Network.Minio.Data.ByteString
45−import Network.Minio.JsonParser (parseErrResponseJSON)
46−import Network.Minio.XmlParser (parseErrResponse)
47−
48−allocateReadFile :: (MonadUnliftIO m, R.MonadResource m)
49− => 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)
5050allocateReadFile fp = do
5151 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
52− either (\(e :: IOException) -> throwIO e) (return . (rk,)) hdlE
52+ either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
5353 where
5454 openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
5555 cleanup = either (const $ return ()) IO.hClose
5656
5757-- | Queries the file size from the handle. Catches any file operation
5858-- exceptions and returns Nothing instead.
59−getFileSize :: (MonadUnliftIO m, R.MonadResource m)
60− => Handle -> m (Maybe Int64)
59+getFileSize ::
60+ (MonadUnliftIO m) =>
61+ Handle ->
62+ m (Maybe Int64)
6163getFileSize h = do
6264 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
6365 case resE of
64− Left (_ :: IOException) -> return Nothing
65− Right s -> return $ Just s
66+ Left (_ :: U.IOException) -> return Nothing
67+ Right s -> return $ Just s
6668
6769-- | Queries if handle is seekable. Catches any file operation
6870-- exceptions and return False instead.
69−isHandleSeekable :: (R.MonadResource m, MonadUnliftIO m)
70− => Handle -> m Bool
71+isHandleSeekable ::
72+ (R.MonadResource m) =>
73+ Handle ->
74+ m Bool
7175isHandleSeekable h = do
7276 resE <- liftIO $ try $ IO.hIsSeekable h
7377 case resE of
74− Left (_ :: IOException) -> return False
75− Right v -> return v
78+ Left (_ :: U.IOException) -> return False
79+ Right v -> return v
7680
7781-- | Helper function that opens a handle to the filepath and performs
7882-- the given action on it. Exceptions of type MError are caught and
7983-- returned - both during file handle allocation and when the action
8084-- is run.
81−withNewHandle :: (MonadUnliftIO m, R.MonadResource m)
82− => 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)
8390withNewHandle fp fileAction = do
8491 -- opening a handle can throw MError exception.
8592 handleE <- try $ allocateReadFile fp
@@ -93,34 +100,61 @@ withNewHandle fp fileAction = do
93100 return resE
94101
95102mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
96−mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
103+mkHeaderFromPairs = map (first mk)
97104
98105lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
99−lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
106+lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
100107
101108getETagHeader :: [HT.Header] -> Maybe Text
102109getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
103110
104111getMetadata :: [HT.Header] -> [(Text, Text)]
105112getMetadata =
106− map ((\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y)))
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
107124
108125toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
109126toMaybeMetadataHeader (k, v) =
110− (, v) <$> userMetadataHeaderNameMaybe k
127+ (,v) <$> userMetadataHeaderNameMaybe k
111128
112129getNonUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
113−getNonUserMetadataMap = H.fromList
114− . filter ( isNothing
115− . userMetadataHeaderNameMaybe
116− . fst
117− )
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))
118145
119146-- | This function collects all headers starting with `x-amz-meta-`
120147-- and strips off this prefix, and returns a map.
121148getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
122−getUserMetadataMap = H.fromList
123− . mapMaybe toMaybeMetadataHeader
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_
124158
125159getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
126160getLastModifiedHeader hs = do
@@ -130,19 +164,21 @@ getLastModifiedHeader hs = do
130164getContentLength :: [HT.Header] -> Maybe Int64
131165getContentLength hs = do
132166 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
133− fst <$> hush (decimal nbs)
134−
167+ fst <$> either (const Nothing) Just (decimal nbs)
135168
136169decodeUtf8Lenient :: ByteString -> Text
137170decodeUtf8Lenient = decodeUtf8With lenientDecode
138171
139172isSuccessStatus :: HT.Status -> Bool
140−isSuccessStatus sts = let s = HT.statusCode sts
141− in (s >= 200 && s < 300)
142−
143−httpLbs :: MonadIO m
144− => NC.Request -> NC.Manager
145− -> 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)
146182httpLbs req mgr = do
147183 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
148184 resp <- either throwIO return respE
@@ -154,21 +190,26 @@ httpLbs req mgr = do
154190 Just "application/json" -> do
155191 sErr <- parseErrResponseJSON $ NC.responseBody resp
156192 throwIO sErr
157−
158− _ -> throwIO $ NC.HttpExceptionRequest req $
159− NC.StatusCodeException (void resp) (show resp)
193+ _ ->
194+ throwIO $
195+ NC.HttpExceptionRequest req $
196+ NC.StatusCodeException (void resp) (showBS resp)
160197
161198 return resp
162199 where
163− tryHttpEx :: IO (NC.Response LByteString)
164− -> IO (Either NC.HttpException (NC.Response LByteString))
200+ tryHttpEx ::
201+ IO (NC.Response LByteString) ->
202+ IO (Either NC.HttpException (NC.Response LByteString))
165203 tryHttpEx = try
166− contentTypeMay resp = lookupHeader Hdr.hContentType $
167− NC.responseHeaders resp
168−
169−http :: (MonadUnliftIO m, R.MonadResource m)
170− => NC.Request -> NC.Manager
171− -> 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 ()))
172213http req mgr = do
173214 respE <- tryHttpEx $ NC.http req mgr
174215 resp <- either throwIO return respE
@@ -178,25 +219,31 @@ http req mgr = do
178219 respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
179220 sErr <- parseErrResponse respBody
180221 throwIO sErr
181−
182222 _ -> do
183223 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
184− throwIO $ NC.HttpExceptionRequest req $
185− NC.StatusCodeException (void resp) content
186−
224+ throwIO $
225+ NC.HttpExceptionRequest req $
226+ NC.StatusCodeException (void resp) content
187227
188228 return resp
189229 where
190− tryHttpEx :: (MonadUnliftIO m) => m a
191− -> m (Either NC.HttpException a)
230+ tryHttpEx ::
231+ (MonadUnliftIO m) =>
232+ m a ->
233+ m (Either NC.HttpException a)
192234 tryHttpEx = try
193− contentTypeMay resp = lookupHeader Hdr.hContentType $
194− NC.responseHeaders resp
235+ contentTypeMay resp =
236+ lookupHeader Hdr.hContentType $
237+ NC.responseHeaders resp
195238
196239-- Similar to mapConcurrently but limits the number of threads that
197240-- can run using a quantity semaphore.
198−limitedMapConcurrently :: MonadUnliftIO m
199− => Int -> (t -> m a) -> [t] -> m [a]
241+limitedMapConcurrently ::
242+ (MonadUnliftIO m) =>
243+ Int ->
244+ (t -> m a) ->
245+ [t] ->
246+ m [a]
200247limitedMapConcurrently 0 _ _ = return []
201248limitedMapConcurrently count act args = do
202249 t' <- U.newTVarIO count
@@ -205,17 +252,15 @@ limitedMapConcurrently count act args = do
205252 where
206253 wThread t arg =
207254 U.bracket_ (waitSem t) (signalSem t) $ act arg
208−
209255 -- quantity semaphore implementation using TVar
210256 waitSem t = U.atomically $ do
211257 v <- U.readTVar t
212258 if v > 0
213− then U.writeTVar t (v-1)
214− else U.retrySTM
215−
259+ then U.writeTVar t (v - 1)
260+ else U.retrySTM
216261 signalSem t = U.atomically $ do
217262 v <- U.readTVar t
218− U.writeTVar t (v+1)
263+ U.writeTVar t (v + 1)
219264
220265-- helper function to 'drop' empty optional parameter.
221266mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -224,7 +269,7 @@ mkQuery k mv = (k,) <$> mv
224269-- helper function to build query parameters that are optional.
225270-- don't use it with mandatory query params with empty value.
226271mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
227−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
272+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
228273
229274-- | Conduit that rechunks bytestrings into the given chunk
230275-- lengths. Stops after given chunk lengths are yielded. Stops if
@@ -232,41 +277,9 @@ mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
232277-- received. Does not throw any errors.
233278chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
234279chunkBSConduit [] = return ()
235−chunkBSConduit (s:ss) = do
236− bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
237− if | B.length bs == s -> C.yield bs >> chunkBSConduit ss
238− | B.length bs > 0 -> C.yield bs
239− | otherwise -> return ()
240−
241−-- | Select part sizes - the logic is that the minimum part-size will
242−-- be 64MiB.
243−selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
244−selectPartSizes size = uncurry (List.zip3 [1..]) $
245− List.unzip $ loop 0 size
246− where
247− ceil :: Double -> Int64
248− ceil = ceiling
249− partSize = max minPartSize (ceil $ fromIntegral size /
250− fromIntegral maxMultipartParts)
251−
252− m = fromIntegral partSize
253− loop st sz
254− | st > sz = []
255− | st + m >= sz = [(st, sz - st)]
256− | otherwise = (st, m) : loop (st + m) sz
257−
258−lookupRegionCache :: Bucket -> Minio (Maybe Region)
259−lookupRegionCache b = do
260− rMVar <- asks mcRegionMap
261− rMap <- UM.readMVar rMVar
262− return $ H.lookup b rMap
263−
264−addToRegionCache :: Bucket -> Region -> Minio ()
265−addToRegionCache b region = do
266− rMVar <- asks mcRegionMap
267− UM.modifyMVar_ rMVar $ return . H.insert b region
268−
269−deleteFromRegionCache :: Bucket -> Minio ()
270−deleteFromRegionCache b = do
271− rMVar <- asks mcRegionMap
272− UM.modifyMVar_ rMVar $ return . H.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−154
@@ -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 qualified Data.HashMap.Strict as H
34−import Data.List (zip3, zip4, zip6)
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−uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
56−uncurry6 f (a, b, c, d, e, g) = f a b c d e g
57−
58−-- | Parse time strings from XML
59−parseS3XMLTime :: MonadIO m => Text -> m UTCTime
60−parseS3XMLTime t =
61− maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
62− parseTimeM True defaultTimeLocale s3TimeFormat $ T.unpack t
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,41 +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 (uncurry6 ObjectInfo) $
153− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
111+ let objects =
112+ map (uncurry6 ObjectInfo) $
113+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
154114
155115 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
156116
@@ -159,28 +119,24 @@ parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
159119parseListObjectsResponse xmldata = do
160120 r <- parseRoot xmldata
161121 ns <- asks getSvcNamespace
162− let
163− s3Elem' = s3Elem ns
164− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
165−
166− nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ content
167−
168− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
169−
170− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
171− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
172− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
173− -- if response xml contains empty etag response fill them with as
174− -- many empty Text for the zip4 below to work as intended.
175− etags = etagsList ++ repeat ""
176− 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
177133
178134 modTimes <- mapM parseS3XMLTime modTimeStr
179135 sizes <- parseDecimals sizeStr
180136
181− let
182− objects = map (uncurry6 ObjectInfo) $
183− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
137+ let objects =
138+ map (uncurry6 ObjectInfo) $
139+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
184140
185141 return $ ListObjectsResult hasMore nextToken objects prefixes
186142
@@ -189,20 +145,18 @@ parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
189145parseListUploadsResponse xmldata = do
190146 r <- parseRoot xmldata
191147 ns <- asks getSvcNamespace
192− let
193− s3Elem' = s3Elem ns
194− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
195− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
196− nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content
197− nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content
198− uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
199− uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
200− 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
201156
202157 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
203158
204− let
205− uploads = zip3 uploadKeys uploadIds uploadInitTimes
159+ let uploads = zip3 uploadKeys uploadIds uploadInitTimes
206160
207161 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
208162
@@ -210,34 +164,25 @@ parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
210164parseListPartsResponse xmldata = do
211165 r <- parseRoot xmldata
212166 ns <- asks getSvcNamespace
213− let
214− s3Elem' = s3Elem ns
215− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
216− nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content
217− partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
218− partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
219− partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
220− 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
221174
222175 partModTimes <- mapM parseS3XMLTime partModTimeStr
223176 partSizes <- parseDecimals partSizeStr
224177 partNumbers <- parseDecimals partNumberStr
225178 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
226179
227− let
228− partInfos = map (uncurry4 ObjectPartInfo) $
229− zip4 partNumbers partETags partSizes partModTimes
180+ let partInfos =
181+ map (uncurry4 ObjectPartInfo) $
182+ zip4 partNumbers partETags partSizes partModTimes
230183
231184 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
232185
233−
234−parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
235−parseErrResponse xmldata = do
236− r <- parseRoot xmldata
237− let code = T.concat $ r $/ element "Code" &/ content
238− message = T.concat $ r $/ element "Message" &/ content
239− return $ toServiceErr code message
240−
241186parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
242187parseNotification xmldata = do
243188 r <- parseRoot xmldata
@@ -246,32 +191,40 @@ parseNotification xmldata = do
246191 qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
247192 tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
248193 lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
249− Notification <$> (mapM (parseNode ns "Queue") qcfg)
250− <*> (mapM (parseNode ns "Topic") tcfg)
251− <*> (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
252198 where
253−
254199 getFilterRule ns c =
255200 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
256201 value = T.concat $ c $/ s3Elem ns "Value" &/ content
257− in FilterRule name value
258−
202+ in FilterRule name value
259203 parseNode ns arnName nodeData = do
260204 let c = fromNode nodeData
261− id = T.concat $ c $/ s3Elem ns "Id" &/ content
205+ itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
262206 arn = T.concat $ c $/ s3Elem ns arnName &/ content
263− events = catMaybes $ map textToEvent $ c $/ s3Elem ns "Event" &/ content
264− rules = c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key" &/
265− s3Elem ns "FilterRule" &| getFilterRule ns
266− return $ NotificationConfig id arn events
267− (Filter $ FilterKey $ FilterRules rules)
268−
269−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
270222parseSelectProgress xmldata = do
271− r <- parseRoot $ LB.fromStrict xmldata
272− let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
273− bProcessed = T.concat $ r $/element "BytesProcessed" &/ content
274− bReturned = T.concat $ r $/element "BytesReturned" &/ content
275− Progress <$> parseDecimal bScanned
276− <*> parseDecimal bProcessed
277− <*> 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-14.6
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-14.6
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+755−481
@@ -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,37 +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 Conduit as C
25−import qualified Control.Monad.Trans.Resource as R
26−import qualified Data.ByteString as BS
27−import Data.Conduit (yield)
28−import qualified Data.Conduit.Binary as CB
29−import Data.Conduit.Combinators (sinkList)
30−import qualified Data.HashMap.Strict as H
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.Data.Crypto
46−import Network.Minio.PutObject
47−import Network.Minio.S3API
48−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
4945
5046main :: IO ()
5147main = defaultMain tests
@@ -53,47 +49,107 @@ main = defaultMain tests
5349tests :: TestTree
5450tests = testGroup "Tests" [liveServerUnitTests]
5551
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+
5697-- conduit that generates random binary stream of given length
57−randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
58−randomDataSrc s' = genBS s'
98+randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
99+randomDataSrc = genBS
59100 where
60− concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++
61− [BS.take (fromIntegral r) bs]
62− where (q, r) = n `divMod` fromIntegral (BS.length bs)
63−
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)
64107 genBS s = do
65108 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
66109 let byteArr64 = BS.pack w8s
67110 if s < oneMiB
68111 then yield $ concatIt byteArr64 s
69− else do yield $ concatIt byteArr64 oneMiB
70− genBS (s - oneMiB)
112+ else do
113+ yield $ concatIt byteArr64 oneMiB
114+ genBS (s - oneMiB)
71115
72−mkRandFile :: R.MonadResource m => Int64 -> m FilePath
116+mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
73117mkRandFile size = do
74− dir <- liftIO $ getTemporaryDirectory
118+ dir <- liftIO getTemporaryDirectory
75119 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
76120
77121funTestBucketPrefix :: Text
78122funTestBucketPrefix = "miniohstest-"
79123
80−loadTestServer :: IO ConnectInfo
81−loadTestServer = do
82− val <- lookupEnv "MINIO_LOCAL"
83− isSecure <- lookupEnv "MINIO_SECURE"
84− return $ case (val, isSecure) of
85− (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"
86− (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"
87− (Nothing, _) -> minioPlayCI
88−
89−funTestWithBucket :: TestName
90− -> (([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
91147funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
92148 -- generate a random name for the bucket
93149 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
94150 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
95151 liftStep = liftIO . step
96− connInfo <- loadTestServer
152+ connInfo <- loadTestServerConnInfo
97153 ret <- runMinio connInfo $ do
98154 liftStep $ "Creating bucket for test - " ++ t
99155 foundBucket <- bucketExists b
@@ -103,26 +159,40 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
103159 deleteBucket b
104160 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
105161
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+
106173liveServerUnitTests :: TestTree
107−liveServerUnitTests = testGroup "Unit tests against a live server"
108− [ basicTests
109− , listingTest
110− , highLevelListingTest
111− , lowLevelMultipartTest
112− , putObjectSizeTest
113− , putObjectNoSizeTest
114− , multipartTest
115− , putObjectContentTypeTest
116− , putObjectContentLanguageTest
117− , putObjectStorageClassTest
118− , putObjectUserMetadataTest
119− , getObjectTest
120− , copyObjectTests
121− , presignedUrlFunTest
122− , presignedPostPolicyFunTest
123− , bucketPolicyFunTest
124− , getNPutSSECTest
125− ]
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+ ]
126196
127197basicTests :: TestTree
128198basicTests = funTestWithBucket "Basic tests" $
@@ -131,83 +201,109 @@ basicTests = funTestWithBucket "Basic tests" $
131201 buckets <- getService
132202 unless (length (filter (== bucket) $ map biName buckets) == 1) $
133203 liftIO $
134− assertFailure ("The bucket " ++ show bucket ++
135− " was expected to exist.")
204+ assertFailure
205+ ( "The bucket "
206+ ++ show bucket
207+ ++ " was expected to exist."
208+ )
136209
137210 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
138211 mbE <- try $ makeBucket bucket Nothing
139212 case mbE of
140213 Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
141− _ -> return ()
214+ _ -> return ()
142215
143216 step "makeBucket with an invalid bucket name and check for appropriate exception."
144217 invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
145218 case invalidMBE of
146219 Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
147− _ -> return ()
220+ _ -> return ()
148221
149222 step "getLocation works"
150223 region <- getLocation bucket
151224 liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
152225
153226 step "singlepart putObject works"
154− fPutObject bucket "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
227+ testFilepath <- mkRandFile 200
228+ fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
155229
156230 step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
157− fpE <- try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
231+ fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
158232 case fpE of
159233 Left exn -> liftIO $ exn @?= NoSuchBucket
160− _ -> return ()
234+ _ -> return ()
161235
162236 outFile <- mkRandFile 0
163237 step "simple fGetObject works"
164− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions
238+ fGetObject bucket "test-file" outFile defaultGetObjectOptions
165239
166240 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
167241 step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
168− resE <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
169− gooIfUnmodifiedSince = (Just unmodifiedTime)
170− }
242+ resE <-
243+ try $
244+ fGetObject
245+ bucket
246+ "test-file"
247+ outFile
248+ defaultGetObjectOptions
249+ { gooIfUnmodifiedSince = Just unmodifiedTime
250+ }
171251 case resE of
172252 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
173− _ -> return ()
253+ _ -> return ()
174254
175255 step "fGetObject an object with no matching etag, check for exception"
176− resE1 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
177− gooIfMatch = (Just "invalid-etag")
178− }
256+ resE1 <-
257+ try $
258+ fGetObject
259+ bucket
260+ "test-file"
261+ outFile
262+ defaultGetObjectOptions
263+ { gooIfMatch = Just "invalid-etag"
264+ }
179265 case resE1 of
180266 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
181− _ -> return ()
267+ _ -> return ()
182268
183269 step "fGetObject an object with no valid range, check for exception"
184− resE2 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
185− gooRange = (Just $ HT.ByteRangeFromTo 100 200)
186− }
270+ resE2 <-
271+ try $
272+ fGetObject
273+ bucket
274+ "test-file"
275+ outFile
276+ defaultGetObjectOptions
277+ { gooRange = Just $ HT.ByteRangeFromTo 100 300
278+ }
187279 case resE2 of
188− Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
189− _ -> return ()
280+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
281+ _ -> return ()
190282
191283 step "fGetObject on object with a valid range"
192− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
193− gooRange = (Just $ HT.ByteRangeFrom 1)
194− }
284+ fGetObject
285+ bucket
286+ "test-file"
287+ outFile
288+ defaultGetObjectOptions
289+ { gooRange = Just $ HT.ByteRangeFrom 1
290+ }
195291
196292 step "fGetObject a non-existent object and check for NoSuchKey exception"
197293 resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
198294 case resE3 of
199295 Left exn -> liftIO $ exn @?= NoSuchKey
200− _ -> return ()
296+ _ -> return ()
201297
202298 step "create new multipart upload works"
203299 uid <- newMultipartUpload bucket "newmpupload" []
204− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
300+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
205301
206302 step "abort a new multipart upload works"
207303 abortMultipartUpload bucket "newmpupload" uid
208304
209305 step "delete object works"
210− deleteObject bucket "lsb-release"
306+ deleteObject bucket "test-file"
211307
212308 step "statObject test"
213309 let object = "sample"
@@ -217,257 +313,310 @@ basicTests = funTestWithBucket "Basic tests" $
217313
218314 step "get metadata of the object"
219315 res <- statObject bucket object defaultGetObjectOptions
220− liftIO $ (oiSize res) @?= 0
316+ liftIO $ oiSize res @?= 0
221317
222318 step "delete object"
223319 deleteObject bucket object
224320
225321lowLevelMultipartTest :: TestTree
226322lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
227− \step bucket -> do
228− -- low-level multipart operation tests.
229− let object = "newmpupload"
230− mb15 = 15 * 1024 * 1024
323+ \step bucket -> do
324+ -- low-level multipart operation tests.
325+ let object = "newmpupload"
326+ mb15 = 15 * 1024 * 1024
231327
232− step "Prepare for low-level multipart tests."
233− step "create new multipart upload"
234− uid <- newMultipartUpload bucket object []
235− 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."
236332
237− randFile <- mkRandFile mb15
333+ randFile <- mkRandFile mb15
238334
239− step "put object parts 1 of 1"
240− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
241− 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
242339
243− step "complete multipart"
244− void $ completeMultipartUpload bucket object uid [partInfo]
340+ step "complete multipart"
341+ void $ completeMultipartUpload bucket object uid [partInfo]
245342
246− destFile <- mkRandFile 0
247− step "Retrieve the created object and check size"
248− fGetObject bucket object destFile defaultGetObjectOptions
249− gotSize <- withNewHandle destFile getFileSize
250− liftIO $ gotSize == Right (Just mb15) @?
251− "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"
252351
253− step "Cleanup actions"
254− removeObject bucket object
352+ step "Cleanup actions"
353+ removeObject bucket object
255354
256355putObjectSizeTest :: TestTree
257356putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
258357 \step bucket -> do
259− -- putObject test (conduit source, size specified)
260− let obj = "msingle"
261− mb1 = 1 * 1024 * 1024
358+ -- putObject test (conduit source, size specified)
359+ let obj = "msingle"
360+ mb1 = 1 * 1024 * 1024
262361
263− step "Prepare for putObject with from source with size."
264− rFile <- mkRandFile mb1
362+ step "Prepare for putObject with from source with size."
363+ rFile <- mkRandFile mb1
265364
266− step "Upload single file."
267− putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
365+ step "Upload single file."
366+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
268367
269− step "Retrieve and verify file size"
270− destFile <- mkRandFile 0
271− fGetObject bucket obj destFile defaultGetObjectOptions
272− gotSize <- withNewHandle destFile getFileSize
273− liftIO $ gotSize == Right (Just mb1) @?
274− "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"
275376
276− step "Cleanup actions"
277− deleteObject bucket obj
377+ step "Cleanup actions"
378+ deleteObject bucket obj
278379
279380putObjectNoSizeTest :: TestTree
280381putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
281382 \step bucket -> do
282− -- putObject test (conduit source, no size specified)
283− let obj = "mpart"
284− mb70 = 70 * 1024 * 1024
383+ -- putObject test (conduit source, no size specified)
384+ let obj = "mpart"
385+ mb70 = 70 * 1024 * 1024
285386
286− step "Prepare for putObject with from source without providing size."
287− rFile <- mkRandFile mb70
387+ step "Prepare for putObject with from source without providing size."
388+ rFile <- mkRandFile mb70
288389
289− step "Upload multipart file."
290− putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
390+ step "Upload multipart file."
391+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
291392
292− step "Retrieve and verify file size"
293− destFile <- mkRandFile 0
294− fGetObject bucket obj destFile defaultGetObjectOptions
295− gotSize <- withNewHandle destFile getFileSize
296− liftIO $ gotSize == Right (Just mb70) @?
297− "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"
298401
299− step "Cleanup actions"
300− deleteObject bucket obj
402+ step "Cleanup actions"
403+ deleteObject bucket obj
301404
302405highLevelListingTest :: TestTree
303406highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
304407 \step bucket -> do
305− step "High-level listObjects Test"
306− step "put 3 objects"
307− let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
308− extractObjectsFromList os =
309− mapM (\t -> case t of
310− ListItemObject o -> Just $ oiObject o
311− _ -> Nothing) os
312− expectedNonRecList = ["o4", "dir/"]
313− extractObjectsAndDirsFromList os =
314− map (\t -> case t of
315− ListItemObject o -> oiObject o
316− ListItemPrefix d -> d) os
317−
318− forM_ expectedObjects $
319− \obj -> fPutObject bucket obj "/etc/lsb-release" defaultPutObjectOptions
320−
321− step "High-level listing of objects"
322− items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
323− 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 $
324433 extractObjectsAndDirsFromList items
325434
326− step "High-level recursive listing of objects"
327− 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
328437
329− liftIO $ assertEqual "Objects match failed!"
330− (Just $ sort expectedObjects) $
331− extractObjectsFromList objects
438+ liftIO
439+ $ assertEqual
440+ "Objects match failed!"
441+ (Just $ sort expectedObjects)
442+ $ extractObjectsFromList objects
332443
333− step "High-level listing of objects (version 1)"
334− itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
335− 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 $
336448 extractObjectsAndDirsFromList itemsV1
337449
338− step "High-level recursive listing of objects (version 1)"
339− objectsV1 <- C.runConduit $ listObjectsV1 bucket Nothing True C..|
340− sinkList
341−
342− liftIO $ assertEqual "Objects match failed!"
343− (Just $ sort expectedObjects) $
344− extractObjectsFromList objectsV1
345−
346− let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
347− expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
348− step "High-level listing with prefix"
349− prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
350− liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
351− expectedPrefListing $ extractObjectsAndDirsFromList prefItems
352−
353− step "High-level listing with prefix recursive"
354− prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
355− liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
356− expectedPrefListingRec $ extractObjectsFromList prefItemsRec
357−
358− step "High-level listing with prefix (version 1)"
359− prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
360− liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
361− expectedPrefListing $ extractObjectsAndDirsFromList prefItemsV1
362−
363− step "High-level listing with prefix recursive (version 1)"
364− prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
365− liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
366− expectedPrefListingRec $ extractObjectsFromList prefItemsRecV1
367−
368− step "Cleanup actions"
369− forM_ expectedObjects $
370− \obj -> removeObject bucket obj
371−
372− step "High-level listIncompleteUploads Test"
373− let object = "newmpupload"
374− step "create 10 multipart uploads"
375− forM_ [1..10::Int] $ \_ -> do
376− uid <- newMultipartUpload bucket object []
377− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
378−
379− step "High-level listing of incomplete multipart uploads"
380− uploads <- C.runConduit $
381− listIncompleteUploads bucket (Just "newmpupload") True C..|
382− sinkList
383− liftIO $ length uploads @?= 10
384−
385− step "cleanup"
386− forM_ uploads $ \(UploadInfo _ uid _ _) ->
387− abortMultipartUpload bucket object uid
388−
389− step "High-level listIncompleteParts Test"
390− let mb5 = 5 * 1024 * 1024
391−
392− 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
393504 uid <- newMultipartUpload bucket object []
394505 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
395506
396− step "put object parts 1..10"
397− inputFile <- mkRandFile mb5
398− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
399− 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 ->
400529 putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
401530
402− step "fetch list parts"
403− incompleteParts <- C.runConduit $ listIncompleteParts bucket object uid
404− C..| sinkList
405− 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
406537
407− step "cleanup"
408− abortMultipartUpload bucket object uid
538+ step "cleanup"
539+ abortMultipartUpload bucket object uid
409540
410541listingTest :: TestTree
411542listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
412− step "listObjects' test"
413− step "put 10 objects"
414− let objects = (\s ->T.concat ["lsb-release", T.pack (show s)]) <$> [1..10::Int]
415−
416− forM_ [1..10::Int] $ \s ->
417− fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release" defaultPutObjectOptions
418−
419− step "Simple list"
420− res <- listObjects' bucket Nothing Nothing Nothing Nothing
421− let expectedObjects = sort objects
422− liftIO $ assertEqual "Objects match failed!" expectedObjects
423− (map oiObject $ lorObjects res)
424−
425− step "Simple list version 1"
426− resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
427− let expected = sort $ map (T.concat .
428− ("lsb-release":) .
429− (\x -> [x]) .
430− T.pack .
431− show) [1..10::Int]
432− liftIO $ assertEqual "Objects match failed!" expected
433− (map oiObject $ lorObjects' resV1)
434−
435− step "Cleanup actions"
436− forM_ objects $ \obj -> deleteObject bucket obj
437−
438− step "listIncompleteUploads' test"
439− step "create 10 multipart uploads"
440− let object = "newmpupload"
441− forM_ [1..10::Int] $ \_ -> do
442− uid <- newMultipartUpload bucket object []
443− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
444−
445− step "list incomplete multipart uploads"
446− incompleteUploads <- listIncompleteUploads' bucket (Just "newmpupload") Nothing
447− Nothing Nothing Nothing
448− liftIO $ (length $ lurUploads incompleteUploads) @?= 10
449−
450− step "cleanup"
451− forM_ (lurUploads incompleteUploads) $
452− \(_, uid, _) -> abortMultipartUpload bucket object uid
453−
454− step "Basic listIncompleteParts Test"
455− let mb5 = 5 * 1024 * 1024
456−
457− step "create a multipart upload"
458− uid <- newMultipartUpload bucket object []
459− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
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."
460587
461− step "put object parts 1..10"
462− inputFile <- mkRandFile mb5
463− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
464− forM_ [1..10] $ \pnum ->
465− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
466−
467− step "fetch list parts"
468− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
469− liftIO $ (length $ lprParts listPartsResult) @?= 10
470− 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
471620
472621presignedUrlFunTest :: TestTree
473622presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -479,29 +628,47 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
479628 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
480629
481630 step "PUT object presigned URL - makePresignedUrl"
482− putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
483− (Just obj) (Just "us-east-1") [] []
484−
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
485642 let size1 = 1000 :: Int64
486643 inputFile <- mkRandFile size1
487644
488645 -- attempt to upload using the presigned URL
489646 putResp <- putR size1 inputFile mgr putUrl
490− liftIO $ (NC.responseStatus putResp == HT.status200) @?
491− "presigned PUT failed"
647+ liftIO $
648+ (NC.responseStatus putResp == HT.status200)
649+ @? "presigned PUT failed"
492650
493651 step "GET object presigned URL - makePresignedUrl"
494− getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
495− (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+ []
496661
497662 getResp <- getR mgr getUrl
498− liftIO $ (NC.responseStatus getResp == HT.status200) @?
499− "presigned GET failed"
663+ liftIO $
664+ (NC.responseStatus getResp == HT.status200)
665+ @? "presigned GET failed"
500666
501667 -- read content from file to compare with response above
502668 bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
503− liftIO $ (bs == NC.responseBody getResp) @?
504− "presigned put and get got mismatched data"
669+ liftIO $
670+ (bs == NC.responseBody getResp)
671+ @? "presigned put and get got mismatched data"
505672
506673 step "PUT object presigned - presignedPutObjectURL"
507674 putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
@@ -510,66 +677,71 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
510677 testFile <- mkRandFile size2
511678
512679 putResp2 <- putR size2 testFile mgr putUrl2
513− liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
514− "presigned PUT failed (presignedPutObjectUrl)"
680+ liftIO $
681+ (NC.responseStatus putResp2 == HT.status200)
682+ @? "presigned PUT failed (presignedPutObjectUrl)"
515683
516684 step "HEAD object presigned URL - presignedHeadObjectUrl"
517685 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
518686
519− headResp <- do let req = NC.parseRequest_ $ toS headUrl
520− NC.httpLbs (req {NC.method = HT.methodHead}) mgr
521− liftIO $ (NC.responseStatus headResp == HT.status200) @?
522− "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)"
523693
524694 -- check that header info is accurate
525695 let h = H.fromList $ NC.responseHeaders headResp
526696 cLen = H.lookupDefault "0" HT.hContentLength h
527− liftIO $ (cLen == show size2) @? "Head req returned bad content length"
697+ liftIO $ (cLen == showBS size2) @? "Head req returned bad content length"
528698
529699 step "GET object presigned URL - presignedGetObjectUrl"
530700 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
531701
532702 getResp2 <- getR mgr getUrl2
533− liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
534− "presigned GET failed (presignedGetObjectUrl)"
703+ liftIO $
704+ (NC.responseStatus getResp2 == HT.status200)
705+ @? "presigned GET failed (presignedGetObjectUrl)"
535706
536707 -- read content from file to compare with response above
537708 bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
538− liftIO $ (bs2 == NC.responseBody getResp2) @?
539− "presigned put and get got mismatched data (presigned*Url)"
540−
709+ liftIO $
710+ (bs2 == NC.responseBody getResp2)
711+ @? "presigned put and get got mismatched data (presigned*Url)"
541712
542713 mapM_ (removeObject bucket) [obj, obj2]
543714 where
544715 putR size filePath mgr url = do
545− let req = NC.parseRequest_ $ toS url
546− let req' = req { NC.method = HT.methodPut
547− , NC.requestBody = NC.requestBodySource size $
548− 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+ }
549724 NC.httpLbs req' mgr
550−
551725 getR mgr url = do
552− let req = NC.parseRequest_ $ toS url
726+ let req = NC.parseRequest_ $ decodeUtf8 url
553727 NC.httpLbs req mgr
554728
555729presignedPostPolicyFunTest :: TestTree
556−presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
730+presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
557731 \step bucket -> do
558−
559732 step "presignedPostPolicy basic test"
560− now <- liftIO $ Time.getCurrentTime
733+ now <- liftIO Time.getCurrentTime
561734
562735 let key = "presignedPostPolicyTest/myfile"
563− policyConds = [ ppCondBucket bucket
564− , ppCondKey key
565− , ppCondContentLengthRange 1 1000
566− , ppCondContentType "application/octet-stream"
567− , ppCondSuccessActionStatus 200
568− ]
569−
736+ policyConds =
737+ [ ppCondBucket bucket,
738+ ppCondKey key,
739+ ppCondContentLengthRange 1 1000,
740+ ppCondContentType "application/octet-stream",
741+ ppCondSuccessActionStatus 200
742+ ]
570743 expirationTime = Time.addUTCTime 3600 now
571744 postPolicyE = newPostPolicy expirationTime policyConds
572−
573745 size = 1000 :: Int64
574746
575747 inputFile <- mkRandFile size
@@ -580,43 +752,43 @@ presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
580752 (url, formData) <- presignedPostPolicy postPolicy
581753 -- liftIO (print url) >> liftIO (print formData)
582754 result <- liftIO $ postForm url formData inputFile
583− liftIO $ (NC.responseStatus result == HT.status200) @?
584− "presigned POST failed"
755+ liftIO $
756+ (NC.responseStatus result == HT.status200)
757+ @? "presigned POST failed"
585758
586759 mapM_ (removeObject bucket) [key]
587− where
588−
589− postForm url formData inputFile = do
590− req <- NC.parseRequest $ toS url
591− let parts = map (\(x, y) -> Form.partBS x y) $
592− H.toList formData
593− parts' = parts ++ [Form.partFile "file" inputFile]
594− req' <- Form.formDataBody parts' req
595− mgr <- NC.newManager NC.tlsManagerSettings
596− 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
597770
598771bucketPolicyFunTest :: TestTree
599772bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
600773 \step bucket -> do
601−
602774 step "bucketPolicy basic test - no policy exception"
603775 resE <- try $ getBucketPolicy bucket
604776 case resE of
605777 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
606− _ -> return ()
778+ _ -> return ()
607779
608780 resE' <- try $ setBucketPolicy bucket T.empty
609781 case resE' of
610782 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
611− _ -> return ()
783+ _ -> return ()
612784
613785 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/*\"]}]}"
614786
615787 step "try a malformed policy, expect error"
616788 resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
617789 case resE'' of
618− Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "Policy has invalid resource."
619− _ -> return ()
790+ Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
791+ _ -> return ()
620792
621793 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 <> "/*\"]}]}"
622794
@@ -631,13 +803,22 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
631803 step "verify bucket policy: (2) get `myobject` anonymously"
632804 connInfo <- asks mcConnInfo
633805 let proto = bool "http://" "https://" $ connectIsSecure connInfo
634− url = BS.concat [proto, getHostAddr connInfo, "/", toS bucket,
635− "/", toS obj]
636− respE <- liftIO $ (fmap (Right . toS) $ NC.simpleHttp $ toS url) `catch`
637− (\(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))
638819 case respE of
639− Left err -> liftIO $ assertFailure $ show err
640− 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")
641822
642823 deleteObject bucket obj
643824
@@ -647,25 +828,28 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
647828multipartTest :: TestTree
648829multipartTest = funTestWithBucket "Multipart Tests" $
649830 \step bucket -> do
650− step "Prepare for putObjectInternal with non-seekable file, with size."
651− step "Upload multipart file."
652− let mb80 = 80 * 1024 * 1024
653− 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"
654837
655− void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
838+ -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
656839
657− step "Retrieve and verify file size"
658− destFile <- mkRandFile 0
659− fGetObject bucket obj destFile defaultGetObjectOptions
660− gotSize <- withNewHandle destFile getFileSize
661− liftIO $ gotSize == Right (Just mb80) @?
662− "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"
663847
664− step "Cleanup actions"
665− removeObject bucket obj
848+ -- step "Cleanup actions"
849+ -- removeObject bucket obj
666850
667− step "cleanup"
668− removeObject bucket "big"
851+ -- step "cleanup"
852+ -- removeObject bucket "big"
669853
670854 step "Prepare for removeIncompleteUpload"
671855 -- low-level multipart operation tests.
@@ -679,15 +863,17 @@ multipartTest = funTestWithBucket "Multipart Tests" $
679863 randFile <- mkRandFile kb5
680864
681865 step "upload 2 parts"
682− forM_ [1,2] $ \partNum -> do
683− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
684− 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
685869
686870 step "remove ongoing upload"
687871 removeIncompleteUpload bucket object
688− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
689− C..| sinkList
690− 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"
691877
692878putObjectContentTypeTest :: TestTree
693879putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
@@ -698,9 +884,13 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
698884
699885 step "create server object with content-type"
700886 inputFile <- mkRandFile size1
701− fPutObject bucket object inputFile defaultPutObjectOptions {
702− pooContentType = Just "application/javascript"
703− }
887+ fPutObject
888+ bucket
889+ object
890+ inputFile
891+ defaultPutObjectOptions
892+ { pooContentType = Just "application/javascript"
893+ }
704894
705895 -- retrieve obj info to check
706896 oi <- headObject bucket object []
@@ -710,16 +900,23 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
710900 liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
711901
712902 step "upload object with content-encoding set to identity"
713− fPutObject bucket object inputFile defaultPutObjectOptions {
714− pooContentEncoding = Just "identity"
715− }
903+ fPutObject
904+ bucket
905+ object
906+ inputFile
907+ defaultPutObjectOptions
908+ { pooContentEncoding = Just "identity"
909+ }
716910
717911 oiCE <- headObject bucket object []
718912 let m' = oiMetadata oiCE
719913
720914 step "Validate content-encoding"
721− liftIO $ assertEqual "Content-Encoding did not match" (Just "identity")
722− (H.lookup "Content-Encoding" m')
915+ liftIO $
916+ assertEqual
917+ "Content-Encoding did not match"
918+ (Just "identity")
919+ (H.lookup "Content-Encoding" m')
723920
724921 step "Cleanup actions"
725922
@@ -734,17 +931,24 @@ putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage test
734931
735932 step "create server object with content-language"
736933 inputFile <- mkRandFile size1
737− fPutObject bucket object inputFile defaultPutObjectOptions {
738− pooContentLanguage = Just "en-US"
739− }
934+ fPutObject
935+ bucket
936+ object
937+ inputFile
938+ defaultPutObjectOptions
939+ { pooContentLanguage = Just "en-US"
940+ }
740941
741942 -- retrieve obj info to check
742943 oi <- headObject bucket object []
743944 let m = oiMetadata oi
744945
745946 step "Validate content-language"
746− liftIO $ assertEqual "content-language did not match" (Just "en-US")
747− (H.lookup "Content-Language" m)
947+ liftIO $
948+ assertEqual
949+ "content-language did not match"
950+ (Just "en-US")
951+ (H.lookup "Content-Language" m)
748952 step "Cleanup actions"
749953
750954 removeObject bucket object
@@ -759,19 +963,26 @@ putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
759963 step "create server object with usermetdata"
760964 inputFile <- mkRandFile size1
761965
762− fPutObject bucket object inputFile defaultPutObjectOptions {
763− pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")
764− , ("mykey2", "myval2")
765− ]
766− }
966+ fPutObject
967+ bucket
968+ object
969+ inputFile
970+ defaultPutObjectOptions
971+ { pooUserMetadata =
972+ [ ("x-Amz-meta-mykey1", "myval1"),
973+ ("mykey2", "myval2")
974+ ]
975+ }
767976
768977 step "Validate user-metadata"
769978 -- retrieve obj info to check
770979 oi <- headObject bucket object []
771980 let m = oiUserMetadata oi
772981 -- need to do a case-insensitive comparison
773− sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
774− H.toList m
982+ sortedMeta =
983+ sort $
984+ map (bimap T.toLower T.toLower) $
985+ H.toList m
775986 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
776987
777988 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -787,19 +998,26 @@ getObjectTest = funTestWithBucket "getObject test" $
787998 size1 = 100 :: Int64
788999
7891000 inputFile <- mkRandFile size1
790− fPutObject bucket object inputFile defaultPutObjectOptions {
791− pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")
792− , ("mykey2", "myval2")
793− ]
794− }
1001+ fPutObject
1002+ bucket
1003+ object
1004+ inputFile
1005+ defaultPutObjectOptions
1006+ { pooUserMetadata =
1007+ [ ("x-Amz-meta-mykey1", "myval1"),
1008+ ("mykey2", "myval2")
1009+ ]
1010+ }
7951011
7961012 step "get the object - check the metadata matches"
7971013 -- retrieve obj info to check
7981014 gor <- getObject bucket object defaultGetObjectOptions
7991015 let m = oiUserMetadata $ gorObjectInfo gor
8001016 -- need to do a case-insensitive comparison
801− sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
802− H.toList m
1017+ sortedMeta =
1018+ sort $
1019+ map (bimap T.toLower T.toLower) $
1020+ H.toList m
8031021 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
8041022
8051023 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -827,13 +1045,21 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
8271045 inputFile' <- mkRandFile size1
8281046 inputFile'' <- mkRandFile size0
8291047
830− fPutObject bucket object inputFile defaultPutObjectOptions {
831− pooStorageClass = Just "STANDARD"
832− }
833−
834− fPutObject bucket object' inputFile' defaultPutObjectOptions {
835− pooStorageClass = Just "REDUCED_REDUNDANCY"
836− }
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+ }
8371063
8381064 removeObject bucket object
8391065
@@ -842,15 +1068,24 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
8421068 let m' = oiMetadata oi'
8431069
8441070 step "Validate x-amz-storage-class rrs"
845− liftIO $ assertEqual "storageClass did not match" (Just "REDUCED_REDUNDANCY")
846− (H.lookup "X-Amz-Storage-Class" m')
847−
848− fpE <- try $ fPutObject bucket object'' inputFile'' defaultPutObjectOptions {
849− pooStorageClass = Just "INVALID_STORAGE_CLASS"
850− }
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+ }
8511086 case fpE of
8521087 Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
853− _ -> return ()
1088+ _ -> return ()
8541089
8551090 step "Cleanup actions"
8561091
@@ -869,7 +1104,7 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
8691104 fPutObject bucket object inputFile defaultPutObjectOptions
8701105
8711106 step "copy object"
872− let srcInfo = defaultSourceInfo { srcBucket = bucket, srcObject = object}
1107+ let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
8731108 (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
8741109
8751110 -- retrieve obj info to check
@@ -880,8 +1115,9 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
8801115
8811116 let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
8821117
883− liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
884− "Copied object did not match expected."
1118+ liftIO $
1119+ (s == size1 && e == etag && isMTimeDiffOk)
1120+ @? "Copied object did not match expected."
8851121
8861122 step "cleanup actions"
8871123 removeObject bucket object
@@ -902,12 +1138,18 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
9021138 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
9031139
9041140 step "put object parts 1-3"
905− let srcInfo' = defaultSourceInfo { srcBucket = bucket, srcObject = srcObj }
906− dstInfo' = defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj }
907− parts <- forM [1..3] $ \p -> do
908− (etag', _) <- copyObjectPart dstInfo' srcInfo'{
909− srcRange = Just $ (,) ((p-1)*mb5) ((p-1)*mb5 + (mb5 - 1))
910− } 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+ []
9111153 return (fromIntegral p, etag')
9121154
9131155 step "complete multipart"
@@ -953,11 +1195,13 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
9531195 fPutObject bucket src inputFile' defaultPutObjectOptions
9541196
9551197 step "copy last 10MiB of object"
956− copyObject defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj } defaultSourceInfo {
957− srcBucket = bucket
958− , srcObject = src
959− , srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
960− }
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+ }
9611205
9621206 step "verify uploaded object"
9631207 cSize <- oiSize <$> headObject bucket copyObj []
@@ -968,42 +1212,72 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
9681212
9691213getNPutSSECTest :: TestTree
9701214getNPutSSECTest =
971− funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
972− -- Skip this test if the server is not using TLS as encryption is
973− -- disabled anyway.
974− isTLSConn <- asks (connectIsSecure . mcConnInfo)
975− if isTLSConn
976− then do step "Make an encryption key"
977− key <- case mkSSECKey $ BS.pack [0..31] of
978− Nothing -> liftIO $ assertFailure "This should not happen"
979− Just k -> return k
980−
981− let mb1 = 1024*1024
982− obj = "1"
983− step "Upload an object using the encryption key"
984− rFile <- mkRandFile mb1
985− let putOpts = defaultPutObjectOptions { pooSSE = Just $ SSEC key }
986− fPutObject bucket obj rFile putOpts
987−
988− step "Stat object without key - should fail"
989− headRes <- try $ statObject bucket obj defaultGetObjectOptions
990− case headRes of
991− Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
992− Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
993− | NC.responseStatus rsp == HT.status400 -> return ()
994− | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
995− Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
996−
997− step "Get file and check length"
998− dstFile <- mkRandFile 0
999− let getOpts = defaultGetObjectOptions { gooSSECKey = Just key }
1000− fGetObject bucket obj dstFile getOpts
1001−
1002− gotSize <- withNewHandle dstFile getFileSize
1003− liftIO $ gotSize == Right (Just mb1) @?
1004− "Wrong file size of object when getting"
1005−
1006− step "Cleanup"
1007− deleteObject bucket obj
1008−
1009− 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.HashMap.Strict as H
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 H.empty H.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 H.empty H.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-----