git.iko.soy/typeable/minio-hs
on master help sign in
48 files changed+1802−834
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml+227−154 CHANGELOG.md+24 README.md+50−29 Setup.hs−19 examples/AssumeRole.hs+47 examples/FileUploader.hs+1−1 examples/GetConfig.hs+2−2 examples/GetObject.hs+1−1 examples/Heal.hs−1 examples/ListIncompleteUploads.hs+1−1 examples/ListObjects.hs+1−1 examples/PresignedGetObject.hs+3−2 examples/PresignedPostPolicy.hs+5−4 examples/PresignedPutObject.hs+2−1 examples/SelectObject.hs+2−2 examples/ServerInfo.hs+2−2 examples/ServiceSendRestart.hs−1 examples/ServiceSendStop.hs−1 examples/ServiceStatus.hs+2−2 minio-hs.cabal+43−19 src/Lib/Prelude.hs+1−1 src/Network/Minio.hs+21−7 src/Network/Minio/API.hs+54−31 src/Network/Minio/AdminAPI.hs+138−73 src/Network/Minio/CopyObject.hs+2−2 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+135−104 src/Network/Minio/Data/Crypto.hs+8−8 src/Network/Minio/Data/Time.hs+2−1 src/Network/Minio/Errors.hs+2−1 src/Network/Minio/ListOps.hs+7−4 src/Network/Minio/PresignedOperations.hs+26−17 src/Network/Minio/S3API.hs+22−14 src/Network/Minio/SelectAPI.hs+7−7 src/Network/Minio/Sign/V4.hs+208−123 src/Network/Minio/Utils.hs+30−48 src/Network/Minio/XmlCommon.hs+65 src/Network/Minio/XmlGenerator.hs+6−4 src/Network/Minio/XmlParser.hs+15−54 stack.yaml+4−4 stack.yaml.lock+12−5 test/LiveServer.hs+165−67 test/Network/Minio/JsonParser/Test.hs+1−1 test/Network/Minio/XmlGenerator/Test.hs+12−4 test/Network/Minio/XmlParser/Test.hs+1−1 test/Spec.hs+12−10
.github/workflows/ci.yml+227−154
@@ -18,176 +18,249 @@ env:
1818 MINIO_ACCESS_KEY: minio
1919 MINIO_SECRET_KEY: minio123
2020 MINIO_LOCAL: 1
21− MINIO_SECURE: 1
2221
2322jobs:
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+
2447 cabal:
2548 name: ${{ matrix.os }} / ghc-${{ matrix.ghc }} / cabal-${{ matrix.cabal }}
2649 runs-on: ${{ matrix.os }}
50+ needs: ormolu
2751 strategy:
2852 matrix:
29− os: [ubuntu-latest, windows-latest] # Removed macos-latest due to cert issues.
30− cabal: ["3.6"]
53+ os: [ubuntu-latest, windows-latest, macos-latest]
54+ cabal: ["3.8", "latest"]
3155 ghc:
32− - "9.0.2"
33− - "8.10.7"
34− - "8.8.4"
35− - "8.6.5"
36− # exclude:
37− # - os: macOS-latest
38− # ghc: 8.8.4
39− # - os: macOS-latest
40− # ghc: 8.6.5
41− # - os: windows-latest
42− # ghc: 8.10.7
43− # - os: windows-latest
44− # ghc: 8.6.5
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"
4573
4674 steps:
47− - uses: actions/checkout@v2
48− if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
49−
50− - uses: haskell/actions/setup@v1
51− id: setup-haskell-cabal
52− name: Setup Haskell
53− with:
54− ghc-version: ${{ matrix.ghc }}
55− cabal-version: ${{ matrix.cabal }}
56−
57− - name: Configure
58− run: |
59− cabal configure --enable-tests --enable-benchmarks --test-show-details=direct -fexamples -flive-test
60−
61− - name: Freeze
62− run: |
63− cabal freeze
64−
65− - uses: actions/cache@v2.1.3
66− name: Cache ~/.cabal/store
67− with:
68− path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }}
69− key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
70−
71− - name: Install dependencies
72− run: |
73− cabal build all --only-dependencies -fexamples -flive-test
74−
75− - name: Build
76− run: |
77− cabal build all -fexamples
78−
79− - name: Setup MinIO for testing (Linux)
80− if: matrix.os == 'ubuntu-latest'
81− run: |
82− mkdir -p /tmp/minio /tmp/minio-config/certs
83− cp test/cert/* /tmp/minio-config/certs/
84− (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
85− sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
86− sudo update-ca-certificates
87−
88− - name: Setup MinIO for testing (MacOS)
89− if: matrix.os == 'macos-latest'
90− run: |
91− mkdir -p /tmp/minio /tmp/minio-config/certs
92− cp test/cert/* /tmp/minio-config/certs/
93− (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
94− sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
95−
96− - name: Setup MinIO for testing (Windows)
97− if: matrix.os == 'windows-latest'
98− run: |
99− New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
100− Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
101− Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
102− Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
103−
104− - name: Test (Non-Windows)
105− if: matrix.os != 'windows-latest'
106− run: |
107− /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
108− ghc --version
109− cabal --version
110− cabal test all -flive-test
111−
112− - name: Test (Windows)
113− if: matrix.os == 'windows-latest'
114− run: |
115− 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"
116− ghc --version
117− cabal --version
118− cabal test all -flive-test
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
119176
120177 stack:
121178 name: stack / ghc ${{ matrix.ghc }}
122179 runs-on: ${{ matrix.os }}
180+ env:
181+ MINIO_SECURE: 1
123182 strategy:
124183 matrix:
125− stack: ["2.7.3"]
126− ghc: ["8.10.7"]
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"
127191 os: [ubuntu-latest]
128192
129193 steps:
130− - uses: actions/checkout@v2
131− if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
132−
133− - uses: haskell/actions/setup@v1
134− name: Setup Haskell Stack
135− with:
136− enable-stack: true
137− ghc-version: ${{ matrix.ghc }}
138− stack-version: ${{ matrix.stack }}
139−
140− - uses: actions/cache@v2.1.3
141− name: Cache ~/.stack
142− with:
143− path: ~/.stack
144− key: ${{ runner.os }}-${{ matrix.ghc }}-stack
145−
146− - name: Install dependencies
147− run: |
148− stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
149−
150− - name: Build
151− run: |
152− stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --flag minio-hs:examples
153−
154− - name: Setup MinIO for testing (Linux)
155− if: matrix.os == 'ubuntu-latest'
156− run: |
157− mkdir -p /tmp/minio /tmp/minio-config/certs
158− cp test/cert/* /tmp/minio-config/certs/
159− (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
160− sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
161− sudo update-ca-certificates
162−
163− - name: Setup MinIO for testing (MacOS)
164− if: matrix.os == 'macos-latest'
165− run: |
166− mkdir -p /tmp/minio /tmp/minio-config/certs
167− cp test/cert/* /tmp/minio-config/certs/
168− (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
169− sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
170−
171− - name: Setup MinIO for testing (Windows)
172− if: matrix.os == 'windows-latest'
173− run: |
174− New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
175− Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
176− Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
177− Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
178−
179− - name: Test (Non-Windows)
180− if: matrix.os != 'windows-latest'
181− run: |
182− /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
183− ghc --version
184− stack --version
185− stack test --system-ghc --flag minio-hs:live-test
186−
187− - name: Test (Windows)
188− if: matrix.os == 'windows-latest'
189− run: |
190− 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"
191− ghc --version
192− cabal --version
193− stack test --system-ghc --flag minio-hs:live-test
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
CHANGELOG.md+24
@@ -1,6 +1,30 @@
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+
428## Version 1.5.3
529
630* Fix windows build
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−19
@@ -1,19 +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−
19−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/FileUploader.hs+1−1
@@ -70,5 +70,5 @@ main = do
7070 fPutObject bucket object filepath defaultPutObjectOptions
7171
7272 case res of
73− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
73+ Left e -> putStrLn $ "file upload failed due to " ++ show e
7474 Right () -> putStrLn "file upload succeeded."
examples/GetConfig.hs+2−2
@@ -16,7 +16,6 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−{-# LANGUAGE OverloadedStrings #-}
2019
2120import Network.Minio
2221import Network.Minio.AdminAPI
@@ -25,6 +24,7 @@ import Prelude
2524main :: IO ()
2625main = do
2726 res <-
28− runMinio minioPlayCI $
27+ runMinio
28+ minioPlayCI
2929 getConfig
3030 print res
examples/GetObject.hs+1−1
@@ -37,5 +37,5 @@ main = do
3737 C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
3838
3939 case res of
40− Left e -> putStrLn $ "getObject failed." ++ (show e)
40+ Left e -> putStrLn $ "getObject failed." ++ show e
4141 Right _ -> putStrLn "getObject succeeded."
examples/Heal.hs−1
@@ -16,7 +16,6 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−{-# LANGUAGE OverloadedStrings #-}
2019
2120import Network.Minio
2221import Network.Minio.AdminAPI
examples/ListIncompleteUploads.hs+1−1
@@ -36,7 +36,7 @@ main = do
3636 res <-
3737 runMinio minioPlayCI $
3838 runConduit $
39− listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
39+ listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
4040 print res
4141
4242{-
examples/ListObjects.hs+1−1
@@ -36,7 +36,7 @@ main = do
3636 res <-
3737 runMinio minioPlayCI $
3838 runConduit $
39− listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
39+ listObjects bucket Nothing True .| mapM_C (liftIO . print)
4040 print res
4141
4242{-
examples/PresignedGetObject.hs+3−2
@@ -46,7 +46,7 @@ main = do
4646 res <- runMinio minioPlayCI $ do
4747 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
4848 putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
49− liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
49+ liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
5050
5151 -- Extract Etag of uploaded object
5252 oi <- statObject bucket object defaultGetObjectOptions
@@ -77,7 +77,8 @@ main = do
7777 let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
7878 curlCmd =
7979 B.intercalate " " $
80− ["curl --fail"] ++ map hdrOpt headers
80+ ["curl --fail"]
81+ ++ map hdrOpt headers
8182 ++ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
8283
8384 putStrLn $
examples/PresignedPostPolicy.hs+5−4
@@ -55,7 +55,7 @@ main = do
5555 ]
5656
5757 case policyE of
58− Left err -> putStrLn $ show err
58+ Left err -> print err
5959 Right policy -> do
6060 res <- runMinio minioPlayCI $ do
6161 (url, formData) <- presignedPostPolicy policy
@@ -74,13 +74,14 @@ main = do
7474 formOptions = B.intercalate " " $ map formFn $ H.toList formData
7575
7676 return $
77− B.intercalate " " $
77+ B.intercalate
78+ " "
7879 ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
7980
8081 case res of
81− Left e -> putStrLn $ "post-policy error: " ++ (show e)
82+ Left e -> putStrLn $ "post-policy error: " ++ show e
8283 Right cmd -> do
83− 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"
8485
8586 -- print the generated curl command
8687 Char8.putStrLn cmd
examples/PresignedPutObject.hs+2−1
@@ -48,7 +48,8 @@ main = do
4848 let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
4949 curlCmd =
5050 B.intercalate " " $
51− ["curl "] ++ map hdrOpt headers
51+ ["curl "]
52+ ++ map hdrOpt headers
5253 ++ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
5354
5455 putStrLn $
examples/SelectObject.hs+2−2
@@ -19,7 +19,7 @@
1919{-# LANGUAGE OverloadedStrings #-}
2020
2121import qualified Conduit as C
22−import Control.Monad (when)
22+import Control.Monad (unless)
2323import Network.Minio
2424import Prelude
2525
@@ -35,7 +35,7 @@ main = do
3535
3636 res <- runMinio minioPlayCI $ do
3737 exists <- bucketExists bucket
38− when (not exists) $
38+ unless exists $
3939 makeBucket bucket Nothing
4040
4141 C.liftIO $ putStrLn "Uploading csv object"
examples/ServerInfo.hs+2−2
@@ -16,7 +16,6 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−{-# LANGUAGE OverloadedStrings #-}
2019
2120import Network.Minio
2221import Network.Minio.AdminAPI
@@ -25,6 +24,7 @@ import Prelude
2524main :: IO ()
2625main = do
2726 res <-
28− runMinio minioPlayCI $
27+ runMinio
28+ minioPlayCI
2929 getServerInfo
3030 print res
examples/ServiceSendRestart.hs−1
@@ -16,7 +16,6 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−{-# LANGUAGE OverloadedStrings #-}
2019
2120import Network.Minio
2221import Network.Minio.AdminAPI
examples/ServiceSendStop.hs−1
@@ -16,7 +16,6 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−{-# LANGUAGE OverloadedStrings #-}
2019
2120import Network.Minio
2221import Network.Minio.AdminAPI
examples/ServiceStatus.hs+2−2
@@ -16,7 +16,6 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−{-# LANGUAGE OverloadedStrings #-}
2019
2120import Network.Minio
2221import Network.Minio.AdminAPI
@@ -25,6 +24,7 @@ import Prelude
2524main :: IO ()
2625main = do
2726 res <-
28− runMinio minioPlayCI $
27+ runMinio
28+ minioPlayCI
2929 serviceStatus
3030 print res
minio-hs.cabal+43−19
@@ -1,6 +1,6 @@
1−cabal-version: 2.2
1+cabal-version: 2.4
22name: minio-hs
3−version: 1.5.3
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,21 +14,29 @@ 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
24−tested-with: GHC == 8.8.4
25− , GHC == 8.10.7
25+tested-with: GHC == 8.10.7
2626 , GHC == 9.0.2
27+ , GHC == 9.2.8
28+ , GHC == 9.4.8
29+ , GHC == 9.6.5
30+ , GHC == 9.8.2
2731
2832source-repository head
2933 type: git
3034 location: https://github.com/minio/minio-hs.git
3135
36+Flag dev
37+ Description: Build package in development mode
38+ Default: False
39+ Manual: True
3240
3341common base-settings
3442 ghc-options: -Wall
@@ -41,16 +49,20 @@ common base-settings
4149 ghc-options: -Wredundant-constraints
4250 if impl(ghc >= 8.2)
4351 ghc-options: -fhide-source-paths
44−
45− -- Add this when we have time. Fixing partial-fields requires major version
46− -- bump at this time.
47− -- if impl(ghc >= 8.4)
48− -- ghc-options: -Wpartial-fields
49− -- -Wmissing-export-lists
50−
52+ if impl(ghc >= 8.4)
53+ ghc-options: -Wpartial-fields
54+ -- -Wmissing-export-lists
5155 if impl(ghc >= 8.8)
5256 ghc-options: -Wmissing-deriving-strategies
5357 -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
5466
5567 default-language: Haskell2010
5668
@@ -58,14 +70,13 @@ common base-settings
5870 , DerivingStrategies
5971 , FlexibleContexts
6072 , FlexibleInstances
73+ , LambdaCase
6174 , MultiParamTypeClasses
6275 , MultiWayIf
6376 , OverloadedStrings
6477 , RankNTypes
6578 , ScopedTypeVariables
6679 , TupleSections
67− , TypeFamilies
68−
6980
7081 other-modules: Lib.Prelude
7182 , Network.Minio.API
@@ -84,7 +95,11 @@ common base-settings
8495 , Network.Minio.Utils
8596 , Network.Minio.XmlGenerator
8697 , Network.Minio.XmlParser
98+ , Network.Minio.XmlCommon
8799 , Network.Minio.JsonParser
100+ , Network.Minio.Credentials.Types
101+ , Network.Minio.Credentials.AssumeRole
102+ , Network.Minio.Credentials
88103
89104 mixins: base hiding (Prelude)
90105 , relude (Relude as Prelude)
@@ -99,12 +114,12 @@ common base-settings
99114 , case-insensitive >= 1.2
100115 , conduit >= 1.3
101116 , conduit-extra >= 1.3
102− , connection
117+ , crypton-connection
103118 , cryptonite >= 0.25
104119 , cryptonite-conduit >= 0.2
120+ , data-default-class
105121 , digest >= 0.0.1
106122 , directory
107− , exceptions
108123 , filepath >= 1.4
109124 , http-client >= 0.5
110125 , http-client-tls
@@ -113,11 +128,11 @@ common base-settings
113128 , ini
114129 , memory >= 0.14
115130 , network-uri
116− , raw-strings-qq >= 1
117131 , resourcet >= 1.2
118132 , retry
119133 , text >= 1.2
120− , time >= 1.8
134+ , time >= 1.9
135+ , time-units ^>= 1.0.0
121136 , transformers >= 0.5
122137 , unliftio >= 0.2 && < 0.3
123138 , unliftio-core >= 0.2 && < 0.3
@@ -151,7 +166,9 @@ test-suite minio-hs-live-server-test
151166 , Network.Minio.Utils.Test
152167 , Network.Minio.XmlGenerator.Test
153168 , Network.Minio.XmlParser.Test
169+ , Network.Minio.Credentials
154170 build-depends: minio-hs
171+ , raw-strings-qq
155172 , tasty
156173 , tasty-hunit
157174 , tasty-quickcheck
@@ -166,6 +183,7 @@ test-suite minio-hs-test
166183 hs-source-dirs: test, src
167184 main-is: Spec.hs
168185 build-depends: minio-hs
186+ , raw-strings-qq
169187 , QuickCheck
170188 , tasty
171189 , tasty-hunit
@@ -182,6 +200,7 @@ test-suite minio-hs-test
182200 , Network.Minio.Utils.Test
183201 , Network.Minio.XmlGenerator.Test
184202 , Network.Minio.XmlParser.Test
203+ , Network.Minio.Credentials
185204
186205Flag examples
187206 Description: Build the examples
@@ -327,3 +346,8 @@ executable SetConfig
327346 import: examples-settings
328347 scope: private
329348 main-is: SetConfig.hs
349+
350+executable AssumeRole
351+ import: examples-settings
352+ scope: private
353+ main-is: AssumeRole.hs
src/Lib/Prelude.hs+1−1
@@ -42,7 +42,7 @@ import UnliftIO as Exports
4242both :: (a -> b) -> (a, a) -> (b, b)
4343both f (a, b) = (f a, f b)
4444
45−showBS :: Show a => a -> ByteString
45+showBS :: (Show a) => a -> ByteString
4646showBS a = encodeUtf8 (show a :: Text)
4747
4848toStrictBS :: LByteString -> ByteString
src/Network/Minio.hs+21−7
@@ -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,7 +16,7 @@
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--
@@ -24,13 +24,17 @@
2424-- storage servers like MinIO.
2525module Network.Minio
2626 ( -- * Credentials
27− Credentials (..),
27+ CredentialValue (..),
28+ credentialValueText,
29+ AccessKey (..),
30+ SecretKey (..),
31+ SessionToken (..),
2832
29− -- ** Credential providers
33+ -- ** Credential Loaders
3034
31− -- | Run actions that retrieve 'Credentials' from the environment or
35+ -- | Run actions that retrieve 'CredentialValue's from the environment or
3236 -- files or other custom sources.
33− Provider,
37+ CredentialLoader,
3438 fromAWSConfigFile,
3539 fromAWSEnv,
3640 fromMinioEnv,
@@ -54,6 +58,15 @@ module Network.Minio
5458 awsCI,
5559 gcsCI,
5660
61+ -- ** STS Credential types
62+ STSAssumeRole (..),
63+ STSAssumeRoleOptions (..),
64+ defaultSTSAssumeRoleOptions,
65+ requestSTSCredential,
66+ setSTSCredential,
67+ ExpiryTime (..),
68+ STSCredentialProvider,
69+
5770 -- * Minio Monad
5871
5972 ----------------
@@ -225,14 +238,15 @@ This module exports the high-level MinIO API for object storage.
225238import qualified Data.Conduit as C
226239import qualified Data.Conduit.Binary as CB
227240import qualified Data.Conduit.Combinators as CC
241+import Network.Minio.API
228242import Network.Minio.CopyObject
243+import Network.Minio.Credentials
229244import Network.Minio.Data
230245import Network.Minio.Errors
231246import Network.Minio.ListOps
232247import Network.Minio.PutObject
233248import Network.Minio.S3API
234249import Network.Minio.SelectAPI
235−import Network.Minio.Utils
236250
237251-- | Lists buckets.
238252listBuckets :: Minio [BucketInfo]
src/Network/Minio/API.hs+54−31
@@ -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.
@@ -26,6 +26,7 @@ module Network.Minio.API
2626 checkBucketNameValidity,
2727 isValidObjectName,
2828 checkObjectNameValidity,
29+ requestSTSCredential,
2930 )
3031where
3132
@@ -41,12 +42,15 @@ import qualified Data.HashMap.Strict as H
4142import qualified Data.Text as T
4243import qualified Data.Time.Clock as Time
4344import Lib.Prelude
45+import Network.HTTP.Client (defaultManagerSettings)
4446import qualified Network.HTTP.Client as NClient
4547import Network.HTTP.Conduit (Response)
4648import qualified Network.HTTP.Conduit as NC
49+import Network.HTTP.Types (simpleQueryToQuery)
4750import qualified Network.HTTP.Types as HT
4851import Network.HTTP.Types.Header (hHost)
4952import Network.Minio.APICommon
53+import Network.Minio.Credentials
5054import Network.Minio.Data
5155import Network.Minio.Errors
5256import Network.Minio.Sign.V4
@@ -130,18 +134,32 @@ getHostPathRegion ri = do
130134 regionMay
131135 )
132136 virtualStyle =
133− ( ( bucket <> "." <> regionHost,
134− encodeUtf8 $ "/" <> fromMaybe "" (riObject ri),
135− regionMay
136− )
137+ ( bucket <> "." <> regionHost,
138+ encodeUtf8 $ "/" <> fromMaybe "" (riObject ri),
139+ regionMay
137140 )
138− if
139− | isAWSConnectInfo ci ->
140− return $
141− if bucketHasPeriods bucket
142− then pathStyle
143− else virtualStyle
144− | otherwise -> return pathStyle
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
145163
146164buildRequest :: S3ReqInfo -> Minio NC.Request
147165buildRequest ri = do
@@ -173,10 +191,15 @@ buildRequest ri = do
173191
174192 timeStamp <- liftIO Time.getCurrentTime
175193
194+ mgr <- asks mcConnManager
195+ cv <- liftIO $ getCredential (connectCreds ci') (getEndpoint ci') mgr
196+
176197 let sp =
177198 SignParams
178− (connectAccessKey ci')
179− (connectSecretKey ci')
199+ (coerce $ cvAccessKey cv)
200+ (coerce $ cvSecretKey cv)
201+ (coerce $ cvSessionToken cv)
202+ ServiceS3
180203 timeStamp
181204 (riRegion ri')
182205 (riPresignExpirySecs ri')
@@ -198,12 +221,12 @@ buildRequest ri = do
198221 | isJust (riPresignExpirySecs ri') ->
199222 -- case 0 from above.
200223 do
201− let signPairs = signV4 sp baseRequest
202− qpToAdd = (fmap . fmap) Just signPairs
224+ let signPairs = signV4QueryParams sp baseRequest
225+ qpToAdd = simpleQueryToQuery signPairs
203226 existingQueryParams = HT.parseQuery (NC.queryString baseRequest)
204227 updatedQueryParams = existingQueryParams ++ qpToAdd
205228 return $ NClient.setQueryString updatedQueryParams baseRequest
206− | isStreamingPayload (riPayload ri') && (not $ connectIsSecure ci') ->
229+ | isStreamingPayload (riPayload ri') && not (connectIsSecure ci') ->
207230 -- case 2 from above.
208231 do
209232 (pLen, pSrc) <- case riPayload ri of
@@ -214,22 +237,22 @@ buildRequest ri = do
214237 | otherwise ->
215238 do
216239 sp' <-
217− if
218− | connectIsSecure ci' ->
219− -- case 1 described above.
220− return sp
221− | otherwise ->
222− -- case 3 described above.
240+ ( if connectIsSecure ci'
241+ then -- case 1 described above.
242+ return sp
243+ else
244+ ( -- case 3 described above.
223245 do
224246 pHash <- getPayloadSHA256Hash $ riPayload ri'
225247 return $ sp {spPayloadHash = Just pHash}
248+ )
249+ )
226250
227251 let signHeaders = signV4 sp' baseRequest
228252 return $
229253 baseRequest
230254 { NC.requestHeaders =
231− NC.requestHeaders baseRequest
232− ++ mkHeaderFromPairs signHeaders,
255+ NC.requestHeaders baseRequest ++ signHeaders,
233256 NC.requestBody = getRequestBody (riPayload ri')
234257 }
235258
@@ -285,8 +308,8 @@ isValidBucketName bucket =
285308 not
286309 ( or
287310 [ len < 3 || len > 63,
288− or (map labelCheck labels),
289− or (map labelCharsCheck labels),
311+ any labelCheck labels,
312+ any labelCharsCheck labels,
290313 isIPCheck
291314 ]
292315 )
@@ -314,9 +337,9 @@ isValidBucketName bucket =
314337 isIPCheck = and labelAsNums && length labelAsNums == 4
315338
316339-- Throws exception iff bucket name is invalid according to AWS rules.
317−checkBucketNameValidity :: MonadIO m => Bucket -> m ()
340+checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
318341checkBucketNameValidity bucket =
319− when (not $ isValidBucketName bucket) $
342+ unless (isValidBucketName bucket) $
320343 throwIO $
321344 MErrVInvalidBucketName bucket
322345
@@ -324,8 +347,8 @@ isValidObjectName :: Object -> Bool
324347isValidObjectName object =
325348 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
326349
327−checkObjectNameValidity :: MonadIO m => Object -> m ()
350+checkObjectNameValidity :: (MonadIO m) => Object -> m ()
328351checkObjectNameValidity object =
329− when (not $ isValidObjectName object) $
352+ unless (isValidObjectName object) $
330353 throwIO $
331354 MErrVInvalidObjectName object
src/Network/Minio/AdminAPI.hs+138−73
@@ -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.
@@ -80,6 +80,7 @@ import qualified Network.HTTP.Conduit as NC
8080import qualified Network.HTTP.Types as HT
8181import Network.HTTP.Types.Header (hHost)
8282import Network.Minio.APICommon
83+import Network.Minio.Credentials
8384import Network.Minio.Data
8485import Network.Minio.Errors
8586import Network.Minio.Sign.V4
@@ -95,9 +96,12 @@ data DriveInfo = DriveInfo
9596instance FromJSON DriveInfo where
9697 parseJSON = withObject "DriveInfo" $ \v ->
9798 DriveInfo
98− <$> v .: "uuid"
99− <*> v .: "endpoint"
100− <*> v .: "state"
99+ <$> v
100+ .: "uuid"
101+ <*> v
102+ .: "endpoint"
103+ <*> v
104+ .: "state"
101105
102106data StorageClass = StorageClass
103107 { scParity :: Int,
@@ -120,12 +124,16 @@ instance FromJSON ErasureInfo where
120124 offlineDisks <- v .: "OfflineDisks"
121125 stdClass <-
122126 StorageClass
123− <$> v .: "StandardSCData"
124− <*> v .: "StandardSCParity"
127+ <$> v
128+ .: "StandardSCData"
129+ <*> v
130+ .: "StandardSCParity"
125131 rrClass <-
126132 StorageClass
127− <$> v .: "RRSCData"
128− <*> v .: "RRSCParity"
133+ <$> v
134+ .: "RRSCData"
135+ <*> v
136+ .: "RRSCParity"
129137 sets <- v .: "Sets"
130138 return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
131139
@@ -151,8 +159,10 @@ data ConnStats = ConnStats
151159instance FromJSON ConnStats where
152160 parseJSON = withObject "ConnStats" $ \v ->
153161 ConnStats
154− <$> v .: "transferred"
155− <*> v .: "received"
162+ <$> v
163+ .: "transferred"
164+ <*> v
165+ .: "received"
156166
157167data ServerProps = ServerProps
158168 { spUptime :: NominalDiffTime,
@@ -182,8 +192,10 @@ data StorageInfo = StorageInfo
182192instance FromJSON StorageInfo where
183193 parseJSON = withObject "StorageInfo" $ \v ->
184194 StorageInfo
185− <$> v .: "Used"
186− <*> v .: "Backend"
195+ <$> v
196+ .: "Used"
197+ <*> v
198+ .: "Backend"
187199
188200data CountNAvgTime = CountNAvgTime
189201 { caCount :: Int64,
@@ -194,8 +206,10 @@ data CountNAvgTime = CountNAvgTime
194206instance FromJSON CountNAvgTime where
195207 parseJSON = withObject "CountNAvgTime" $ \v ->
196208 CountNAvgTime
197− <$> v .: "count"
198− <*> v .: "avgDuration"
209+ <$> v
210+ .: "count"
211+ <*> v
212+ .: "avgDuration"
199213
200214data HttpStats = HttpStats
201215 { hsTotalHeads :: CountNAvgTime,
@@ -214,16 +228,26 @@ data HttpStats = HttpStats
214228instance FromJSON HttpStats where
215229 parseJSON = withObject "HttpStats" $ \v ->
216230 HttpStats
217− <$> v .: "totalHEADs"
218− <*> v .: "successHEADs"
219− <*> v .: "totalGETs"
220− <*> v .: "successGETs"
221− <*> v .: "totalPUTs"
222− <*> v .: "successPUTs"
223− <*> v .: "totalPOSTs"
224− <*> v .: "successPOSTs"
225− <*> v .: "totalDELETEs"
226− <*> v .: "successDELETEs"
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"
227251
228252data SIData = SIData
229253 { sdStorage :: StorageInfo,
@@ -236,10 +260,14 @@ data SIData = SIData
236260instance FromJSON SIData where
237261 parseJSON = withObject "SIData" $ \v ->
238262 SIData
239− <$> v .: "storage"
240− <*> v .: "network"
241− <*> v .: "http"
242− <*> v .: "server"
263+ <$> v
264+ .: "storage"
265+ <*> v
266+ .: "network"
267+ <*> v
268+ .: "http"
269+ <*> v
270+ .: "server"
243271
244272data ServerInfo = ServerInfo
245273 { siError :: Text,
@@ -251,9 +279,12 @@ data ServerInfo = ServerInfo
251279instance FromJSON ServerInfo where
252280 parseJSON = withObject "ServerInfo" $ \v ->
253281 ServerInfo
254− <$> v .: "error"
255− <*> v .: "addr"
256− <*> v .: "data"
282+ <$> v
283+ .: "error"
284+ <*> v
285+ .: "addr"
286+ <*> v
287+ .: "data"
257288
258289data ServerVersion = ServerVersion
259290 { svVersion :: Text,
@@ -264,8 +295,10 @@ data ServerVersion = ServerVersion
264295instance FromJSON ServerVersion where
265296 parseJSON = withObject "ServerVersion" $ \v ->
266297 ServerVersion
267− <$> v .: "version"
268− <*> v .: "commitID"
298+ <$> v
299+ .: "version"
300+ <*> v
301+ .: "commitID"
269302
270303data ServiceStatus = ServiceStatus
271304 { ssVersion :: ServerVersion,
@@ -306,9 +339,12 @@ data HealStartResp = HealStartResp
306339instance FromJSON HealStartResp where
307340 parseJSON = withObject "HealStartResp" $ \v ->
308341 HealStartResp
309− <$> v .: "clientToken"
310− <*> v .: "clientAddress"
311− <*> v .: "startTime"
342+ <$> v
343+ .: "clientToken"
344+ <*> v
345+ .: "clientAddress"
346+ <*> v
347+ .: "startTime"
312348
313349data HealOpts = HealOpts
314350 { hoRecursive :: Bool,
@@ -325,8 +361,10 @@ instance ToJSON HealOpts where
325361instance FromJSON HealOpts where
326362 parseJSON = withObject "HealOpts" $ \v ->
327363 HealOpts
328− <$> v .: "recursive"
329− <*> v .: "dryRun"
364+ <$> v
365+ .: "recursive"
366+ <*> v
367+ .: "dryRun"
330368
331369data HealItemType
332370 = HealItemMetadata
@@ -353,9 +391,12 @@ data NodeSummary = NodeSummary
353391instance FromJSON NodeSummary where
354392 parseJSON = withObject "NodeSummary" $ \v ->
355393 NodeSummary
356− <$> v .: "name"
357− <*> v .: "errSet"
358− <*> v .: "errMsg"
394+ <$> v
395+ .: "name"
396+ <*> v
397+ .: "errSet"
398+ <*> v
399+ .: "errMsg"
359400
360401data SetConfigResult = SetConfigResult
361402 { scrStatus :: Bool,
@@ -366,8 +407,10 @@ data SetConfigResult = SetConfigResult
366407instance FromJSON SetConfigResult where
367408 parseJSON = withObject "SetConfigResult" $ \v ->
368409 SetConfigResult
369− <$> v .: "status"
370− <*> v .: "nodeResults"
410+ <$> v
411+ .: "status"
412+ <*> v
413+ .: "nodeResults"
371414
372415data HealResultItem = HealResultItem
373416 { hriResultIdx :: Int,
@@ -388,16 +431,26 @@ data HealResultItem = HealResultItem
388431instance FromJSON HealResultItem where
389432 parseJSON = withObject "HealResultItem" $ \v ->
390433 HealResultItem
391− <$> v .: "resultId"
392− <*> v .: "type"
393− <*> v .: "bucket"
394− <*> v .: "object"
395− <*> v .: "detail"
396− <*> v .:? "parityBlocks"
397− <*> v .:? "dataBlocks"
398− <*> v .: "diskCount"
399− <*> v .: "setCount"
400− <*> v .: "objectSize"
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"
401454 <*> ( do
402455 before <- v .: "before"
403456 before .: "drives"
@@ -420,19 +473,27 @@ data HealStatus = HealStatus
420473instance FromJSON HealStatus where
421474 parseJSON = withObject "HealStatus" $ \v ->
422475 HealStatus
423− <$> v .: "Summary"
424− <*> v .: "StartTime"
425− <*> v .: "Settings"
426− <*> v .: "NumDisks"
427− <*> v .:? "Detail"
428− <*> v .: "Items"
476+ <$> v
477+ .: "Summary"
478+ <*> v
479+ .: "StartTime"
480+ <*> v
481+ .: "Settings"
482+ <*> v
483+ .: "NumDisks"
484+ <*> v
485+ .:? "Detail"
486+ <*> v
487+ .: "Items"
429488
430489healPath :: Maybe Bucket -> Maybe Text -> ByteString
431490healPath bucket prefix = do
432− if (isJust bucket)
491+ if isJust bucket
433492 then
434493 encodeUtf8 $
435− "v1/heal/" <> fromMaybe "" bucket <> "/"
494+ "v1/heal/"
495+ <> fromMaybe "" bucket
496+ <> "/"
436497 <> fromMaybe "" prefix
437498 else encodeUtf8 ("v1/heal/" :: Text)
438499
@@ -597,29 +658,33 @@ buildAdminRequest :: AdminReqInfo -> Minio NC.Request
597658buildAdminRequest areq = do
598659 ci <- asks mcConnInfo
599660 sha256Hash <-
600− if
601− | connectIsSecure ci ->
602− -- if secure connection
603− return "UNSIGNED-PAYLOAD"
604− -- otherwise compute sha256
605− | otherwise -> getPayloadSHA256Hash (ariPayload areq)
661+ if connectIsSecure ci
662+ then -- if secure connection
663+ return "UNSIGNED-PAYLOAD"
664+ else -- otherwise compute sha256
665+ getPayloadSHA256Hash (ariPayload areq)
606666
607667 timeStamp <- liftIO getCurrentTime
608668
669+ mgr <- asks mcConnManager
670+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671+
609672 let hostHeader = (hHost, getHostAddr ci)
610673 newAreq =
611674 areq
612675 { ariPayloadHash = Just sha256Hash,
613676 ariHeaders =
614− hostHeader :
615− sha256Header sha256Hash :
616− ariHeaders areq
677+ hostHeader
678+ : sha256Header sha256Hash
679+ : ariHeaders areq
617680 }
618681 signReq = toRequest ci newAreq
619682 sp =
620683 SignParams
621− (connectAccessKey ci)
622− (connectSecretKey ci)
684+ (coerce $ cvAccessKey cv)
685+ (coerce $ cvSecretKey cv)
686+ (coerce $ cvSessionToken cv)
687+ ServiceS3
623688 timeStamp
624689 Nothing
625690 Nothing
@@ -629,7 +694,7 @@ buildAdminRequest areq = do
629694 -- Update signReq with Authorization header containing v4 signature
630695 return
631696 signReq
632− { NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
697+ { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
633698 }
634699 where
635700 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
src/Network/Minio/CopyObject.hs+2−2
@@ -50,8 +50,8 @@ copyObjectInternal b' o srcInfo = do
5050 || (endOffset >= srcSize)
5151 )
5252 )
53− $ throwIO $
54− MErrVInvalidSrcObjByteRange range
53+ $ throwIO
54+ $ MErrVInvalidSrcObjByteRange range
5555
5656 -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
5757 -- 2. If startOffset /= 0 use multipart copy
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+135−104
@@ -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.
@@ -16,6 +16,7 @@
1616{-# LANGUAGE CPP #-}
1717{-# LANGUAGE GADTs #-}
1818{-# LANGUAGE GeneralizedNewtypeDeriving #-}
19+{-# LANGUAGE StrictData #-}
1920{-# LANGUAGE TypeFamilies #-}
2021
2122module Network.Minio.Data where
@@ -34,9 +35,10 @@ import qualified Data.Aeson as A
3435import qualified Data.ByteArray as BA
3536import qualified Data.ByteString as B
3637import qualified Data.ByteString.Lazy as LB
37−import Data.CaseInsensitive (mk)
38+import Data.Default.Class (def)
3839import qualified Data.HashMap.Strict as H
3940import qualified Data.Ini as Ini
41+import qualified Data.List as List
4042import qualified Data.Text as T
4143import qualified Data.Text.Encoding as TE
4244import Data.Time (defaultTimeLocale, formatTime)
@@ -53,6 +55,7 @@ import Network.HTTP.Types
5355 hRange,
5456 )
5557import qualified Network.HTTP.Types as HT
58+import Network.Minio.Credentials
5659import Network.Minio.Data.Crypto
5760 ( encodeToBase64,
5861 hashMD5ToBase64,
@@ -62,11 +65,12 @@ import Network.Minio.Errors
6265 ( MErrV (MErrVInvalidEncryptionKeyLength, MErrVMissingCredentials),
6366 MinioErr (..),
6467 )
68+import Network.Minio.Utils
6569import System.Directory (doesFileExist, getHomeDirectory)
6670import qualified System.Environment as Env
6771import System.FilePath.Posix (combine)
68−import Text.XML (Name (Name))
6972import qualified UnliftIO as U
73+import qualified UnliftIO.MVar as UM
7074
7175-- | max obj size is 5TiB
7276maxObjectSize :: Int64
@@ -97,13 +101,29 @@ awsRegionMap =
97101 ("us-west-2", "s3.us-west-2.amazonaws.com"),
98102 ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
99103 ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
104+ ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
100105 ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
101106 ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
107+ ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
102108 ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
103109 ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
110+ ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
104111 ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
105112 ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
113+ ("eu-west-3", "s3.eu-west-3.amazonaws.com"),
106114 ("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"),
107127 ("sa-east-1", "s3.sa-east-1.amazonaws.com")
108128 ]
109129
@@ -115,14 +135,15 @@ awsRegionMap =
115135data ConnectInfo = ConnectInfo
116136 { connectHost :: Text,
117137 connectPort :: Int,
118− connectAccessKey :: Text,
119− connectSecretKey :: Text,
138+ connectCreds :: Creds,
120139 connectIsSecure :: Bool,
121140 connectRegion :: Region,
122141 connectAutoDiscoverRegion :: Bool,
123142 connectDisableTLSCertValidation :: Bool
124143 }
125− deriving stock (Eq, Show)
144+
145+getEndpoint :: ConnectInfo -> Endpoint
146+getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
126147
127148instance IsString ConnectInfo where
128149 fromString str =
@@ -130,38 +151,33 @@ instance IsString ConnectInfo where
130151 in ConnectInfo
131152 { connectHost = TE.decodeUtf8 $ NC.host req,
132153 connectPort = NC.port req,
133− connectAccessKey = "",
134− connectSecretKey = "",
154+ connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
135155 connectIsSecure = NC.secure req,
136156 connectRegion = "",
137157 connectAutoDiscoverRegion = True,
138158 connectDisableTLSCertValidation = False
139159 }
140160
141−-- | Contains access key and secret key to access object storage.
142−data Credentials = Credentials
143− { cAccessKey :: Text,
144− cSecretKey :: Text
145− }
146− deriving stock (Eq, Show)
147−
148−-- | A Provider is an action that may return Credentials. Providers
149−-- may be chained together using 'findFirst'.
150−type Provider = IO (Maybe Credentials)
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)
151166
152−-- | Combines the given list of providers, by calling each one in
153−-- order until Credentials are found.
154−findFirst :: [Provider] -> Provider
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)
155170findFirst [] = return Nothing
156171findFirst (f : fs) = do
157172 c <- f
158173 maybe (findFirst fs) (return . Just) c
159174
160−-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
161−fromAWSConfigFile :: Provider
175+-- | This action returns a 'CredentialValue' populated from
176+-- @~\/.aws\/credentials@
177+fromAWSConfigFile :: CredentialLoader
162178fromAWSConfigFile = do
163179 credsE <- runExceptT $ do
164− homeDir <- lift $ getHomeDirectory
180+ homeDir <- lift getHomeDirectory
165181 let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
166182 fileExists <- lift $ doesFileExist awsCredsFile
167183 bool (throwE "FileNotFound") (return ()) fileExists
@@ -174,44 +190,50 @@ fromAWSConfigFile = do
174190 ExceptT $
175191 return $
176192 Ini.lookupValue "default" "aws_secret_access_key" ini
177− return $ Credentials akey skey
193+ return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
178194 return $ either (const Nothing) Just credsE
179195
180−-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
181−-- @AWS_SECRET_ACCESS_KEY@ environment variables.
182−fromAWSEnv :: Provider
196+-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
197+-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
198+fromAWSEnv :: CredentialLoader
183199fromAWSEnv = runMaybeT $ do
184200 akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
185201 skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
186− return $ Credentials (T.pack akey) (T.pack skey)
202+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
187203
188−-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
189−-- @MINIO_SECRET_KEY@ environment variables.
190−fromMinioEnv :: Provider
204+-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
205+-- and @MINIO_SECRET_KEY@ environment variables.
206+fromMinioEnv :: CredentialLoader
191207fromMinioEnv = runMaybeT $ do
192208 akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
193209 skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
194− return $ Credentials (T.pack akey) (T.pack skey)
210+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
195211
196−-- | setCredsFrom retrieves access credentials from the first
197−-- `Provider` form the given list that succeeds and sets it in the
198−-- `ConnectInfo`.
199−setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo
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
200215setCredsFrom ps ci = do
201216 pMay <- findFirst ps
202217 maybe
203218 (throwIO MErrVMissingCredentials)
204− (return . (flip setCreds ci))
219+ (return . (`setCreds` ci))
205220 pMay
206221
207−-- | setCreds sets the given `Credentials` in the `ConnectInfo`.
208−setCreds :: Credentials -> ConnectInfo -> ConnectInfo
209−setCreds (Credentials accessKey secretKey) connInfo =
222+-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
223+setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
224+setCreds cv connInfo =
210225 connInfo
211− { connectAccessKey = accessKey,
212− connectSecretKey = secretKey
226+ { connectCreds = CredsStatic cv
213227 }
214228
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}
236+
215237-- | Set the S3 region parameter in the `ConnectInfo`
216238setRegion :: Region -> ConnectInfo -> ConnectInfo
217239setRegion r connInfo =
@@ -233,15 +255,7 @@ disableTLSCertValidation :: ConnectInfo -> ConnectInfo
233255disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
234256
235257getHostAddr :: ConnectInfo -> ByteString
236−getHostAddr ci =
237− if
238− | port == 80 || port == 443 -> encodeUtf8 host
239− | otherwise ->
240− encodeUtf8 $
241− T.concat [host, ":", show port]
242− where
243− port = connectPort ci
244− host = connectHost ci
258+getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
245259
246260-- | Default Google Compute Storage ConnectInfo. Works only for
247261-- "Simple Migration" use-case with interoperability mode enabled on
@@ -264,7 +278,7 @@ awsCI = "https://s3.amazonaws.com"
264278-- ConnectInfo. Credentials are already filled in.
265279minioPlayCI :: ConnectInfo
266280minioPlayCI =
267− let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
281+ let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
268282 in setCreds playCreds $
269283 setRegion
270284 "us-east-1"
@@ -291,7 +305,7 @@ newtype SSECKey = SSECKey BA.ScrubbedBytes
291305
292306-- | Validates that the given ByteString is 32 bytes long and creates
293307-- an encryption key.
294−mkSSECKey :: MonadThrow m => ByteString -> m SSECKey
308+mkSSECKey :: (MonadThrow m) => ByteString -> m SSECKey
295309mkSSECKey keyBytes
296310 | B.length keyBytes /= 32 =
297311 throwM MErrVInvalidEncryptionKeyLength
@@ -308,7 +322,7 @@ data SSE where
308322 -- argument is the optional KMS context that must have a
309323 -- `A.ToJSON` instance - please refer to the AWS S3 documentation
310324 -- for detailed information.
311− SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
325+ SSEKMS :: (A.ToJSON a) => Maybe ByteString -> Maybe a -> SSE
312326 -- | Specifies server-side encryption with customer provided
313327 -- key. The argument is the encryption key to be used.
314328 SSEC :: SSECKey -> SSE
@@ -366,28 +380,10 @@ data PutObjectOptions = PutObjectOptions
366380defaultPutObjectOptions :: PutObjectOptions
367381defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
368382
369−-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
370−-- stripped and a Just is returned.
371−userMetadataHeaderNameMaybe :: Text -> Maybe Text
372−userMetadataHeaderNameMaybe k =
373− let prefix = T.toCaseFold "X-Amz-Meta-"
374− n = T.length prefix
375− in if T.toCaseFold (T.take n k) == prefix
376− then Just (T.drop n k)
377− else Nothing
378−
379−addXAmzMetaPrefix :: Text -> Text
380−addXAmzMetaPrefix s
381− | isJust (userMetadataHeaderNameMaybe s) = s
382− | otherwise = "X-Amz-Meta-" <> s
383−
384−mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
385−mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix $ x, encodeUtf8 y))
386−
387383pooToHeaders :: PutObjectOptions -> [HT.Header]
388384pooToHeaders poo =
389385 userMetadata
390− ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
386+ ++ mapMaybe tupToMaybe (zip names values)
391387 ++ maybe [] toPutObjectHeaders (pooSSE poo)
392388 where
393389 tupToMaybe (k, Just v) = Just (k, v)
@@ -423,6 +419,29 @@ data BucketInfo = BucketInfo
423419-- | A type alias to represent a part-number for multipart upload
424420type PartNumber = Int16
425421
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+
426445-- | A type alias to represent an upload-id for multipart upload
427446type UploadId = Text
428447
@@ -587,7 +606,8 @@ defaultGetObjectOptions =
587606
588607gooToHeaders :: GetObjectOptions -> [HT.Header]
589608gooToHeaders goo =
590− rangeHdr ++ zip names values
609+ rangeHdr
610+ ++ zip names values
591611 ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
592612 where
593613 names =
@@ -657,7 +677,7 @@ textToEvent t = case t of
657677 _ -> Nothing
658678
659679-- | Filter data type - part of notification configuration
660−data Filter = Filter
680+newtype Filter = Filter
661681 { fFilter :: FilterKey
662682 }
663683 deriving stock (Show, Eq)
@@ -668,7 +688,7 @@ defaultFilter :: Filter
668688defaultFilter = Filter defaultFilterKey
669689
670690-- | FilterKey contains FilterRules, and is part of a Filter.
671−data FilterKey = FilterKey
691+newtype FilterKey = FilterKey
672692 { fkKey :: FilterRules
673693 }
674694 deriving stock (Show, Eq)
@@ -679,7 +699,7 @@ defaultFilterKey :: FilterKey
679699defaultFilterKey = FilterKey defaultFilterRules
680700
681701-- | FilterRules represents a collection of `FilterRule`s.
682−data FilterRules = FilterRules
702+newtype FilterRules = FilterRules
683703 { frFilterRules :: [FilterRule]
684704 }
685705 deriving stock (Show, Eq)
@@ -855,21 +875,15 @@ type CSVInputProp = CSVProp
855875
856876-- | CSVProp represents CSV format properties. It is built up using
857877-- the Monoid instance.
858−data CSVProp = CSVProp (H.HashMap Text Text)
878+newtype CSVProp = CSVProp (H.HashMap Text Text)
859879 deriving stock (Show, Eq)
860880
861−#if (__GLASGOW_HASKELL__ >= 804)
862881instance Semigroup CSVProp where
863− (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
864−#endif
882+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
865883
866884instance Monoid CSVProp where
867885 mempty = CSVProp mempty
868886
869−#if (__GLASGOW_HASKELL__ < 804)
870− mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
871−#endif
872−
873887csvPropsList :: CSVProp -> [(Text, Text)]
874888csvPropsList (CSVProp h) = sort $ H.toList h
875889
@@ -926,9 +940,9 @@ setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
926940
927941-- | Set the CSV format properties in the OutputSerialization.
928942outputCSVFromProps :: CSVProp -> OutputSerialization
929−outputCSVFromProps p = OutputSerializationCSV p
943+outputCSVFromProps = OutputSerializationCSV
930944
931−data JSONInputProp = JSONInputProp {jsonipType :: JSONType}
945+newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
932946 deriving stock (Show, Eq)
933947
934948data JSONType = JSONTypeDocument | JSONTypeLines
@@ -956,7 +970,7 @@ quoteFields q = CSVProp $
956970data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
957971 deriving stock (Show, Eq)
958972
959−data JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
973+newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
960974 deriving stock (Show, Eq)
961975
962976-- | Set the output record delimiter for JSON format
@@ -968,13 +982,14 @@ outputJSONFromRecordDelimiter t =
968982
969983-- | An EventMessage represents each kind of message received from the server.
970984data EventMessage
971− = ProgressEventMessage {emProgress :: Progress}
972− | StatsEventMessage {emStats :: Stats}
985+ = ProgressEventMessage Progress
986+ | StatsEventMessage Stats
973987 | RequestLevelErrorMessage
974− { emErrorCode :: Text,
975− emErrorMessage :: Text
976− }
977− | RecordPayloadEventMessage {emPayloadBytes :: ByteString}
988+ Text
989+ -- ^ Error code
990+ Text
991+ -- ^ Error message
992+ | RecordPayloadEventMessage ByteString
978993 deriving stock (Show, Eq)
979994
980995data MsgHeaderName
@@ -1088,11 +1103,10 @@ class HasSvcNamespace env where
10881103instance HasSvcNamespace MinioConn where
10891104 getSvcNamespace env =
10901105 let host = connectHost $ mcConnInfo env
1091− in if
1092− | host == "storage.googleapis.com" ->
1093− "http://doc.s3.amazonaws.com/2006-03-01"
1094− | otherwise ->
1095− "http://s3.amazonaws.com/doc/2006-03-01/"
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+ )
10961110
10971111-- | Takes connection information and returns a connection object to
10981112-- be passed to 'runMinio'. The returned value can be kept in the
@@ -1102,7 +1116,11 @@ connect :: ConnectInfo -> IO MinioConn
11021116connect ci = do
11031117 let settings
11041118 | connectIsSecure ci && connectDisableTLSCertValidation ci =
1105− let badTlsSettings = Conn.TLSSettingsSimple True False False
1119+ let badTlsSettings =
1120+ case def of
1121+ tlsSettings@Conn.TLSSettingsSimple {} ->
1122+ tlsSettings {Conn.settingDisableCertificateValidation = True}
1123+ _ -> def
11061124 in TLS.mkManagerSettings badTlsSettings Nothing
11071125 | connectIsSecure ci = NC.tlsManagerSettings
11081126 | otherwise = defaultManagerSettings
@@ -1152,9 +1170,22 @@ runMinioRes ci m = do
11521170 conn <- liftIO $ connect ci
11531171 runMinioResWith conn m
11541172
1155−s3Name :: Text -> Text -> Name
1156−s3Name ns s = Name s (Just ns) Nothing
1157−
11581173-- | Format as per RFC 1123.
11591174formatRFC1123 :: UTCTime -> T.Text
11601175formatRFC1123 = 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/Crypto.hs+8−8
@@ -43,26 +43,26 @@ import qualified Data.Conduit as C
4343hashSHA256 :: ByteString -> ByteString
4444hashSHA256 = digestToBase16 . hashWith SHA256
4545
46−hashSHA256FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
46+hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
4747hashSHA256FromSource src = do
4848 digest <- C.connect src sinkSHA256Hash
4949 return $ digestToBase16 digest
5050 where
5151 -- To help with type inference
52− sinkSHA256Hash :: Monad m => C.ConduitM ByteString Void m (Digest SHA256)
52+ sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
5353 sinkSHA256Hash = sinkHash
5454
5555-- Returns MD5 hash hex encoded.
5656hashMD5 :: ByteString -> ByteString
5757hashMD5 = digestToBase16 . hashWith MD5
5858
59−hashMD5FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
59+hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
6060hashMD5FromSource src = do
6161 digest <- C.connect src sinkMD5Hash
6262 return $ digestToBase16 digest
6363 where
6464 -- To help with type inference
65− sinkMD5Hash :: Monad m => C.ConduitM ByteString Void m (Digest MD5)
65+ sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
6666 sinkMD5Hash = sinkHash
6767
6868hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -71,15 +71,15 @@ hmacSHA256 message key = hmac key message
7171hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
7272hmacSHA256RawBS message key = convert $ hmacSHA256 message key
7373
74−digestToBS :: ByteArrayAccess a => a -> ByteString
74+digestToBS :: (ByteArrayAccess a) => a -> ByteString
7575digestToBS = convert
7676
77−digestToBase16 :: ByteArrayAccess a => a -> ByteString
77+digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
7878digestToBase16 = convertToBase Base16
7979
8080-- Returns MD5 hash base 64 encoded.
81−hashMD5ToBase64 :: ByteArrayAccess a => a -> ByteString
81+hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
8282hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
8383
84−encodeToBase64 :: ByteArrayAccess a => a -> ByteString
84+encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
8585encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+2−1
@@ -27,6 +27,7 @@ where
2727
2828import Data.ByteString.Char8 (pack)
2929import qualified Data.Time as Time
30+import Data.Time.Format.ISO8601 (iso8601Show)
3031import Lib.Prelude
3132
3233-- | Time to expire for a presigned URL. It interpreted as a number of
@@ -49,4 +50,4 @@ awsParseTime :: [Char] -> Maybe UTCTime
4950awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
5051
5152iso8601TimeFormat :: UTCTime -> [Char]
52−iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
53+iso8601TimeFormat = iso8601Show
src/Network/Minio/Errors.hs+2−1
@@ -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.
@@ -49,6 +49,7 @@ data MErrV
4949 | MErrVInvalidEncryptionKeyLength
5050 | MErrVStreamingBodyUnexpectedEOF
5151 | MErrVUnexpectedPayload
52+ | MErrVSTSEndpointNotFound
5253 deriving stock (Show, Eq)
5354
5455instance Exception MErrV
src/Network/Minio/ListOps.hs+7−4
@@ -144,11 +144,14 @@ listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
144144 return $ foldl' (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
145145
146146 CL.sourceList $
147− map
148− ( \((uKey, uId, uInitTime), size) ->
149− UploadInfo uKey uId uInitTime size
147+ zipWith
148+ ( curry
149+ ( \((uKey, uId, uInitTime), size) ->
150+ UploadInfo uKey uId uInitTime size
151+ )
150152 )
151− $ zip (lurUploads res) aggrSizes
153+ (lurUploads res)
154+ aggrSizes
152155
153156 when (lurHasMore res) $
154157 loop (lurNextKey res) (lurNextUpload res)
src/Network/Minio/PresignedOperations.hs+26−17
@@ -1,7 +1,5 @@
1−{-# LANGUAGE CPP #-}
2−
31--
4−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
53--
64-- Licensed under the Apache License, Version 2.0 (the "License");
75-- you may not use this file except in compliance with the License.
@@ -15,6 +13,7 @@
1513-- See the License for the specific language governing permissions and
1614-- limitations under the License.
1715--
16+{-# LANGUAGE CPP #-}
1817
1918module Network.Minio.PresignedOperations
2019 ( UrlExpiry,
@@ -47,15 +46,18 @@ import Lib.Prelude
4746import qualified Network.HTTP.Client as NClient
4847import qualified Network.HTTP.Types as HT
4948import Network.Minio.API (buildRequest)
49+import Network.Minio.Credentials
5050import Network.Minio.Data
5151import Network.Minio.Data.Time
5252import Network.Minio.Errors
5353import Network.Minio.Sign.V4
5454import Network.URI (uriToString)
5555
56+{- ORMOLU_DISABLE -}
5657#if MIN_VERSION_aeson(2,0,0)
5758import qualified Data.Aeson.Key as A
5859#endif
60+{- ORMOLU_ENABLE -}
5961
6062-- | Generate a presigned URL. This function allows for advanced usage
6163-- - for simple cases prefer the `presigned*Url` functions.
@@ -178,6 +180,7 @@ data PostPolicyCondition
178180 | PPCRange Text Int64 Int64
179181 deriving stock (Show, Eq)
180182
183+{- ORMOLU_DISABLE -}
181184instance Json.ToJSON PostPolicyCondition where
182185 toJSON (PPCStartsWith k v) = Json.toJSON ["starts-with", k, v]
183186#if MIN_VERSION_aeson(2,0,0)
@@ -196,6 +199,7 @@ instance Json.ToJSON PostPolicyCondition where
196199#endif
197200 toEncoding (PPCRange k minVal maxVal) =
198201 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202+{- ORMOLU_ENABLE -}
199203
200204-- | A PostPolicy is required to perform uploads via browser forms.
201205data PostPolicy = PostPolicy
@@ -206,7 +210,7 @@ data PostPolicy = PostPolicy
206210
207211instance Json.ToJSON PostPolicy where
208212 toJSON (PostPolicy e c) =
209− Json.object $
213+ Json.object
210214 [ "expiration" .= iso8601TimeFormat e,
211215 "conditions" .= c
212216 ]
@@ -294,33 +298,37 @@ presignedPostPolicy ::
294298 Minio (ByteString, H.HashMap Text ByteString)
295299presignedPostPolicy p = do
296300 ci <- asks mcConnInfo
297− signTime <- liftIO $ Time.getCurrentTime
301+ signTime <- liftIO Time.getCurrentTime
302+ mgr <- asks mcConnManager
303+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
298304
299− let extraConditions =
305+ let extraConditions signParams =
300306 [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
301307 PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
302308 PPCEquals
303309 "x-amz-credential"
304310 ( T.intercalate
305311 "/"
306− [ connectAccessKey ci,
307− decodeUtf8 $ mkScope signTime region
312+ [ coerce $ cvAccessKey cv,
313+ decodeUtf8 $ credentialScope signParams
308314 ]
309315 )
310316 ]
311− ppWithCreds =
317+ ppWithCreds signParams =
312318 p
313− { conditions = conditions p ++ extraConditions
319+ { conditions = conditions p ++ extraConditions signParams
314320 }
315321 sp =
316322 SignParams
317− (connectAccessKey ci)
318− (connectSecretKey ci)
323+ (coerce $ cvAccessKey cv)
324+ (coerce $ cvSecretKey cv)
325+ (coerce $ cvSessionToken cv)
326+ ServiceS3
319327 signTime
320328 (Just $ connectRegion ci)
321329 Nothing
322330 Nothing
323− signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
331+ signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
324332 -- compute form-data
325333 mkPair (PPCStartsWith k v) = Just (k, v)
326334 mkPair (PPCEquals k v) = Just (k, v)
@@ -328,17 +336,18 @@ presignedPostPolicy p = do
328336 formFromPolicy =
329337 H.map encodeUtf8 $
330338 H.fromList $
331− catMaybes $
332− mkPair <$> conditions ppWithCreds
339+ mapMaybe
340+ mkPair
341+ (conditions $ ppWithCreds sp)
333342 formData = formFromPolicy `H.union` signData
334343 -- compute POST upload URL
335344 bucket = H.lookupDefault "" "bucket" formData
336345 scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
337− region = connectRegion ci
338346 url =
339347 toStrictBS $
340348 toLazyByteString $
341− scheme <> byteString (getHostAddr ci)
349+ scheme
350+ <> byteString (getHostAddr ci)
342351 <> byteString "/"
343352 <> byteString bucket
344353 <> byteString "/"
src/Network/Minio/S3API.hs+22−14
@@ -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,6 +14,14 @@
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
1826 ( Region,
1927 getLocation,
@@ -131,7 +139,8 @@ parseGetObjectHeaders object headers =
131139 let metadataPairs = getMetadata headers
132140 userMetadata = getUserMetadataMap metadataPairs
133141 metadata = getNonUserMetadataMap metadataPairs
134− in ObjectInfo <$> Just object
142+ in ObjectInfo
143+ <$> Just object
135144 <*> getLastModifiedHeader headers
136145 <*> getETagHeader headers
137146 <*> getContentLength headers
@@ -387,8 +396,8 @@ srcInfoToHeaders srcInfo =
387396 "/",
388397 srcObject srcInfo
389398 ]
390− ) :
391− rangeHdr
399+ )
400+ : rangeHdr
392401 ++ zip names values
393402 where
394403 names =
@@ -406,8 +415,7 @@ srcInfoToHeaders srcInfo =
406415 fmap formatRFC1123 . srcIfModifiedSince
407416 ]
408417 rangeHdr =
409− maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) $
410− toByteRange <$> srcRange srcInfo
418+ maybe [] ((\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) . toByteRange) (srcRange srcInfo)
411419 toByteRange :: (Int64, Int64) -> HT.ByteRange
412420 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
413421
@@ -519,14 +527,14 @@ listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys
519527 where
520528 -- build query params
521529 params =
522− ("uploads", Nothing) :
523− mkOptionalParams
524− [ ("prefix", prefix),
525− ("delimiter", delimiter),
526− ("key-marker", keyMarker),
527− ("upload-id-marker", uploadIdMarker),
528− ("max-uploads", show <$> maxKeys)
529− ]
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+ ]
530538
531539-- | List parts of an ongoing multipart upload.
532540listIncompleteParts' ::
src/Network/Minio/SelectAPI.hs+7−7
@@ -119,7 +119,7 @@ instance Exception EventStreamException
119119chunkSize :: Int
120120chunkSize = 32 * 1024
121121
122−parseBinary :: Bin.Binary a => ByteString -> IO a
122+parseBinary :: (Bin.Binary a) => ByteString -> IO a
123123parseBinary b = do
124124 case Bin.decodeOrFail $ LB.fromStrict b of
125125 Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
@@ -135,7 +135,7 @@ bytesToHeaderName t = case t of
135135 _ -> throwIO ESEInvalidHeaderType
136136
137137parseHeaders ::
138− MonadUnliftIO m =>
138+ (MonadUnliftIO m) =>
139139 Word32 ->
140140 C.ConduitM ByteString a m [MessageHeader]
141141parseHeaders 0 = return []
@@ -163,7 +163,7 @@ parseHeaders hdrLen = do
163163
164164-- readNBytes returns N bytes read from the string and throws an
165165-- exception if N bytes are not present on the stream.
166−readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
166+readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
167167readNBytes n = do
168168 b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
169169 if B.length b /= n
@@ -171,7 +171,7 @@ readNBytes n = do
171171 else return b
172172
173173crcCheck ::
174− MonadUnliftIO m =>
174+ (MonadUnliftIO m) =>
175175 C.ConduitM ByteString ByteString m ()
176176crcCheck = do
177177 b <- readNBytes 12
@@ -208,7 +208,7 @@ crcCheck = do
208208 then accumulateYield n' c'
209209 else return c'
210210
211−handleMessage :: MonadUnliftIO m => C.ConduitT ByteString EventMessage m ()
211+handleMessage :: (MonadUnliftIO m) => C.ConduitT ByteString EventMessage m ()
212212handleMessage = do
213213 b1 <- readNBytes 4
214214 msgLen :: Word32 <- liftIO $ parseBinary b1
@@ -254,7 +254,7 @@ handleMessage = do
254254 passThrough $ n - B.length b
255255
256256selectProtoConduit ::
257− MonadUnliftIO m =>
257+ (MonadUnliftIO m) =>
258258 C.ConduitT ByteString EventMessage m ()
259259selectProtoConduit = crcCheck .| handleMessage
260260
@@ -281,7 +281,7 @@ selectObjectContent b o r = do
281281 return $ NC.responseBody resp .| selectProtoConduit
282282
283283-- | A helper conduit that returns only the record payload bytes.
284−getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
284+getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
285285getPayloadBytes = do
286286 evM <- C.await
287287 case evM of
src/Network/Minio/Sign/V4.hs+208−123
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,9 +15,19 @@
1515--
1616{-# LANGUAGE BangPatterns #-}
1717
18−module Network.Minio.Sign.V4 where
18+module Network.Minio.Sign.V4
19+ ( SignParams (..),
20+ signV4QueryParams,
21+ signV4,
22+ signV4PostPolicy,
23+ signV4Stream,
24+ Service (..),
25+ credentialScope,
26+ )
27+where
1928
2029import qualified Conduit as C
30+import qualified Data.ByteArray as BA
2131import qualified Data.ByteString as B
2232import qualified Data.ByteString.Base64 as Base64
2333import qualified Data.ByteString.Char8 as B8
@@ -26,11 +36,14 @@ import Data.CaseInsensitive (mk)
2636import qualified Data.CaseInsensitive as CI
2737import qualified Data.HashMap.Strict as Map
2838import qualified Data.HashSet as Set
39+import Data.List (partition)
40+import qualified Data.List.NonEmpty as NE
2941import qualified Data.Time as Time
3042import Lib.Prelude
3143import qualified Network.HTTP.Conduit as NC
32−import Network.HTTP.Types (Header, parseQuery)
44+import Network.HTTP.Types (Header, SimpleQuery, hContentEncoding, parseQuery)
3345import qualified Network.HTTP.Types as H
46+import Network.HTTP.Types.Header (RequestHeaders)
3447import Network.Minio.Data.ByteString
3548import Network.Minio.Data.Crypto
3649import Network.Minio.Data.Time
@@ -49,20 +62,18 @@ ignoredHeaders =
4962 H.hUserAgent
5063 ]
5164
52−data SignV4Data = SignV4Data
53− { sv4SignTime :: UTCTime,
54− sv4Scope :: ByteString,
55− sv4CanonicalRequest :: ByteString,
56− sv4HeadersToSign :: [(ByteString, ByteString)],
57− sv4Output :: [(ByteString, ByteString)],
58− sv4StringToSign :: ByteString,
59− sv4SigningKey :: ByteString
60− }
61− deriving stock (Show)
65+data Service = ServiceS3 | ServiceSTS
66+ deriving stock (Eq, Show)
67+
68+toByteString :: Service -> ByteString
69+toByteString ServiceS3 = "s3"
70+toByteString ServiceSTS = "sts"
6271
6372data SignParams = SignParams
6473 { spAccessKey :: Text,
65− spSecretKey :: Text,
74+ spSecretKey :: BA.ScrubbedBytes,
75+ spSessionToken :: Maybe BA.ScrubbedBytes,
76+ spService :: Service,
6677 spTimeStamp :: UTCTime,
6778 spRegion :: Maybe Text,
6879 spExpirySecs :: Maybe UrlExpiry,
@@ -70,23 +81,6 @@ data SignParams = SignParams
7081 }
7182 deriving stock (Show)
7283
73−debugPrintSignV4Data :: SignV4Data -> IO ()
74−debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
75− B8.putStrLn "SignV4Data:"
76− B8.putStr "Timestamp: " >> print t
77− B8.putStr "Scope: " >> B8.putStrLn s
78− B8.putStrLn "Canonical Request:"
79− B8.putStrLn cr
80− B8.putStr "Headers to Sign: " >> print h2s
81− B8.putStr "Output: " >> print o
82− B8.putStr "StringToSign: " >> B8.putStrLn sts
83− B8.putStr "SigningKey: " >> printBytes sk
84− B8.putStrLn "END of SignV4Data ========="
85− where
86− printBytes b = do
87− mapM_ (\x -> B.putStr $ B.singleton x <> " ") $ B.unpack b
88− B8.putStrLn ""
89−
9084mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
9185mkAuthHeader accessKey scope signedHeaderKeys sign =
9286 let authValue =
@@ -102,6 +96,12 @@ mkAuthHeader accessKey scope signedHeaderKeys sign =
10296 ]
10397 in (H.hAuthorization, authValue)
10498
99+data IsStreaming = IsStreamingLength Int64 | NotStreaming
100+ deriving stock (Eq, Show)
101+
102+amzSecurityToken :: ByteString
103+amzSecurityToken = "X-Amz-Security-Token"
104+
105105-- | Given SignParams and request details, including request method,
106106-- request path, headers, query params and payload hash, generates an
107107-- updated set of headers, including the x-amz-date header and the
@@ -114,36 +114,23 @@ mkAuthHeader accessKey scope signedHeaderKeys sign =
114114-- is being created. The expiry is interpreted as an integer number of
115115-- seconds. The output will be the list of query-parameters to add to
116116-- the request.
117−signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
118−signV4 !sp !req =
119− let region = fromMaybe "" $ spRegion sp
120− ts = spTimeStamp sp
121− scope = mkScope ts region
122− accessKey = encodeUtf8 $ spAccessKey sp
123− secretKey = encodeUtf8 $ spSecretKey sp
117+signV4QueryParams :: SignParams -> NC.Request -> SimpleQuery
118+signV4QueryParams !sp !req =
119+ let scope = credentialScope sp
124120 expiry = spExpirySecs sp
125− sha256Hdr =
126− ( "x-amz-content-sha256",
127− fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
128− )
129− -- headers to be added to the request
130− datePair = ("X-Amz-Date", awsTimeFormatBS ts)
131− computedHeaders =
132− NC.requestHeaders req
133− ++ if isJust $ expiry
134− then []
135− else map (\(x, y) -> (mk x, y)) [datePair, sha256Hdr]
136− headersToSign = getHeadersToSign computedHeaders
121+
122+ headersToSign = getHeadersToSign $ NC.requestHeaders req
137123 signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
138124 -- query-parameters to be added before signing for presigned URLs
139125 -- (i.e. when `isJust expiry`)
140126 authQP =
141127 [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
142− ("X-Amz-Credential", B.concat [accessKey, "/", scope]),
143− datePair,
128+ ("X-Amz-Credential", B.concat [encodeUtf8 $ spAccessKey sp, "/", scope]),
129+ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
144130 ("X-Amz-Expires", maybe "" showBS expiry),
145131 ("X-Amz-SignedHeaders", signedHeaderKeys)
146132 ]
133+ ++ maybeToList ((amzSecurityToken,) . BA.convert <$> spSessionToken sp)
147134 finalQP =
148135 parseQuery (NC.queryString req)
149136 ++ if isJust expiry
@@ -156,39 +143,129 @@ signV4 !sp !req =
156143 sp
157144 (NC.setQueryString finalQP req)
158145 headersToSign
146+
159147 -- 2. compute string to sign
160− stringToSign = mkStringToSign ts scope canonicalRequest
148+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
161149 -- 3.1 compute signing key
162− signingKey = mkSigningKey ts region secretKey
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]
162+signV4 !sp !req =
163+ let scope = credentialScope sp
164+
165+ -- extra headers to be added for signing purposes.
166+ extraHeaders =
167+ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp)
168+ : ( -- payload hash is only used for S3 (not STS)
169+ [ ( "x-amz-content-sha256",
170+ fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
171+ )
172+ | spService sp == ServiceS3
173+ ]
174+ )
175+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
176+
177+ -- 1. compute canonical request
178+ reqHeaders = NC.requestHeaders req ++ extraHeaders
179+ (canonicalRequest, signedHeaderKeys) =
180+ getCanonicalRequestAndSignedHeaders
181+ NotStreaming
182+ sp
183+ req
184+ reqHeaders
185+
186+ -- 2. compute string to sign
187+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
188+ -- 3.1 compute signing key
189+ signingKey = getSigningKey sp
163190 -- 3.2 compute signature
164191 signature = computeSignature stringToSign signingKey
165192 -- 4. compute auth header
166193 authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
167− -- finally compute output pairs
168− output =
169− if isJust expiry
170− then ("X-Amz-Signature", signature) : authQP
171− else
172− [ (\(x, y) -> (CI.foldedCase x, y)) authHeader,
173− datePair,
174− sha256Hdr
175− ]
176− in output
177−
178−mkScope :: UTCTime -> Text -> ByteString
179−mkScope ts region =
180− B.intercalate
181− "/"
182− [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts,
183− encodeUtf8 region,
184− "s3",
185− "aws4_request"
186− ]
194+ in authHeader : extraHeaders
187195
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+ ]
206+
207+-- Folds header name, trims whitespace in header values, skips ignored headers
208+-- and sorts headers.
188209getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
189210getHeadersToSign !h =
190211 filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
191− map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
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)
192269
193270mkCanonicalRequest ::
194271 Bool ->
@@ -197,15 +274,16 @@ mkCanonicalRequest ::
197274 [(ByteString, ByteString)] ->
198275 ByteString
199276mkCanonicalRequest !isStreaming !sp !req !headersForSign =
200− let canonicalQueryString =
277+ let httpMethod = NC.method req
278+ canonicalUri = uriEncode False $ NC.path req
279+ canonicalQueryString =
201280 B.intercalate "&" $
202281 map (\(x, y) -> B.concat [x, "=", y]) $
203− sort $
282+ sortBy (\a b -> compare (fst a) (fst b)) $
204283 map
205− ( \(x, y) ->
206− (uriEncode True x, maybe "" (uriEncode True) y)
284+ ( bimap (uriEncode True) (maybe "" (uriEncode True))
207285 )
208− $ (parseQuery $ NC.queryString req)
286+ (parseQuery $ NC.queryString req)
209287 sortedHeaders = sort headersForSign
210288 canonicalHeaders =
211289 B.concat $
@@ -217,8 +295,8 @@ mkCanonicalRequest !isStreaming !sp !req !headersForSign =
217295 else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
218296 in B.intercalate
219297 "\n"
220− [ NC.method req,
221− uriEncode False $ NC.path req,
298+ [ httpMethod,
299+ canonicalUri,
222300 canonicalQueryString,
223301 canonicalHeaders,
224302 signedHeaders,
@@ -235,13 +313,13 @@ mkStringToSign ts !scope !canonicalRequest =
235313 hashSHA256 canonicalRequest
236314 ]
237315
238−mkSigningKey :: UTCTime -> Text -> ByteString -> ByteString
239−mkSigningKey ts region !secretKey =
316+getSigningKey :: SignParams -> ByteString
317+getSigningKey sp =
240318 hmacSHA256RawBS "aws4_request"
241− . hmacSHA256RawBS "s3"
242− . hmacSHA256RawBS (encodeUtf8 region)
243− . hmacSHA256RawBS (awsDateFormatBS ts)
244− $ B.concat ["AWS4", secretKey]
319+ . hmacSHA256RawBS (toByteString $ spService sp)
320+ . hmacSHA256RawBS (encodeUtf8 $ fromMaybe "" $ spRegion sp)
321+ . hmacSHA256RawBS (awsDateFormatBS $ spTimeStamp sp)
322+ $ B.concat ["AWS4", BA.convert $ spSecretKey sp]
245323
246324computeSignature :: ByteString -> ByteString -> ByteString
247325computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -255,20 +333,20 @@ signV4PostPolicy ::
255333 Map.HashMap Text ByteString
256334signV4PostPolicy !postPolicyJSON !sp =
257335 let stringToSign = Base64.encode postPolicyJSON
258− region = fromMaybe "" $ spRegion sp
259− signingKey = mkSigningKey (spTimeStamp sp) region $ encodeUtf8 $ spSecretKey sp
336+ signingKey = getSigningKey sp
260337 signature = computeSignature stringToSign signingKey
261− in Map.fromList
338+ in Map.fromList $
262339 [ ("x-amz-signature", signature),
263340 ("policy", stringToSign)
264341 ]
342+ ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
265343
266344chunkSizeConstant :: Int
267345chunkSizeConstant = 64 * 1024
268346
269347-- base16Len computes the number of bytes required to represent @n (> 0)@ in
270348-- hexadecimal.
271−base16Len :: Integral a => a -> Int
349+base16Len :: (Integral a) => a -> Int
272350base16Len n
273351 | n == 0 = 0
274352 | otherwise = 1 + base16Len (n `div` 16)
@@ -285,60 +363,60 @@ signedStreamLength dataLen =
285363 finalChunkSize = 1 + 17 + 64 + 2 + 2
286364 in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
287365
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+
288377signV4Stream ::
289378 Int64 ->
290379 SignParams ->
291380 NC.Request ->
292381 (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
293−-- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
294382signV4Stream !payloadLength !sp !req =
295383 let ts = spTimeStamp sp
296− addContentEncoding hs =
297− let ceMay = find (\(x, _) -> x == "content-encoding") hs
298− in case ceMay of
299− Nothing -> ("content-encoding", "aws-chunked") : hs
300− Just (_, ce) ->
301− ("content-encoding", ce <> ",aws-chunked") :
302− filter (\(x, _) -> x /= "content-encoding") hs
303− -- headers to be added to the request
304− datePair = ("X-Amz-Date", awsTimeFormatBS ts)
305− computedHeaders =
306− addContentEncoding $
307− datePair : NC.requestHeaders req
308− -- headers specific to streaming signature
384+
385+ -- compute the updated list of headers to be added for signing purposes.
309386 signedContentLength = signedStreamLength payloadLength
310− streamingHeaders :: [Header]
311− streamingHeaders =
312− [ ("x-amz-decoded-content-length", showBS payloadLength),
387+ extraHeaders =
388+ [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
389+ ("x-amz-decoded-content-length", showBS payloadLength),
313390 ("content-length", showBS signedContentLength),
314391 ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
315392 ]
316− headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders
317− signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
318− finalQP = parseQuery (NC.queryString req)
393+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
394+ requestHeaders =
395+ addContentEncoding $
396+ foldr setHeader (NC.requestHeaders req) extraHeaders
397+
319398 -- 1. Compute Seed Signature
320399 -- 1.1 Canonical Request
321− canonicalReq =
322− mkCanonicalRequest
323− True
400+ (canonicalReq, signedHeaderKeys) =
401+ getCanonicalRequestAndSignedHeaders
402+ (IsStreamingLength payloadLength)
324403 sp
325− (NC.setQueryString finalQP req)
326− headersToSign
327− region = fromMaybe "" $ spRegion sp
328− scope = mkScope ts region
404+ req
405+ requestHeaders
406+
407+ scope = credentialScope sp
329408 accessKey = spAccessKey sp
330− secretKey = spSecretKey sp
331409 -- 1.2 String toSign
332410 stringToSign = mkStringToSign ts scope canonicalReq
333411 -- 1.3 Compute signature
334412 -- 1.3.1 compute signing key
335− signingKey = mkSigningKey ts region $ encodeUtf8 secretKey
413+ signingKey = getSigningKey sp
336414 -- 1.3.2 Compute signature
337415 seedSignature = computeSignature stringToSign signingKey
338416 -- 1.3.3 Compute Auth Header
339417 authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
340418 -- 1.4 Updated headers for the request
341− finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)
419+ finalReqHeaders = authHeader : requestHeaders
342420 -- headersToAdd = authHeader : datePair : streamingHeaders
343421
344422 toHexStr n = B8.pack $ printf "%x" n
@@ -385,7 +463,8 @@ signV4Stream !payloadLength !sp !req =
385463 let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
386464 nextSign = computeSignature strToSign signingKey
387465 chunkBS =
388− toHexStr lps <> ";chunk-signature="
466+ toHexStr lps
467+ <> ";chunk-signature="
389468 <> nextSign
390469 <> "\r\n"
391470 <> bs
@@ -407,3 +486,9 @@ signV4Stream !payloadLength !sp !req =
407486 NC.requestBodySource signedContentLength $
408487 src C..| signerConduit numParts lastPSize seedSignature
409488 }
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+30−48
@@ -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.
@@ -24,7 +24,6 @@ import qualified Data.ByteString.Lazy as LB
2424import Data.CaseInsensitive (mk, original)
2525import qualified Data.Conduit.Binary as CB
2626import qualified Data.HashMap.Strict as H
27−import qualified Data.List as List
2827import qualified Data.Text as T
2928import Data.Text.Read (decimal)
3029import Data.Time
@@ -37,14 +36,12 @@ import Network.HTTP.Conduit (Response)
3736import qualified Network.HTTP.Conduit as NC
3837import qualified Network.HTTP.Types as HT
3938import qualified Network.HTTP.Types.Header as Hdr
40−import Network.Minio.Data
4139import Network.Minio.Data.ByteString
4240import Network.Minio.JsonParser (parseErrResponseJSON)
43−import Network.Minio.XmlParser (parseErrResponse)
41+import Network.Minio.XmlCommon (parseErrResponse)
4442import qualified System.IO as IO
4543import qualified UnliftIO as U
4644import qualified UnliftIO.Async as A
47−import qualified UnliftIO.MVar as UM
4845
4946allocateReadFile ::
5047 (MonadUnliftIO m, R.MonadResource m) =>
@@ -103,7 +100,7 @@ withNewHandle fp fileAction = do
103100 return resE
104101
105102mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
106−mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
103+mkHeaderFromPairs = map (first mk)
107104
108105lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
109106lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
@@ -113,7 +110,17 @@ getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
113110
114111getMetadata :: [HT.Header] -> [(Text, Text)]
115112getMetadata =
116− 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
117124
118125toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
119126toMaybeMetadataHeader (k, v) =
@@ -128,6 +135,14 @@ getNonUserMetadataMap =
128135 . fst
129136 )
130137
138+addXAmzMetaPrefix :: Text -> Text
139+addXAmzMetaPrefix s
140+ | isJust (userMetadataHeaderNameMaybe s) = s
141+ | otherwise = "X-Amz-Meta-" <> s
142+
143+mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
144+mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix x, encodeUtf8 y))
145+
131146-- | This function collects all headers starting with `x-amz-meta-`
132147-- and strips off this prefix, and returns a map.
133148getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
@@ -135,6 +150,12 @@ getUserMetadataMap =
135150 H.fromList
136151 . mapMaybe toMaybeMetadataHeader
137152
153+getHostHeader :: (ByteString, Int) -> ByteString
154+getHostHeader (host_, port_) =
155+ if port_ == 80 || port_ == 443
156+ then host_
157+ else host_ <> ":" <> show port_
158+
138159getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
139160getLastModifiedHeader hs = do
140161 modTimebs <- decodeUtf8Lenient <$> lookupHeader Hdr.hLastModified hs
@@ -154,7 +175,7 @@ isSuccessStatus sts =
154175 in (s >= 200 && s < 300)
155176
156177httpLbs ::
157− MonadIO m =>
178+ (MonadIO m) =>
158179 NC.Request ->
159180 NC.Manager ->
160181 m (NC.Response LByteString)
@@ -218,7 +239,7 @@ http req mgr = do
218239-- Similar to mapConcurrently but limits the number of threads that
219240-- can run using a quantity semaphore.
220241limitedMapConcurrently ::
221− MonadUnliftIO m =>
242+ (MonadUnliftIO m) =>
222243 Int ->
223244 (t -> m a) ->
224245 [t] ->
@@ -262,42 +283,3 @@ chunkBSConduit (s : ss) = do
262283 | B.length bs == s -> C.yield bs >> chunkBSConduit ss
263284 | B.length bs > 0 -> C.yield bs
264285 | otherwise -> return ()
265−
266−-- | Select part sizes - the logic is that the minimum part-size will
267−-- be 64MiB.
268−selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
269−selectPartSizes size =
270− uncurry (List.zip3 [1 ..]) $
271− List.unzip $
272− loop 0 size
273− where
274− ceil :: Double -> Int64
275− ceil = ceiling
276− partSize =
277− max
278− minPartSize
279− ( ceil $
280− fromIntegral size
281− / fromIntegral maxMultipartParts
282− )
283− m = partSize
284− loop st sz
285− | st > sz = []
286− | st + m >= sz = [(st, sz - st)]
287− | otherwise = (st, m) : loop (st + m) sz
288−
289−lookupRegionCache :: Bucket -> Minio (Maybe Region)
290−lookupRegionCache b = do
291− rMVar <- asks mcRegionMap
292− rMap <- UM.readMVar rMVar
293− return $ H.lookup b rMap
294−
295−addToRegionCache :: Bucket -> Region -> Minio ()
296−addToRegionCache b region = do
297− rMVar <- asks mcRegionMap
298− UM.modifyMVar_ rMVar $ return . H.insert b region
299−
300−deleteFromRegionCache :: Bucket -> Minio ()
301−deleteFromRegionCache b = do
302− rMVar <- asks mcRegionMap
303− UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/XmlCommon.hsnew file+65
@@ -0,0 +1,65 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.XmlCommon where
18+
19+import qualified Data.Text as T
20+import Data.Text.Read (decimal)
21+import Data.Time (UTCTime)
22+import Data.Time.Format.ISO8601 (iso8601ParseM)
23+import Lib.Prelude (throwIO)
24+import Network.Minio.Errors
25+import Text.XML (Name (Name), def, parseLBS)
26+import Text.XML.Cursor (Axis, Cursor, content, element, fromDocument, laxElement, ($/), (&/))
27+
28+s3Name :: Text -> Text -> Name
29+s3Name ns s = Name s (Just ns) Nothing
30+
31+uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
32+uncurry4 f (a, b, c, d) = f a b c d
33+
34+uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
35+uncurry6 f (a, b, c, d, e, g) = f a b c d e g
36+
37+-- | Parse time strings from XML
38+parseS3XMLTime :: (MonadIO m) => Text -> m UTCTime
39+parseS3XMLTime t =
40+ maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
41+ iso8601ParseM $
42+ toString t
43+
44+parseDecimal :: (MonadIO m, Integral a) => Text -> m a
45+parseDecimal numStr =
46+ either (throwIO . MErrVXmlParse . show) return $
47+ fst <$> decimal numStr
48+
49+parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
50+parseDecimals numStr = forM numStr parseDecimal
51+
52+s3Elem :: Text -> Text -> Axis
53+s3Elem ns = element . s3Name ns
54+
55+parseRoot :: (MonadIO m) => LByteString -> m Cursor
56+parseRoot =
57+ either (throwIO . MErrVXmlParse . show) (return . fromDocument)
58+ . parseLBS def
59+
60+parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
61+parseErrResponse xmldata = do
62+ r <- parseRoot xmldata
63+ let code = T.concat $ r $/ laxElement "Code" &/ content
64+ message = T.concat $ r $/ laxElement "Message" &/ content
65+ return $ toServiceErr code message
src/Network/Minio/XmlGenerator.hs+6−4
@@ -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.
@@ -25,6 +25,7 @@ where
2525import qualified Data.ByteString.Lazy as LBS
2626import qualified Data.Text as T
2727import Network.Minio.Data
28+import Network.Minio.XmlCommon
2829import Text.XML
2930
3031-- | Create a bucketConfig request body XML
@@ -105,7 +106,8 @@ instance ToXNode Notification where
105106toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
106107toXNodesWithArnName eltName arnName (NotificationConfig itemId arn events fRule) =
107108 XNode eltName $
108− [XLeaf "Id" itemId, XLeaf arnName arn] ++ map toXNode events
109+ [XLeaf "Id" itemId, XLeaf arnName arn]
110+ ++ map toXNode events
109111 ++ [toXNode fRule]
110112
111113instance ToXNode Filter where
@@ -189,7 +191,7 @@ mkSelectRequest r = LBS.toStrict $ renderLBS def sr
189191 Element
190192 "CSV"
191193 mempty
192− (map NodeElement $ map kvElement $ csvPropsList c)
194+ (map (NodeElement . kvElement) (csvPropsList c))
193195 formatNode (InputFormatJSON p) =
194196 Element
195197 "JSON"
@@ -217,7 +219,7 @@ mkSelectRequest r = LBS.toStrict $ renderLBS def sr
217219 Element
218220 "CSV"
219221 mempty
220− (map NodeElement $ map kvElement $ csvPropsList c)
222+ (map (NodeElement . kvElement) (csvPropsList c))
221223 ]
222224 rdElem Nothing = []
223225 rdElem (Just t) =
src/Network/Minio/XmlParser.hs+15−54
@@ -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.
@@ -34,48 +34,11 @@ import qualified Data.ByteString.Lazy as LB
3434import qualified Data.HashMap.Strict as H
3535import Data.List (zip4, zip6)
3636import qualified Data.Text as T
37−import Data.Text.Read (decimal)
3837import Data.Time
39−import Lib.Prelude
4038import Network.Minio.Data
41−import Network.Minio.Errors
42−import Text.XML
39+import Network.Minio.XmlCommon
4340import Text.XML.Cursor hiding (bool)
4441
45−-- | Represent the time format string returned by S3 API calls.
46−s3TimeFormat :: [Char]
47−s3TimeFormat = iso8601DateFormat $ Just "%T%QZ"
48−
49−-- | Helper functions.
50−uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
51−uncurry4 f (a, b, c, d) = f a b c d
52−
53−uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
54−uncurry6 f (a, b, c, d, e, g) = f a b c d e g
55−
56−-- | Parse time strings from XML
57−parseS3XMLTime :: MonadIO m => Text -> m UTCTime
58−parseS3XMLTime t =
59− maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
60− parseTimeM True defaultTimeLocale s3TimeFormat $
61− T.unpack t
62−
63−parseDecimal :: (MonadIO m, Integral a) => Text -> m a
64−parseDecimal numStr =
65− 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 =
76− either (throwIO . MErrVXmlParse . show) (return . fromDocument)
77− . parseLBS def
78−
7942-- | Parse the response XML of a list buckets call.
8043parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
8144parseListBuckets xmldata = do
@@ -220,13 +183,6 @@ parseListPartsResponse xmldata = do
220183
221184 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
222185
223−parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
224−parseErrResponse xmldata = do
225− r <- parseRoot xmldata
226− let code = T.concat $ r $/ element "Code" &/ content
227− message = T.concat $ r $/ element "Message" &/ content
228− return $ toServiceErr code message
229−
230186parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
231187parseNotification xmldata = do
232188 r <- parseRoot xmldata
@@ -235,9 +191,10 @@ parseNotification xmldata = do
235191 qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
236192 tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
237193 lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
238− Notification <$> (mapM (parseNode ns "Queue") qcfg)
239− <*> (mapM (parseNode ns "Topic") tcfg)
240− <*> (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
241198 where
242199 getFilterRule ns c =
243200 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
@@ -247,10 +204,13 @@ parseNotification xmldata = do
247204 let c = fromNode nodeData
248205 itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
249206 arn = T.concat $ c $/ s3Elem ns arnName &/ content
250− events = catMaybes $ map textToEvent $ c $/ s3Elem ns "Event" &/ content
207+ events = mapMaybe textToEvent (c $/ s3Elem ns "Event" &/ content)
251208 rules =
252− c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key"
253− &/ s3Elem ns "FilterRule" &| getFilterRule ns
209+ c
210+ $/ s3Elem ns "Filter"
211+ &/ s3Elem ns "S3Key"
212+ &/ s3Elem ns "FilterRule"
213+ &| getFilterRule ns
254214 return $
255215 NotificationConfig
256216 itemId
@@ -258,12 +218,13 @@ parseNotification xmldata = do
258218 events
259219 (Filter $ FilterKey $ FilterRules rules)
260220
261−parseSelectProgress :: MonadIO m => ByteString -> m Progress
221+parseSelectProgress :: (MonadIO m) => ByteString -> m Progress
262222parseSelectProgress xmldata = do
263223 r <- parseRoot $ LB.fromStrict xmldata
264224 let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
265225 bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content
266226 bReturned = T.concat $ r $/ element "BytesReturned" &/ content
267− Progress <$> parseDecimal bScanned
227+ Progress
228+ <$> parseDecimal bScanned
268229 <*> parseDecimal bProcessed
269230 <*> parseDecimal bReturned
stack.yaml+4−4
@@ -15,7 +15,7 @@
1515# resolver:
1616# name: custom-snapshot
1717# location: "./custom-snapshot.yaml"
18−resolver: lts-19.7
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-19.7
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.lock+12−5
@@ -3,10 +3,17 @@
33# For more information, please see the documentation at:
44# https://docs.haskellstack.org/en/stable/lock_files
55
6−packages: []
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
714snapshots:
815- completed:
9− size: 618884
10− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/7.yaml
11− sha256: 57d4ce67cc097fea2058446927987bc1f7408890e3a6df0da74e5e318f051c20
12− original: lts-19.7
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+165−67
@@ -1,7 +1,5 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−
31--
4−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
53--
64-- Licensed under the Apache License, Version 2.0 (the "License");
75-- you may not use this file except in compliance with the License.
@@ -32,6 +30,7 @@ import qualified Network.HTTP.Client.MultipartFormData as Form
3230import qualified Network.HTTP.Conduit as NC
3331import qualified Network.HTTP.Types as HT
3432import Network.Minio
33+import Network.Minio.Credentials (Creds (CredsStatic))
3534import Network.Minio.Data
3635import Network.Minio.Data.Crypto
3736import Network.Minio.S3API
@@ -50,9 +49,54 @@ main = defaultMain tests
5049tests :: TestTree
5150tests = testGroup "Tests" [liveServerUnitTests]
5251
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+
5397-- conduit that generates random binary stream of given length
54−randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
55−randomDataSrc s' = genBS s'
98+randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
99+randomDataSrc = genBS
56100 where
57101 concatIt bs n =
58102 BS.concat $
@@ -69,7 +113,7 @@ randomDataSrc s' = genBS s'
69113 yield $ concatIt byteArr64 oneMiB
70114 genBS (s - oneMiB)
71115
72−mkRandFile :: R.MonadResource m => Int64 -> m FilePath
116+mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
73117mkRandFile size = do
74118 dir <- liftIO getTemporaryDirectory
75119 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
@@ -77,14 +121,24 @@ mkRandFile size = do
77121funTestBucketPrefix :: Text
78122funTestBucketPrefix = "miniohstest-"
79123
80−loadTestServer :: IO ConnectInfo
81−loadTestServer = do
82− val <- Env.lookupEnv "MINIO_LOCAL"
83− isSecure <- Env.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
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)
88142
89143funTestWithBucket ::
90144 TestName ->
@@ -95,7 +149,7 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
95149 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
96150 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
97151 liftStep = liftIO . step
98− connInfo <- loadTestServer
152+ connInfo <- loadTestServerConnInfo
99153 ret <- runMinio connInfo $ do
100154 liftStep $ "Creating bucket for test - " ++ t
101155 foundBucket <- bucketExists b
@@ -105,6 +159,17 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
105159 deleteBucket b
106160 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
107161
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+
108173liveServerUnitTests :: TestTree
109174liveServerUnitTests =
110175 testGroup
@@ -125,7 +190,8 @@ liveServerUnitTests =
125190 presignedUrlFunTest,
126191 presignedPostPolicyFunTest,
127192 bucketPolicyFunTest,
128− getNPutSSECTest
193+ getNPutSSECTest,
194+ assumeRoleRequestTest
129195 ]
130196
131197basicTests :: TestTree
@@ -136,7 +202,8 @@ basicTests = funTestWithBucket "Basic tests" $
136202 unless (length (filter (== bucket) $ map biName buckets) == 1) $
137203 liftIO $
138204 assertFailure
139− ( "The bucket " ++ show bucket
205+ ( "The bucket "
206+ ++ show bucket
140207 ++ " was expected to exist."
141208 )
142209
@@ -179,7 +246,7 @@ basicTests = funTestWithBucket "Basic tests" $
179246 "test-file"
180247 outFile
181248 defaultGetObjectOptions
182− { gooIfUnmodifiedSince = (Just unmodifiedTime)
249+ { gooIfUnmodifiedSince = Just unmodifiedTime
183250 }
184251 case resE of
185252 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
@@ -193,7 +260,7 @@ basicTests = funTestWithBucket "Basic tests" $
193260 "test-file"
194261 outFile
195262 defaultGetObjectOptions
196− { gooIfMatch = (Just "invalid-etag")
263+ { gooIfMatch = Just "invalid-etag"
197264 }
198265 case resE1 of
199266 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
@@ -207,7 +274,7 @@ basicTests = funTestWithBucket "Basic tests" $
207274 "test-file"
208275 outFile
209276 defaultGetObjectOptions
210− { gooRange = (Just $ HT.ByteRangeFromTo 100 300)
277+ { gooRange = Just $ HT.ByteRangeFromTo 100 300
211278 }
212279 case resE2 of
213280 Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
@@ -219,7 +286,7 @@ basicTests = funTestWithBucket "Basic tests" $
219286 "test-file"
220287 outFile
221288 defaultGetObjectOptions
222− { gooRange = (Just $ HT.ByteRangeFrom 1)
289+ { gooRange = Just $ HT.ByteRangeFrom 1
223290 }
224291
225292 step "fGetObject a non-existent object and check for NoSuchKey exception"
@@ -230,7 +297,7 @@ basicTests = funTestWithBucket "Basic tests" $
230297
231298 step "create new multipart upload works"
232299 uid <- newMultipartUpload bucket "newmpupload" []
233− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
300+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
234301
235302 step "abort a new multipart upload works"
236303 abortMultipartUpload bucket "newmpupload" uid
@@ -246,7 +313,7 @@ basicTests = funTestWithBucket "Basic tests" $
246313
247314 step "get metadata of the object"
248315 res <- statObject bucket object defaultGetObjectOptions
249− liftIO $ (oiSize res) @?= 0
316+ liftIO $ oiSize res @?= 0
250317
251318 step "delete object"
252319 deleteObject bucket object
@@ -261,7 +328,7 @@ lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
261328 step "Prepare for low-level multipart tests."
262329 step "create new multipart upload"
263330 uid <- newMultipartUpload bucket object []
264− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
331+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
265332
266333 randFile <- mkRandFile mb15
267334
@@ -278,7 +345,8 @@ lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
278345 fGetObject bucket object destFile defaultGetObjectOptions
279346 gotSize <- withNewHandle destFile getFileSize
280347 liftIO $
281− gotSize == Right (Just mb15)
348+ gotSize
349+ == Right (Just mb15)
282350 @? "Wrong file size of put file after getting"
283351
284352 step "Cleanup actions"
@@ -302,7 +370,8 @@ putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
302370 fGetObject bucket obj destFile defaultGetObjectOptions
303371 gotSize <- withNewHandle destFile getFileSize
304372 liftIO $
305− gotSize == Right (Just mb1)
373+ gotSize
374+ == Right (Just mb1)
306375 @? "Wrong file size of put file after getting"
307376
308377 step "Cleanup actions"
@@ -326,7 +395,8 @@ putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no siz
326395 fGetObject bucket obj destFile defaultGetObjectOptions
327396 gotSize <- withNewHandle destFile getFileSize
328397 liftIO $
329− gotSize == Right (Just mb70)
398+ gotSize
399+ == Right (Just mb70)
330400 @? "Wrong file size of put file after getting"
331401
332402 step "Cleanup actions"
@@ -337,22 +407,20 @@ highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
337407 \step bucket -> do
338408 step "High-level listObjects Test"
339409 step "put 3 objects"
340− let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
341− extractObjectsFromList os =
410+ let extractObjectsFromList =
342411 mapM
343− ( \t -> case t of
412+ ( \case
344413 ListItemObject o -> Just $ oiObject o
345414 _ -> Nothing
346415 )
347− os
348− expectedNonRecList = ["o4", "dir/"]
349− extractObjectsAndDirsFromList os =
416+ extractObjectsAndDirsFromList =
350417 map
351− ( \t -> case t of
418+ ( \case
352419 ListItemObject o -> oiObject o
353420 ListItemPrefix d -> d
354421 )
355− os
422+ expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
423+ expectedNonRecList = ["o4", "dir/"]
356424
357425 testFilepath <- mkRandFile 200
358426 forM_ expectedObjects $
@@ -367,11 +435,11 @@ highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
367435 step "High-level recursive listing of objects"
368436 objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
369437
370− liftIO $
371− assertEqual
438+ liftIO
439+ $ assertEqual
372440 "Objects match failed!"
373441 (Just $ sort expectedObjects)
374− $ extractObjectsFromList objects
442+ $ extractObjectsFromList objects
375443
376444 step "High-level listing of objects (version 1)"
377445 itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
@@ -385,45 +453,45 @@ highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
385453 listObjectsV1 bucket Nothing True
386454 C..| sinkList
387455
388− liftIO $
389− assertEqual
456+ liftIO
457+ $ assertEqual
390458 "Objects match failed!"
391459 (Just $ sort expectedObjects)
392− $ extractObjectsFromList objectsV1
460+ $ extractObjectsFromList objectsV1
393461
394462 let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
395463 expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
396464 step "High-level listing with prefix"
397465 prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
398− liftIO $
399− assertEqual
466+ liftIO
467+ $ assertEqual
400468 "Objects/Dirs under prefix match failed!"
401469 expectedPrefListing
402− $ extractObjectsAndDirsFromList prefItems
470+ $ extractObjectsAndDirsFromList prefItems
403471
404472 step "High-level listing with prefix recursive"
405473 prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
406− liftIO $
407− assertEqual
474+ liftIO
475+ $ assertEqual
408476 "Objects/Dirs under prefix match recursive failed!"
409477 expectedPrefListingRec
410− $ extractObjectsFromList prefItemsRec
478+ $ extractObjectsFromList prefItemsRec
411479
412480 step "High-level listing with prefix (version 1)"
413481 prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
414− liftIO $
415− assertEqual
482+ liftIO
483+ $ assertEqual
416484 "Objects/Dirs under prefix match failed!"
417485 expectedPrefListing
418− $ extractObjectsAndDirsFromList prefItemsV1
486+ $ extractObjectsAndDirsFromList prefItemsV1
419487
420488 step "High-level listing with prefix recursive (version 1)"
421489 prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
422− liftIO $
423− assertEqual
490+ liftIO
491+ $ assertEqual
424492 "Objects/Dirs under prefix match recursive failed!"
425493 expectedPrefListingRec
426− $ extractObjectsFromList prefItemsRecV1
494+ $ extractObjectsFromList prefItemsRecV1
427495
428496 step "Cleanup actions"
429497 forM_ expectedObjects $
@@ -434,7 +502,7 @@ highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
434502 step "create 10 multipart uploads"
435503 forM_ [1 .. 10 :: Int] $ \_ -> do
436504 uid <- newMultipartUpload bucket object []
437− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
505+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
438506
439507 step "High-level listing of incomplete multipart uploads"
440508 uploads <-
@@ -496,7 +564,7 @@ listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
496564 map
497565 ( T.concat
498566 . ("test-file-" :)
499− . (\x -> [x])
567+ . (: [])
500568 . T.pack
501569 . show
502570 )
@@ -515,7 +583,7 @@ listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
515583 let object = "newmpupload"
516584 forM_ [1 .. 10 :: Int] $ \_ -> do
517585 uid <- newMultipartUpload bucket object []
518− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
586+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
519587
520588 step "list incomplete multipart uploads"
521589 incompleteUploads <-
@@ -526,7 +594,7 @@ listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
526594 Nothing
527595 Nothing
528596 Nothing
529− liftIO $ (length $ lurUploads incompleteUploads) @?= 10
597+ liftIO $ length (lurUploads incompleteUploads) @?= 10
530598
531599 step "cleanup"
532600 forM_ (lurUploads incompleteUploads) $
@@ -537,7 +605,7 @@ listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
537605
538606 step "create a multipart upload"
539607 uid <- newMultipartUpload bucket object []
540− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
608+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
541609
542610 step "put object parts 1..10"
543611 inputFile <- mkRandFile mb5
@@ -547,7 +615,7 @@ listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
547615
548616 step "fetch list parts"
549617 listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
550− liftIO $ (length $ lprParts listPartsResult) @?= 10
618+ liftIO $ length (lprParts listPartsResult) @?= 10
551619 abortMultipartUpload bucket object uid
552620
553621presignedUrlFunTest :: TestTree
@@ -570,6 +638,7 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
570638 []
571639 []
572640
641+ -- print putUrl
573642 let size1 = 1000 :: Int64
574643 inputFile <- mkRandFile size1
575644
@@ -658,10 +727,10 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
658727 NC.httpLbs req mgr
659728
660729presignedPostPolicyFunTest :: TestTree
661−presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
730+presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
662731 \step bucket -> do
663732 step "presignedPostPolicy basic test"
664− now <- liftIO $ Time.getCurrentTime
733+ now <- liftIO Time.getCurrentTime
665734
666735 let key = "presignedPostPolicyTest/myfile"
667736 policyConds =
@@ -692,7 +761,7 @@ presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
692761 postForm url formData inputFile = do
693762 req <- NC.parseRequest $ decodeUtf8 url
694763 let parts =
695− map (\(x, y) -> Form.partBS x y) $
764+ map (uncurry Form.partBS) $
696765 H.toList formData
697766 parts' = parts ++ [Form.partFile "file" inputFile]
698767 req' <- Form.formDataBody parts' req
@@ -749,7 +818,7 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
749818 `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
750819 case respE of
751820 Left err -> liftIO $ assertFailure $ show err
752− Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")
821+ Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
753822
754823 deleteObject bucket obj
755824
@@ -804,7 +873,7 @@ multipartTest = funTestWithBucket "Multipart Tests" $
804873 C.runConduit $
805874 listIncompleteUploads bucket (Just object) False
806875 C..| sinkList
807− liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
876+ liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
808877
809878putObjectContentTypeTest :: TestTree
810879putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
@@ -912,7 +981,7 @@ putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
912981 -- need to do a case-insensitive comparison
913982 sortedMeta =
914983 sort $
915− map (\(k, v) -> (T.toLower k, T.toLower v)) $
984+ map (bimap T.toLower T.toLower) $
916985 H.toList m
917986 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
918987
@@ -947,7 +1016,7 @@ getObjectTest = funTestWithBucket "getObject test" $
9471016 -- need to do a case-insensitive comparison
9481017 sortedMeta =
9491018 sort $
950− map (\(k, v) -> (T.toLower k, T.toLower v)) $
1019+ map (bimap T.toLower T.toLower) $
9511020 H.toList m
9521021 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
9531022
@@ -1177,9 +1246,38 @@ getNPutSSECTest =
11771246
11781247 gotSize <- withNewHandle dstFile getFileSize
11791248 liftIO $
1180− gotSize == Right (Just mb1)
1249+ gotSize
1250+ == Right (Just mb1)
11811251 @? "Wrong file size of object when getting"
11821252
11831253 step "Cleanup"
11841254 deleteObject bucket obj
11851255 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/JsonParser/Test.hs+1−1
@@ -34,7 +34,7 @@ jsonParserTests =
3434 ]
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
test/Network/Minio/XmlGenerator/Test.hs+12−4
@@ -20,6 +20,7 @@ module Network.Minio.XmlGenerator.Test
2020 )
2121where
2222
23+import qualified Data.ByteString.Lazy as LBS
2324import Lib.Prelude
2425import Network.Minio.Data
2526import Network.Minio.TestHelpers
@@ -28,6 +29,7 @@ import Network.Minio.XmlParser (parseNotification)
2829import Test.Tasty
2930import Test.Tasty.HUnit
3031import Text.RawString.QQ (r)
32+import Text.XML (def, parseLBS)
3133
3234xmlGeneratorTests :: TestTree
3335xmlGeneratorTests =
@@ -120,7 +122,13 @@ testMkPutNotificationRequest =
120122testMkSelectRequest :: Assertion
121123testMkSelectRequest = mapM_ assertFn cases
122124 where
123− assertFn (a, b) = assertEqual "selectRequest XML should match: " b $ mkSelectRequest a
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
124132 cases =
125133 [ ( SelectRequest
126134 "Select * from S3Object"
@@ -143,8 +151,8 @@ testMkSelectRequest = mapM_ assertFn cases
143151 <> quoteEscapeCharacter "\""
144152 )
145153 (Just False),
146− [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>&#34;</QuoteCharacter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><RecordDelimiter>
147−</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>&#34;</QuoteCharacter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
154+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><FieldDelimiter>,</FieldDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><RecordDelimiter>
155+</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
148156</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
149157 ),
150158 ( setRequestProgressEnabled False $
@@ -168,7 +176,7 @@ testMkSelectRequest = mapM_ assertFn cases
168176 <> quoteCharacter "\""
169177 <> quoteEscapeCharacter "\""
170178 ),
171− [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>&#34;</QuoteCharacter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
179+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
172180</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
173181 )
174182 ]
test/Network/Minio/XmlParser/Test.hs+1−1
@@ -49,7 +49,7 @@ xmlParserTests =
4949 ]
5050
5151tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
52−tryValidationErr act = try act
52+tryValidationErr = try
5353
5454assertValidtionErr :: MErrV -> Assertion
5555assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
test/Spec.hs+12−10
@@ -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.
@@ -20,7 +20,6 @@ import Lib.Prelude
2020import Network.Minio.API.Test
2121import Network.Minio.CopyObject
2222import Network.Minio.Data
23−import Network.Minio.PutObject
2423import Network.Minio.Utils.Test
2524import Network.Minio.XmlGenerator.Test
2625import Network.Minio.XmlParser.Test
@@ -55,17 +54,17 @@ qcProps =
5554 \n ->
5655 let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
5756 -- check that pns increments from 1.
58− isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1 ..]
57+ isPNumsAscendingFrom1 = all (uncurry (==)) $ zip pns [1 ..]
5958 consPairs [] = []
6059 consPairs [_] = []
61− consPairs (a : (b : c)) = (a, b) : (consPairs (b : c))
60+ consPairs (a : (b : c)) = (a, b) : consPairs (b : c)
6261 -- check `offs` is monotonically increasing.
63− isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs
62+ isOffsetsAsc = all (uncurry (<)) $ consPairs offs
6463 -- check sizes sums to n.
6564 isSumSizeOk = sum sizes == n
6665 -- check sizes are constant except last
6766 isSizesConstantExceptLast =
68− all (\(a, b) -> a == b) (consPairs $ L.init sizes)
67+ all (uncurry (==)) (consPairs $ L.init sizes)
6968 -- check each part except last is at least minPartSize;
7069 -- last part may be 0 only if it is the only part.
7170 nparts = length sizes
@@ -79,7 +78,9 @@ qcProps =
7978 listToMaybe sizes
8079 | otherwise -> False
8180 in n < 0
82− || ( isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk
81+ || ( isPNumsAscendingFrom1
82+ && isOffsetsAsc
83+ && isSumSizeOk
8384 && isSizesConstantExceptLast
8485 && isMinPartSizeOk
8586 ),
@@ -92,7 +93,7 @@ qcProps =
9293 isFirstPartOk = maybe False ((start ==) . fst) $ listToMaybe pairs
9394 -- each pair is >=64MiB except last, and all those parts
9495 -- have same size.
95− initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ init <$> nonEmpty pairs
96+ initSizes = maybe [] (map (\(a, b) -> b - a + 1) . init) (nonEmpty pairs)
9697 isPartSizesOk =
9798 all (>= minPartSize) initSizes
9899 && maybe
@@ -104,8 +105,9 @@ qcProps =
104105 snds = take (length pairs - 1) $ map snd pairs
105106 isContParts =
106107 length fsts == length snds
107− && and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
108− in start < 0 || start > end
108+ && all (\(a, b) -> a == b + 1) (zip fsts snds)
109+ in start < 0
110+ || start > end
109111 || (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts),
110112 QC.testProperty "mkSSECKey:" $
111113 \w8s ->