git.iko.soy/typeable/minio-hs
on master help sign in
60 files changed+2525−1273
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml+266 .github/workflows/haskell.yml−54 .travis.yml−61 CHANGELOG.md+31 CNAME+1 README.md+50−29 Setup.hs−19 examples/AssumeRole.hs+47 examples/FileUploader.hs+1−2 examples/GetConfig.hs+2−2 examples/GetObject.hs+1−1 examples/Heal.hs−1 examples/ListIncompleteUploads.hs+3−3 examples/ListObjects.hs+3−3 examples/PresignedGetObject.hs+3−2 examples/PresignedPostPolicy.hs+7−5 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+71−15 src/Lib/Prelude.hs+6−10 src/Network/Minio.hs+22−8 src/Network/Minio/API.hs+124−51 src/Network/Minio/APICommon.hs+9−1 src/Network/Minio/AdminAPI.hs+163−97 src/Network/Minio/CopyObject.hs+7−9 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+235−188 src/Network/Minio/Data/ByteString.hs+7−6 src/Network/Minio/Data/Crypto.hs+8−9 src/Network/Minio/Data/Time.hs+7−1 src/Network/Minio/Errors.hs+13−7 src/Network/Minio/JsonParser.hs+3−3 src/Network/Minio/ListOps.hs+56−21 src/Network/Minio/PresignedOperations.hs+73−66 src/Network/Minio/PutObject.hs+7−7 src/Network/Minio/S3API.hs+96−74 src/Network/Minio/SelectAPI.hs+11−11 src/Network/Minio/Sign/V4.hs+241−156 src/Network/Minio/Utils.hs+45−61 src/Network/Minio/XmlCommon.hs+65 src/Network/Minio/XmlGenerator.hs+23−22 src/Network/Minio/XmlParser.hs+23−62 stack.yaml+3−5 stack.yaml.lock+8−15 test/LiveServer.hs+217−110 test/Network/Minio/API/Test.hs+9−7 test/Network/Minio/JsonParser/Test.hs+4−4 test/Network/Minio/TestHelpers.hs−1 test/Network/Minio/Utils/Test.hs−1 test/Network/Minio/XmlGenerator/Test.hs+38−29 test/Network/Minio/XmlParser/Test.hs+10−9 test/Spec.hs+18−16 test/cert/private.key+28 test/cert/public.crt+19
.github/workflows/ci.ymlnew file+266
@@ -0,0 +1,266 @@
1+name: CI
2+
3+# Trigger the workflow on push or pull request, but only for the master branch
4+on:
5+ pull_request:
6+ branches: [master]
7+ push:
8+ branches: [master]
9+
10+# This ensures that previous jobs for the PR are canceled when the PR is
11+# updated.
12+concurrency:
13+ group: ${{ github.workflow }}-${{ github.head_ref }}
14+ cancel-in-progress: true
15+
16+# Env vars for tests
17+env:
18+ MINIO_ACCESS_KEY: minio
19+ MINIO_SECRET_KEY: minio123
20+ MINIO_LOCAL: 1
21+
22+jobs:
23+ ormolu:
24+ runs-on: ubuntu-latest
25+ steps:
26+ - uses: actions/checkout@v4
27+ - uses: haskell-actions/run-ormolu@v15
28+ with:
29+ version: "0.5.0.1"
30+
31+ hlint:
32+ runs-on: ubuntu-latest
33+ steps:
34+ - uses: actions/checkout@v4
35+
36+ - name: "Set up HLint"
37+ uses: haskell-actions/hlint-setup@v2
38+ with:
39+ version: "3.5"
40+
41+ - name: "Run HLint"
42+ uses: haskell-actions/hlint-run@v2
43+ with:
44+ path: '["src/", "test/", "examples"]'
45+ fail-on: warning
46+
47+ cabal:
48+ name: ${{ matrix.os }} / ghc-${{ matrix.ghc }} / cabal-${{ matrix.cabal }}
49+ runs-on: ${{ matrix.os }}
50+ needs: ormolu
51+ strategy:
52+ matrix:
53+ os: [ubuntu-latest, windows-latest, macos-latest]
54+ cabal: ["3.8", "latest"]
55+ ghc:
56+ - "9.8"
57+ - "9.6"
58+ - "9.4"
59+ - "9.2"
60+ - "9.0"
61+ - "8.10"
62+ exclude:
63+ # macos llvm issue for versions less than 9.2
64+ - os: macos-latest
65+ ghc: "8.10"
66+ - os: macos-latest
67+ ghc: "9.0"
68+ # Cabal 3.8 supports GHC < 9.6
69+ - cabal: "3.8"
70+ ghc: "9.6"
71+ - cabal: "3.8"
72+ ghc: "9.8"
73+
74+ steps:
75+ - uses: actions/checkout@v4
76+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
77+
78+ - uses: haskell-actions/setup@v2
79+ id: setup
80+ name: Setup Haskell
81+ with:
82+ ghc-version: ${{ matrix.ghc }}
83+ cabal-version: ${{ matrix.cabal }}
84+ cabal-update: true
85+
86+ - name: Configure
87+ run: |
88+ cabal configure --enable-tests --enable-benchmarks --test-show-details=direct -fexamples -fdev -flive-test
89+ cabal build all --dry-run
90+ # The last step generates dist-newstyle/cache/plan.json for the cache key.
91+
92+ - name: Restore cached dependencies
93+ uses: actions/cache/restore@v4
94+ id: cache
95+ env:
96+ key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
97+ with:
98+ path: ${{ steps.setup.outputs.cabal-store }}
99+ key: ${{ env.key }}-plan-${{ hashFiles('**/plan.json') }}
100+ restore-keys: ${{ env.key }}-
101+
102+ - name: Install dependencies
103+ # If we had an exact cache hit, the dependencies will be up to date.
104+ if: steps.cache.outputs.cache-hit != 'true'
105+ run: cabal build all --only-dependencies
106+
107+ # Cache dependencies already here, so that we do not have to rebuild them should the subsequent steps fail.
108+ - name: Save cached dependencies
109+ uses: actions/cache/save@v4
110+ # If we had an exact cache hit, trying to save the cache would error because of key clash.
111+ if: steps.cache.outputs.cache-hit != 'true'
112+ with:
113+ path: ${{ steps.setup.outputs.cabal-store }}
114+ key: ${{ steps.cache.outputs.cache-primary-key }}
115+
116+ - name: Build
117+ run: |
118+ cabal build all
119+
120+ - name: Setup TLS certs for MinIO for testing (Linux)
121+ if: matrix.os == 'ubuntu-latest'
122+ run: |
123+ mkdir -p /tmp/minio /tmp/minio-config/certs
124+ cp test/cert/* /tmp/minio-config/certs/
125+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
126+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
127+ sudo update-ca-certificates
128+
129+ ## Currently disable TLS setup for MacOS due to issues in trusting it on MacOS.
130+ - name: Setup TLS certs for MinIO for testing (MacOS)
131+ if: matrix.os == 'macos-latest'
132+ run: |
133+ mkdir -p /tmp/minio /tmp/minio-config/certs
134+ cp test/cert/* /tmp/minio-config/certs/
135+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
136+ # sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
137+
138+ - name: Setup MinIO for testing (Windows)
139+ if: matrix.os == 'windows-latest'
140+ run: |
141+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
142+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
143+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
144+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
145+
146+ - name: Test (Linux)
147+ if: matrix.os == 'ubuntu-latest'
148+ env:
149+ MINIO_SECURE: 1
150+ run: |
151+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
152+ ghc --version
153+ cabal --version
154+ cabal test all
155+
156+ - name: Test (MacOS)
157+ if: matrix.os == 'macos-latest'
158+ # # Leave MINIO_SECURE unset to disable TLS in tests.
159+ # env:
160+ # MINIO_SECURE: 1
161+ run: |
162+ /tmp/minio/minio server --quiet data1 data2 data3 data4 2>&1 > minio.log &
163+ ghc --version
164+ cabal --version
165+ cabal test all
166+
167+ - name: Test (Windows)
168+ if: matrix.os == 'windows-latest'
169+ env:
170+ MINIO_SECURE: 1
171+ run: |
172+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
173+ ghc --version
174+ cabal --version
175+ cabal test all
176+
177+ stack:
178+ name: stack / ghc ${{ matrix.ghc }}
179+ runs-on: ${{ matrix.os }}
180+ env:
181+ MINIO_SECURE: 1
182+ strategy:
183+ matrix:
184+ ghc:
185+ - "8.10.7"
186+ - "9.0.2"
187+ - "9.2.8"
188+ - "9.4.8"
189+ - "9.6.5"
190+ - "9.8.2"
191+ os: [ubuntu-latest]
192+
193+ steps:
194+ - uses: actions/checkout@v4
195+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
196+
197+ - uses: haskell-actions/setup@v2
198+ with:
199+ ghc-version: ${{ matrix.ghc }}
200+ enable-stack: true
201+ stack-version: "latest"
202+
203+ - uses: actions/cache@v4
204+ name: Cache ~/.stack
205+ with:
206+ path: ~/.stack
207+ key: ${{ runner.os }}-stack-global-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}
208+ restore-keys: |
209+ ${{ runner.os }}-stack-global-
210+ - uses: actions/cache@v4
211+ name: Cache .stack-work
212+ with:
213+ path: .stack-work
214+ key: ${{ runner.os }}-stack-work-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}-${{ hashFiles('**/*.hs') }}
215+ restore-keys: |
216+ ${{ runner.os }}-stack-work-
217+
218+ - name: Install dependencies
219+ run: |
220+ stack --version
221+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
222+
223+ - name: Build
224+ run: |
225+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --flag minio-hs:examples --flag minio-hs:live-test --flag minio-hs:dev
226+
227+ - name: Setup MinIO for testing (Linux)
228+ if: matrix.os == 'ubuntu-latest'
229+ run: |
230+ mkdir -p /tmp/minio /tmp/minio-config/certs
231+ cp test/cert/* /tmp/minio-config/certs/
232+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
233+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
234+ sudo update-ca-certificates
235+
236+ - name: Setup MinIO for testing (MacOS)
237+ if: matrix.os == 'macos-latest'
238+ run: |
239+ mkdir -p /tmp/minio /tmp/minio-config/certs
240+ cp test/cert/* /tmp/minio-config/certs/
241+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
242+ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
243+
244+ - name: Setup MinIO for testing (Windows)
245+ if: matrix.os == 'windows-latest'
246+ run: |
247+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
248+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
249+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
250+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
251+
252+ - name: Test (Non-Windows)
253+ if: matrix.os != 'windows-latest'
254+ run: |
255+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
256+ ghc --version
257+ stack --version
258+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
259+
260+ - name: Test (Windows)
261+ if: matrix.os == 'windows-latest'
262+ run: |
263+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
264+ ghc --version
265+ cabal --version
266+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
.github/workflows/haskell.ymldeleted−54
@@ -1,54 +0,0 @@
1−name: Haskell CI
2−
3−on:
4− push:
5− branches: [ master ]
6− pull_request:
7− branches: [ master ]
8−
9−jobs:
10− build:
11−
12− runs-on: ${{ matrix.os }}
13−
14− strategy:
15− fail-fast: false
16− matrix:
17− ghc: ['8.6.5', '8.8.3']
18− cabal: ['2.4', '3.0', '3.2']
19− os: [ubuntu-latest, macOS-latest, windows-latest]
20− exclude:
21− # GHC 8.8+ only works with cabal v3+
22− - ghc: 8.8.3
23− cabal: 2.4
24− # Appears to be buggy to build in windows
25− - ghc: 8.8.3
26− os: windows-latest
27−
28− steps:
29− - uses: actions/checkout@v2
30− - uses: actions/setup-haskell@v1.1
31− with:
32− ghc-version: ${{ matrix.ghc }}
33− cabal-version: ${{ matrix.cabal }}
34−
35− - name: Cache
36− uses: actions/cache@v1
37− env:
38− cache-name: cache-cabal
39− with:
40− path: ~/.cabal
41− key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
42− restore-keys: |
43− ${{ runner.os }}-build-${{ env.cache-name }}-
44− ${{ runner.os }}-build-
45− ${{ runner.os }}-
46−
47− - name: Install dependencies
48− run: |
49− cabal v2-update
50− cabal v2-build --only-dependencies --enable-tests --enable-benchmarks
51− - name: Build
52− run: cabal v2-build --enable-tests --enable-benchmarks all
53− - name: Run tests
54− run: cabal v2-test all
.travis.ymldeleted−61
@@ -1,61 +0,0 @@
1−sudo: true
2−language: haskell
3−
4−git:
5− depth: 5
6−
7−cabal: "3.0"
8−
9−cache:
10− directories:
11− - "$HOME/.cabal/store"
12− - "$HOME/.stack"
13− - "$TRAVIS_BUILD_DIR/.stack-work"
14−
15−matrix:
16− include:
17−
18− # Cabal
19− - ghc: 8.4.4
20− - ghc: 8.6.5
21− - ghc: 8.8.3
22−
23− # Stack
24− - ghc: 8.6.5
25− env: STACK_YAML="$TRAVIS_BUILD_DIR/stack.yaml"
26−
27−before_install:
28− - sudo apt-get install devscripts
29− - mkdir /tmp/minio /tmp/certs
30− - (cd /tmp/minio; wget https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
31− - (cd /tmp/certs; openssl req -newkey rsa:2048 -nodes -keyout private.key -x509 -days 36500 -out public.crt -subj "/C=US/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=localhost/emailAddress=email@example.com")
32− - sudo cp /tmp/certs/public.crt /usr/local/share/ca-certificates/
33− - sudo update-ca-certificates
34− - MINIO_ACCESS_KEY=minio MINIO_SECRET_KEY=minio123 /tmp/minio/minio server --quiet --certs-dir /tmp/certs data 2>&1 > minio.log &
35−
36−install:
37− - |
38− if [ -z "$STACK_YAML" ]; then
39− ghc --version
40− cabal --version
41− cabal new-update
42− cabal new-build --enable-tests --enable-benchmarks -fexamples
43− else
44− # install stack
45− curl -sSL https://get.haskellstack.org/ | sh
46−
47− # build project with stack
48− stack --version
49− stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --flag minio-hs:examples
50− fi
51−
52−script:
53− - |
54− if [ -z "$STACK_YAML" ]; then
55− MINIO_LOCAL=1 MINIO_SECURE=1 cabal new-test --enable-tests -flive-test
56− else
57− MINIO_LOCAL=1 MINIO_SECURE=1 stack test --system-ghc --flag minio-hs:live-test
58− fi
59−
60−notifications:
61− email: false
CHANGELOG.md+31
@@ -1,6 +1,37 @@
11Changelog
22==========
33
4+## Version 1.7.0 -- Unreleased
5+
6+* Fix data type `EventMessage` to not export partial fields (#179)
7+* Bump up min bound on time dep and fix deprecation warnings (#181)
8+* Add `dev` flag to cabal for building with warnings as errors (#182)
9+* Fix AWS region map (#185)
10+* Fix XML generator tests (#187)
11+* Add support for STS Assume Role API (#188)
12+
13+### Breaking changes in 1.7.0
14+
15+* `Credentials` type has been removed. Use `CredentialValue` instead.
16+* `Provider` type has been replaced with `CredentialLoader`.
17+* `EventMessage` data type is updated.
18+
19+## Version 1.6.0
20+
21+* HLint fixes - some types were changed to newtype (#173)
22+* Fix XML generation test for S3 SELECT (#161)
23+* Use region specific endpoints for AWS S3 in presigned Urls (#164)
24+* Replace protolude with relude and build with GHC 9.0.2 (#168)
25+* Support aeson 2 (#169)
26+* CI updates and code formatting changes with ormolu 0.5.0.0
27+
28+## Version 1.5.3
29+
30+* Fix windows build
31+* Fix support for Yandex Storage (#147)
32+* Fix for HEAD requests to S3/Minio (#155)
33+* Bump up some dependencies, new code formatting, Github CI, example fixes and other minor improvements.
34+
435## Version 1.5.2
536
637* Fix region `us-west-2` for AWS S3 (#139)
CNAMEnew file+1
@@ -0,0 +1 @@
1+minio-hs.min.io
README.md+50−29
@@ -1,10 +1,8 @@
1−# MinIO Client SDK for Haskell [![Build Status](https://travis-ci.org/minio/minio-hs.svg?branch=master)](https://travis-ci.org/minio/minio-hs)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
1+# MinIO Haskell Client SDK for Amazon S3 Compatible Cloud Storage [![CI](https://github.com/minio/minio-hs/actions/workflows/ci.yml/badge.svg)](https://github.com/minio/minio-hs/actions/workflows/ci.yml)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
22
3−The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and Amazon S3 compatible object storage server.
3+The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
44
5−## Minimum Requirements
6−
7−- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
5+This guide assumes that you have a working [Haskell development environment](https://www.haskell.org/downloads/).
86
97## Installation
108
@@ -12,20 +10,35 @@ The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.
1210
1311Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
1412
15−### Try it out directly with `ghci`
13+### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
14+
15+#### For a cabal based environment
16+
17+Download the library source and change to the extracted directory:
18+
19+``` sh
20+$ cabal get minio-hs
21+$ cd minio-hs-1.6.0/ # directory name could be different
22+```
23+
24+Then load the `ghci` REPL environment with the library and browse the available APIs:
25+
26+``` sh
27+$ cabal repl
28+ghci> :browse Network.Minio
29+```
30+
31+#### For a stack based environment
1632
1733From your home folder or any non-haskell project directory, just run:
1834
1935```sh
20−
2136stack install minio-hs
22−
2337```
2438
2539Then start an interpreter session and browse the available APIs with:
2640
2741```sh
28−
2942$ stack ghci
3043> :browse Network.Minio
3144```
@@ -134,44 +147,52 @@ main = do
134147
135148### Development
136149
137−To setup:
150+#### Download the source
138151
139152```sh
140−git clone https://github.com/minio/minio-hs.git
141−
142−cd minio-hs/
153+$ git clone https://github.com/minio/minio-hs.git
154+$ cd minio-hs/
155+```
143156
144−stack install
145−```
157+#### Build the package:
146158
147−Tests can be run with:
159+With `cabal`:
148160
149161```sh
162+$ # Configure cabal for development enabling all optional flags defined by the package.
163+$ cabal configure --enable-tests --test-show-details=direct -fexamples -fdev -flive-test
164+$ cabal build
165+```
150166
151−stack test
167+With `stack`:
152168
169+``` sh
170+$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
153171```
172+#### Running tests:
154173
155−A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
174+A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000` with the credentials `access_key=minio` and `secret_key=minio123`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
156175
157−To run the live server tests, set a build flag as shown below:
176+With `cabal`:
158177
159178```sh
179+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
180+$ cabal test
181+```
160182
161−stack test --flag minio-hs:live-test
162−
163−# OR against a local MinIO server with:
164−
165−MINIO_LOCAL=1 stack test --flag minio-hs:live-test
183+With `stack`:
166184
185+``` sh
186+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
187+stack test --flag minio-hs:live-test --flag minio-hs:dev
167188```
168189
169−The configured CI system always runs both test-suites for every change.
190+This will run all the test suites.
170191
171−Documentation can be locally built with:
192+#### Building documentation:
172193
173194```sh
174−
175−stack haddock
176−
195+$ cabal haddock
196+$ # OR
197+$ stack haddock
177198```
Setup.hsdeleted−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−2
@@ -19,7 +19,6 @@
1919{-# LANGUAGE OverloadedStrings #-}
2020{-# LANGUAGE ScopedTypeVariables #-}
2121
22−import Data.Monoid ((<>))
2322import Data.Text (pack)
2423import Network.Minio
2524import Options.Applicative
@@ -71,5 +70,5 @@ main = do
7170 fPutObject bucket object filepath defaultPutObjectOptions
7271
7372 case res of
74− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
73+ Left e -> putStrLn $ "file upload failed due to " ++ show e
7574 Right () -> putStrLn "file upload succeeded."
examples/GetConfig.hs+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+3−3
@@ -34,9 +34,9 @@ main = do
3434 -- Performs a recursive listing of incomplete uploads under bucket "test"
3535 -- on a local minio server.
3636 res <-
37− runMinio minioPlayCI
38− $ runConduit
39− $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
37+ runMinio minioPlayCI $
38+ runConduit $
39+ listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
4040 print res
4141
4242{-
examples/ListObjects.hs+3−3
@@ -34,9 +34,9 @@ main = do
3434 -- Performs a recursive listing of all objects under bucket "test"
3535 -- on play.min.io.
3636 res <-
37− runMinio minioPlayCI
38− $ runConduit
39− $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
37+ runMinio minioPlayCI $
38+ runConduit $
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+7−5
@@ -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
@@ -73,13 +73,15 @@ main = do
7373 ]
7474 formOptions = B.intercalate " " $ map formFn $ H.toList formData
7575
76− return $ B.intercalate " " $
77− ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
76+ return $
77+ B.intercalate
78+ " "
79+ ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
7880
7981 case res of
80− Left e -> putStrLn $ "post-policy error: " ++ (show e)
82+ Left e -> putStrLn $ "post-policy error: " ++ show e
8183 Right cmd -> do
82− 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"
8385
8486 -- print the generated curl command
8587 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+71−15
@@ -1,6 +1,6 @@
1−cabal-version: 2.2
1+cabal-version: 2.4
22name: minio-hs
3−version: 1.5.2
3+version: 1.7.0
44synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
55 storage.
66description: The MinIO Haskell client library provides simple APIs to
@@ -14,29 +14,70 @@ maintainer: dev@min.io
1414category: Network, AWS, Object Storage
1515build-type: Simple
1616stability: Experimental
17−extra-source-files:
17+extra-doc-files:
1818 CHANGELOG.md
1919 CONTRIBUTING.md
2020 docs/API.md
21− examples/*.hs
2221 README.md
22+extra-source-files:
23+ examples/*.hs
2324 stack.yaml
25+tested-with: GHC == 8.10.7
26+ , GHC == 9.0.2
27+ , GHC == 9.2.8
28+ , GHC == 9.4.8
29+ , GHC == 9.6.5
30+ , GHC == 9.8.2
2431
32+source-repository head
33+ type: git
34+ location: https://github.com/minio/minio-hs.git
35+
36+Flag dev
37+ Description: Build package in development mode
38+ Default: False
39+ Manual: True
2540
2641common base-settings
2742 ghc-options: -Wall
43+ -Wcompat
44+ -Widentities
45+ -Wincomplete-uni-patterns
46+ -Wincomplete-record-updates
47+ -haddock
48+ if impl(ghc >= 8.0)
49+ ghc-options: -Wredundant-constraints
50+ if impl(ghc >= 8.2)
51+ ghc-options: -fhide-source-paths
52+ if impl(ghc >= 8.4)
53+ ghc-options: -Wpartial-fields
54+ -- -Wmissing-export-lists
55+ if impl(ghc >= 8.8)
56+ ghc-options: -Wmissing-deriving-strategies
57+ -Werror=missing-deriving-strategies
58+ -- if impl(ghc >= 8.10)
59+ -- ghc-options: -Wunused-packages -- disabled due to bug related to mixin config
60+ if impl(ghc >= 9.0)
61+ ghc-options: -Winvalid-haddock
62+ if impl(ghc >= 9.2)
63+ ghc-options: -Wredundant-bang-patterns
64+ if flag(dev)
65+ ghc-options: -Werror
66+
2867 default-language: Haskell2010
68+
2969 default-extensions: BangPatterns
70+ , DerivingStrategies
3071 , FlexibleContexts
3172 , FlexibleInstances
73+ , LambdaCase
3274 , MultiParamTypeClasses
3375 , MultiWayIf
34− , NoImplicitPrelude
3576 , OverloadedStrings
3677 , RankNTypes
3778 , ScopedTypeVariables
38− , TypeFamilies
3979 , TupleSections
80+
4081 other-modules: Lib.Prelude
4182 , Network.Minio.API
4283 , Network.Minio.APICommon
@@ -54,22 +95,31 @@ common base-settings
5495 , Network.Minio.Utils
5596 , Network.Minio.XmlGenerator
5697 , Network.Minio.XmlParser
98+ , Network.Minio.XmlCommon
5799 , Network.Minio.JsonParser
100+ , Network.Minio.Credentials.Types
101+ , Network.Minio.Credentials.AssumeRole
102+ , Network.Minio.Credentials
103+
104+ mixins: base hiding (Prelude)
105+ , relude (Relude as Prelude)
106+ , relude
107+
58108 build-depends: base >= 4.7 && < 5
59− , protolude >= 0.3 && < 0.4
60− , aeson >= 1.2
109+ , relude >= 0.7 && < 2
110+ , aeson >= 1.2 && < 3
61111 , base64-bytestring >= 1.0
62112 , binary >= 0.8.5.0
63113 , bytestring >= 0.10
64114 , case-insensitive >= 1.2
65115 , conduit >= 1.3
66116 , conduit-extra >= 1.3
67− , connection
117+ , crypton-connection
68118 , cryptonite >= 0.25
69119 , cryptonite-conduit >= 0.2
120+ , data-default-class
70121 , digest >= 0.0.1
71122 , directory
72− , exceptions
73123 , filepath >= 1.4
74124 , http-client >= 0.5
75125 , http-client-tls
@@ -77,11 +127,12 @@ common base-settings
77127 , http-types >= 0.12
78128 , ini
79129 , memory >= 0.14
80− , raw-strings-qq >= 1
130+ , network-uri
81131 , resourcet >= 1.2
82132 , retry
83133 , text >= 1.2
84− , time >= 1.8
134+ , time >= 1.9
135+ , time-units ^>= 1.0.0
85136 , transformers >= 0.5
86137 , unliftio >= 0.2 && < 0.3
87138 , unliftio-core >= 0.2 && < 0.3
@@ -115,7 +166,9 @@ test-suite minio-hs-live-server-test
115166 , Network.Minio.Utils.Test
116167 , Network.Minio.XmlGenerator.Test
117168 , Network.Minio.XmlParser.Test
169+ , Network.Minio.Credentials
118170 build-depends: minio-hs
171+ , raw-strings-qq
119172 , tasty
120173 , tasty-hunit
121174 , tasty-quickcheck
@@ -130,6 +183,7 @@ test-suite minio-hs-test
130183 hs-source-dirs: test, src
131184 main-is: Spec.hs
132185 build-depends: minio-hs
186+ , raw-strings-qq
133187 , QuickCheck
134188 , tasty
135189 , tasty-hunit
@@ -146,6 +200,7 @@ test-suite minio-hs-test
146200 , Network.Minio.Utils.Test
147201 , Network.Minio.XmlGenerator.Test
148202 , Network.Minio.XmlParser.Test
203+ , Network.Minio.Credentials
149204
150205Flag examples
151206 Description: Build the examples
@@ -292,6 +347,7 @@ executable SetConfig
292347 scope: private
293348 main-is: SetConfig.hs
294349
295−source-repository head
296− type: git
297− location: https://github.com/minio/minio-hs
350+executable AssumeRole
351+ import: examples-settings
352+ scope: private
353+ main-is: AssumeRole.hs
src/Lib/Prelude.hs+6−10
@@ -20,6 +20,7 @@ module Lib.Prelude
2020 showBS,
2121 toStrictBS,
2222 fromStrictBS,
23+ lastMay,
2324 )
2425where
2526
@@ -29,14 +30,6 @@ import Data.Time as Exports
2930 ( UTCTime (..),
3031 diffUTCTime,
3132 )
32−import Protolude as Exports hiding
33− ( Handler,
34− catch,
35− catches,
36− throwIO,
37− try,
38− yield,
39− )
4033import UnliftIO as Exports
4134 ( Handler,
4235 catch,
@@ -49,11 +42,14 @@ import UnliftIO as Exports
4942both :: (a -> b) -> (a, a) -> (b, b)
5043both f (a, b) = (f a, f b)
5144
52−showBS :: Show a => a -> ByteString
53−showBS a = toUtf8 (show a :: Text)
45+showBS :: (Show a) => a -> ByteString
46+showBS a = encodeUtf8 (show a :: Text)
5447
5548toStrictBS :: LByteString -> ByteString
5649toStrictBS = LB.toStrict
5750
5851fromStrictBS :: ByteString -> LByteString
5952fromStrictBS = LB.fromStrict
53+
54+lastMay :: [a] -> Maybe a
55+lastMay a = last <$> nonEmpty a
src/Network/Minio.hs+22−8
@@ -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,7 +58,17 @@ 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
71+
5872 ----------------
5973
6074 -- | The Minio Monad provides connection-reuse, bucket-location
@@ -224,15 +238,15 @@ This module exports the high-level MinIO API for object storage.
224238import qualified Data.Conduit as C
225239import qualified Data.Conduit.Binary as CB
226240import qualified Data.Conduit.Combinators as CC
227−import Lib.Prelude
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+124−51
@@ -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.
@@ -19,12 +19,14 @@ module Network.Minio.API
1919 S3ReqInfo (..),
2020 runMinio,
2121 executeRequest,
22+ buildRequest,
2223 mkStreamRequest,
2324 getLocation,
2425 isValidBucketName,
2526 checkBucketNameValidity,
2627 isValidObjectName,
2728 checkObjectNameValidity,
29+ requestSTSCredential,
2830 )
2931where
3032
@@ -40,11 +42,15 @@ import qualified Data.HashMap.Strict as H
4042import qualified Data.Text as T
4143import qualified Data.Time.Clock as Time
4244import Lib.Prelude
45+import Network.HTTP.Client (defaultManagerSettings)
46+import qualified Network.HTTP.Client as NClient
4347import Network.HTTP.Conduit (Response)
4448import qualified Network.HTTP.Conduit as NC
49+import Network.HTTP.Types (simpleQueryToQuery)
4550import qualified Network.HTTP.Types as HT
4651import Network.HTTP.Types.Header (hHost)
4752import Network.Minio.APICommon
53+import Network.Minio.Credentials
4854import Network.Minio.Data
4955import Network.Minio.Errors
5056import Network.Minio.Sign.V4
@@ -78,6 +84,7 @@ discoverRegion ri = runMaybeT $ do
7884 return
7985 regionMay
8086
87+-- | Returns the region to be used for the request.
8188getRegion :: S3ReqInfo -> Minio (Maybe Region)
8289getRegion ri = do
8390 ci <- asks mcConnInfo
@@ -85,10 +92,10 @@ getRegion ri = do
8592 -- getService/makeBucket/getLocation -- don't need location
8693 if
8794 | not $ riNeedsLocation ri ->
88− return $ Just $ connectRegion ci
95+ return $ Just $ connectRegion ci
8996 -- if autodiscovery of location is disabled by user
9097 | not $ connectAutoDiscoverRegion ci ->
91− return $ Just $ connectRegion ci
98+ return $ Just $ connectRegion ci
9299 -- discover the region for the request
93100 | otherwise -> discoverRegion ri
94101
@@ -104,6 +111,56 @@ getRegionHost r = do
104111 (H.lookup r awsRegionMap)
105112 else return $ connectHost ci
106113
114+-- | Computes the appropriate host, path and region for the request.
115+--
116+-- For AWS, always use virtual bucket style, unless bucket has periods. For
117+-- MinIO and other non-AWS, default to path style.
118+getHostPathRegion :: S3ReqInfo -> Minio (Text, ByteString, Maybe Region)
119+getHostPathRegion ri = do
120+ ci <- asks mcConnInfo
121+ regionMay <- getRegion ri
122+ case riBucket ri of
123+ Nothing ->
124+ -- Implies a ListBuckets request.
125+ return (connectHost ci, "/", regionMay)
126+ Just bucket -> do
127+ regionHost <- case regionMay of
128+ Nothing -> return $ connectHost ci
129+ Just "" -> return $ connectHost ci
130+ Just r -> getRegionHost r
131+ let pathStyle =
132+ ( regionHost,
133+ getS3Path (riBucket ri) (riObject ri),
134+ regionMay
135+ )
136+ virtualStyle =
137+ ( bucket <> "." <> regionHost,
138+ encodeUtf8 $ "/" <> fromMaybe "" (riObject ri),
139+ regionMay
140+ )
141+ ( if isAWSConnectInfo ci
142+ then
143+ return $
144+ if bucketHasPeriods bucket
145+ then pathStyle
146+ else virtualStyle
147+ else return pathStyle
148+ )
149+
150+-- | requestSTSCredential requests temporary credentials using the Security Token
151+-- Service API. The returned credential will include a populated 'SessionToken'
152+-- and an 'ExpiryTime'.
153+requestSTSCredential :: (STSCredentialProvider p) => p -> IO (CredentialValue, ExpiryTime)
154+requestSTSCredential p = do
155+ endpoint <- maybe (throwIO $ MErrValidation MErrVSTSEndpointNotFound) return $ getSTSEndpoint p
156+ let endPt = NC.parseRequest_ $ toString endpoint
157+ settings
158+ | NC.secure endPt = NC.tlsManagerSettings
159+ | otherwise = defaultManagerSettings
160+
161+ mgr <- NC.newManager settings
162+ liftIO $ retrieveSTSCredentials p ("", 0, False) mgr
163+
107164buildRequest :: S3ReqInfo -> Minio NC.Request
108165buildRequest ri = do
109166 maybe (return ()) checkBucketNameValidity $ riBucket ri
@@ -111,17 +168,15 @@ buildRequest ri = do
111168
112169 ci <- asks mcConnInfo
113170
114− regionMay <- getRegion ri
115−
116− regionHost <- maybe (return $ connectHost ci) getRegionHost regionMay
171+ (host, path, regionMay) <- getHostPathRegion ri
117172
118− let ri' =
173+ let ci' = ci {connectHost = host}
174+ hostHeader = (hHost, getHostAddr ci')
175+ ri' =
119176 ri
120177 { riHeaders = hostHeader : riHeaders ri,
121178 riRegion = regionMay
122179 }
123− ci' = ci {connectHost = regionHost}
124− hostHeader = (hHost, getHostAddr ci')
125180 -- Does not contain body and auth info.
126181 baseRequest =
127182 NC.defaultRequest
@@ -129,24 +184,31 @@ buildRequest ri = do
129184 NC.secure = connectIsSecure ci',
130185 NC.host = encodeUtf8 $ connectHost ci',
131186 NC.port = connectPort ci',
132− NC.path = getS3Path (riBucket ri') (riObject ri'),
187+ NC.path = path,
133188 NC.requestHeaders = riHeaders ri',
134189 NC.queryString = HT.renderQuery False $ riQueryParams ri'
135190 }
136191
137192 timeStamp <- liftIO Time.getCurrentTime
138193
194+ mgr <- asks mcConnManager
195+ cv <- liftIO $ getCredential (connectCreds ci') (getEndpoint ci') mgr
196+
139197 let sp =
140198 SignParams
141− (connectAccessKey ci')
142− (connectSecretKey ci')
199+ (coerce $ cvAccessKey cv)
200+ (coerce $ cvSecretKey cv)
201+ (coerce $ cvSessionToken cv)
202+ ServiceS3
143203 timeStamp
144204 (riRegion ri')
145− Nothing
205+ (riPresignExpirySecs ri')
146206 Nothing
147207
148208 -- Cases to handle:
149209 --
210+ -- 0. Handle presign URL case.
211+ --
150212 -- 1. Connection is secure: use unsigned payload
151213 --
152214 -- 2. Insecure connection, streaming signature is enabled via use of
@@ -155,40 +217,51 @@ buildRequest ri = do
155217 -- 3. Insecure connection, non-conduit payload: compute payload
156218 -- sha256hash, buffer request in memory and perform request.
157219
158− -- case 2 from above.
159220 if
160− | isStreamingPayload (riPayload ri')
161− && (not $ connectIsSecure ci') -> do
162− (pLen, pSrc) <- case riPayload ri of
163− PayloadC l src -> return (l, src)
164− _ -> throwIO MErrVUnexpectedPayload
165− let reqFn = signV4Stream pLen sp baseRequest
166− return $ reqFn pSrc
167− | otherwise -> do
168− -- case 1 described above.
169− sp' <-
170− if
171− | connectIsSecure ci' -> return sp
172− -- case 3 described above.
173− | otherwise -> do
174− pHash <- getPayloadSHA256Hash $ riPayload ri'
175− return $ sp {spPayloadHash = Just pHash}
221+ | isJust (riPresignExpirySecs ri') ->
222+ -- case 0 from above.
223+ do
224+ let signPairs = signV4QueryParams sp baseRequest
225+ qpToAdd = simpleQueryToQuery signPairs
226+ existingQueryParams = HT.parseQuery (NC.queryString baseRequest)
227+ updatedQueryParams = existingQueryParams ++ qpToAdd
228+ return $ NClient.setQueryString updatedQueryParams baseRequest
229+ | isStreamingPayload (riPayload ri') && not (connectIsSecure ci') ->
230+ -- case 2 from above.
231+ do
232+ (pLen, pSrc) <- case riPayload ri of
233+ PayloadC l src -> return (l, src)
234+ _ -> throwIO MErrVUnexpectedPayload
235+ let reqFn = signV4Stream pLen sp baseRequest
236+ return $ reqFn pSrc
237+ | otherwise ->
238+ do
239+ sp' <-
240+ ( if connectIsSecure ci'
241+ then -- case 1 described above.
242+ return sp
243+ else
244+ ( -- case 3 described above.
245+ do
246+ pHash <- getPayloadSHA256Hash $ riPayload ri'
247+ return $ sp {spPayloadHash = Just pHash}
248+ )
249+ )
176250
177− let signHeaders = signV4 sp' baseRequest
178− return $
179− baseRequest
180− { NC.requestHeaders =
181− NC.requestHeaders baseRequest
182− ++ mkHeaderFromPairs signHeaders,
183− NC.requestBody = getRequestBody (riPayload ri')
184− }
251+ let signHeaders = signV4 sp' baseRequest
252+ return $
253+ baseRequest
254+ { NC.requestHeaders =
255+ NC.requestHeaders baseRequest ++ signHeaders,
256+ NC.requestBody = getRequestBody (riPayload ri')
257+ }
185258
186259retryAPIRequest :: Minio a -> Minio a
187260retryAPIRequest apiCall = do
188261 resE <-
189− retrying retryPolicy (const shouldRetry)
190− $ const
191− $ try apiCall
262+ retrying retryPolicy (const shouldRetry) $
263+ const $
264+ try apiCall
192265 either throwIO return resE
193266 where
194267 -- Retry using the full-jitter backoff method for up to 10 mins
@@ -235,8 +308,8 @@ isValidBucketName bucket =
235308 not
236309 ( or
237310 [ len < 3 || len > 63,
238− or (map labelCheck labels),
239− or (map labelCharsCheck labels),
311+ any labelCheck labels,
312+ any labelCharsCheck labels,
240313 isIPCheck
241314 ]
242315 )
@@ -264,18 +337,18 @@ isValidBucketName bucket =
264337 isIPCheck = and labelAsNums && length labelAsNums == 4
265338
266339-- Throws exception iff bucket name is invalid according to AWS rules.
267−checkBucketNameValidity :: MonadIO m => Bucket -> m ()
340+checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
268341checkBucketNameValidity bucket =
269− when (not $ isValidBucketName bucket)
270− $ throwIO
271− $ MErrVInvalidBucketName bucket
342+ unless (isValidBucketName bucket) $
343+ throwIO $
344+ MErrVInvalidBucketName bucket
272345
273346isValidObjectName :: Object -> Bool
274347isValidObjectName object =
275348 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
276349
277−checkObjectNameValidity :: MonadIO m => Object -> m ()
350+checkObjectNameValidity :: (MonadIO m) => Object -> m ()
278351checkObjectNameValidity object =
279− when (not $ isValidObjectName object)
280− $ throwIO
281− $ MErrVInvalidObjectName object
352+ unless (isValidObjectName object) $
353+ throwIO $
354+ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hs+9−1
@@ -20,6 +20,7 @@ import qualified Conduit as C
2020import qualified Data.ByteString as BS
2121import qualified Data.ByteString.Lazy as LB
2222import Data.Conduit.Binary (sourceHandleRange)
23+import qualified Data.Text as T
2324import Lib.Prelude
2425import qualified Network.HTTP.Conduit as NC
2526import qualified Network.HTTP.Types as HT
@@ -45,7 +46,7 @@ getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
4546getRequestBody :: Payload -> NC.RequestBody
4647getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
4748getRequestBody (PayloadH h off size) =
48− NC.requestBodySource (fromIntegral size) $
49+ NC.requestBodySource size $
4950 sourceHandleRange
5051 h
5152 (return . fromIntegral $ off)
@@ -70,3 +71,10 @@ mkStreamingPayload payload =
7071isStreamingPayload :: Payload -> Bool
7172isStreamingPayload (PayloadC _ _) = True
7273isStreamingPayload _ = False
74+
75+-- | Checks if the connect info is for Amazon S3.
76+isAWSConnectInfo :: ConnectInfo -> Bool
77+isAWSConnectInfo ci = ".amazonaws.com" `T.isSuffixOf` connectHost ci
78+
79+bucketHasPeriods :: Bucket -> Bool
80+bucketHasPeriods b = isJust $ T.find (== '.') b
src/Network/Minio/AdminAPI.hs+163−97
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2018-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -16,7 +16,8 @@
1616
1717module Network.Minio.AdminAPI
1818 ( -- * MinIO Admin API
19− --------------------
19+
20+ --------------------
2021
2122 -- | Provides MinIO admin API and related types. It is in
2223 -- experimental state.
@@ -52,10 +53,7 @@ module Network.Minio.AdminAPI
5253where
5354
5455import Data.Aeson
55− ( (.:),
56− (.:?),
57− (.=),
58− FromJSON,
56+ ( FromJSON,
5957 ToJSON,
6058 Value (Object),
6159 eitherDecode,
@@ -66,6 +64,9 @@ import Data.Aeson
6664 toJSON,
6765 withObject,
6866 withText,
67+ (.:),
68+ (.:?),
69+ (.=),
6970 )
7071import qualified Data.Aeson as A
7172import Data.Aeson.Types (typeMismatch)
@@ -79,6 +80,7 @@ import qualified Network.HTTP.Conduit as NC
7980import qualified Network.HTTP.Types as HT
8081import Network.HTTP.Types.Header (hHost)
8182import Network.Minio.APICommon
83+import Network.Minio.Credentials
8284import Network.Minio.Data
8385import Network.Minio.Errors
8486import Network.Minio.Sign.V4
@@ -89,20 +91,23 @@ data DriveInfo = DriveInfo
8991 diEndpoint :: Text,
9092 diState :: Text
9193 }
92− deriving (Eq, Show)
94+ deriving stock (Show, Eq)
9395
9496instance FromJSON DriveInfo where
9597 parseJSON = withObject "DriveInfo" $ \v ->
9698 DriveInfo
97− <$> v .: "uuid"
98− <*> v .: "endpoint"
99− <*> v .: "state"
99+ <$> v
100+ .: "uuid"
101+ <*> v
102+ .: "endpoint"
103+ <*> v
104+ .: "state"
100105
101106data StorageClass = StorageClass
102107 { scParity :: Int,
103108 scData :: Int
104109 }
105− deriving (Eq, Show)
110+ deriving stock (Show, Eq)
106111
107112data ErasureInfo = ErasureInfo
108113 { eiOnlineDisks :: Int,
@@ -111,7 +116,7 @@ data ErasureInfo = ErasureInfo
111116 eiReducedRedundancy :: StorageClass,
112117 eiSets :: [[DriveInfo]]
113118 }
114− deriving (Eq, Show)
119+ deriving stock (Show, Eq)
115120
116121instance FromJSON ErasureInfo where
117122 parseJSON = withObject "ErasureInfo" $ \v -> do
@@ -119,19 +124,23 @@ instance FromJSON ErasureInfo where
119124 offlineDisks <- v .: "OfflineDisks"
120125 stdClass <-
121126 StorageClass
122− <$> v .: "StandardSCData"
123− <*> v .: "StandardSCParity"
127+ <$> v
128+ .: "StandardSCData"
129+ <*> v
130+ .: "StandardSCParity"
124131 rrClass <-
125132 StorageClass
126− <$> v .: "RRSCData"
127− <*> v .: "RRSCParity"
133+ <$> v
134+ .: "RRSCData"
135+ <*> v
136+ .: "RRSCParity"
128137 sets <- v .: "Sets"
129138 return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
130139
131140data Backend
132141 = BackendFS
133142 | BackendErasure ErasureInfo
134− deriving (Eq, Show)
143+ deriving stock (Show, Eq)
135144
136145instance FromJSON Backend where
137146 parseJSON = withObject "Backend" $ \v -> do
@@ -145,13 +154,15 @@ data ConnStats = ConnStats
145154 { csTransferred :: Int64,
146155 csReceived :: Int64
147156 }
148− deriving (Eq, Show)
157+ deriving stock (Show, Eq)
149158
150159instance FromJSON ConnStats where
151160 parseJSON = withObject "ConnStats" $ \v ->
152161 ConnStats
153− <$> v .: "transferred"
154− <*> v .: "received"
162+ <$> v
163+ .: "transferred"
164+ <*> v
165+ .: "received"
155166
156167data ServerProps = ServerProps
157168 { spUptime :: NominalDiffTime,
@@ -160,7 +171,7 @@ data ServerProps = ServerProps
160171 spRegion :: Text,
161172 spSqsArns :: [Text]
162173 }
163− deriving (Eq, Show)
174+ deriving stock (Show, Eq)
164175
165176instance FromJSON ServerProps where
166177 parseJSON = withObject "SIServer" $ \v -> do
@@ -176,25 +187,29 @@ data StorageInfo = StorageInfo
176187 { siUsed :: Int64,
177188 siBackend :: Backend
178189 }
179− deriving (Eq, Show)
190+ deriving stock (Show, Eq)
180191
181192instance FromJSON StorageInfo where
182193 parseJSON = withObject "StorageInfo" $ \v ->
183194 StorageInfo
184− <$> v .: "Used"
185− <*> v .: "Backend"
195+ <$> v
196+ .: "Used"
197+ <*> v
198+ .: "Backend"
186199
187200data CountNAvgTime = CountNAvgTime
188201 { caCount :: Int64,
189202 caAvgDuration :: Text
190203 }
191− deriving (Eq, Show)
204+ deriving stock (Show, Eq)
192205
193206instance FromJSON CountNAvgTime where
194207 parseJSON = withObject "CountNAvgTime" $ \v ->
195208 CountNAvgTime
196− <$> v .: "count"
197− <*> v .: "avgDuration"
209+ <$> v
210+ .: "count"
211+ <*> v
212+ .: "avgDuration"
198213
199214data HttpStats = HttpStats
200215 { hsTotalHeads :: CountNAvgTime,
@@ -208,21 +223,31 @@ data HttpStats = HttpStats
208223 hsTotalDeletes :: CountNAvgTime,
209224 hsSuccessDeletes :: CountNAvgTime
210225 }
211− deriving (Eq, Show)
226+ deriving stock (Show, Eq)
212227
213228instance FromJSON HttpStats where
214229 parseJSON = withObject "HttpStats" $ \v ->
215230 HttpStats
216− <$> v .: "totalHEADs"
217− <*> v .: "successHEADs"
218− <*> v .: "totalGETs"
219− <*> v .: "successGETs"
220− <*> v .: "totalPUTs"
221− <*> v .: "successPUTs"
222− <*> v .: "totalPOSTs"
223− <*> v .: "successPOSTs"
224− <*> v .: "totalDELETEs"
225− <*> 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"
226251
227252data SIData = SIData
228253 { sdStorage :: StorageInfo,
@@ -230,47 +255,56 @@ data SIData = SIData
230255 sdHttpStats :: HttpStats,
231256 sdProps :: ServerProps
232257 }
233− deriving (Eq, Show)
258+ deriving stock (Show, Eq)
234259
235260instance FromJSON SIData where
236261 parseJSON = withObject "SIData" $ \v ->
237262 SIData
238− <$> v .: "storage"
239− <*> v .: "network"
240− <*> v .: "http"
241− <*> v .: "server"
263+ <$> v
264+ .: "storage"
265+ <*> v
266+ .: "network"
267+ <*> v
268+ .: "http"
269+ <*> v
270+ .: "server"
242271
243272data ServerInfo = ServerInfo
244273 { siError :: Text,
245274 siAddr :: Text,
246275 siData :: SIData
247276 }
248− deriving (Eq, Show)
277+ deriving stock (Show, Eq)
249278
250279instance FromJSON ServerInfo where
251280 parseJSON = withObject "ServerInfo" $ \v ->
252281 ServerInfo
253− <$> v .: "error"
254− <*> v .: "addr"
255− <*> v .: "data"
282+ <$> v
283+ .: "error"
284+ <*> v
285+ .: "addr"
286+ <*> v
287+ .: "data"
256288
257289data ServerVersion = ServerVersion
258290 { svVersion :: Text,
259291 svCommitId :: Text
260292 }
261− deriving (Eq, Show)
293+ deriving stock (Show, Eq)
262294
263295instance FromJSON ServerVersion where
264296 parseJSON = withObject "ServerVersion" $ \v ->
265297 ServerVersion
266− <$> v .: "version"
267− <*> v .: "commitID"
298+ <$> v
299+ .: "version"
300+ <*> v
301+ .: "commitID"
268302
269303data ServiceStatus = ServiceStatus
270304 { ssVersion :: ServerVersion,
271305 ssUptime :: NominalDiffTime
272306 }
273− deriving (Eq, Show)
307+ deriving stock (Show, Eq)
274308
275309instance FromJSON ServiceStatus where
276310 parseJSON = withObject "ServiceStatus" $ \v -> do
@@ -282,7 +316,7 @@ instance FromJSON ServiceStatus where
282316data ServiceAction
283317 = ServiceActionRestart
284318 | ServiceActionStop
285− deriving (Eq, Show)
319+ deriving stock (Show, Eq)
286320
287321instance ToJSON ServiceAction where
288322 toJSON a = object ["action" .= serviceActionToText a]
@@ -300,20 +334,23 @@ data HealStartResp = HealStartResp
300334 hsrClientAddr :: Text,
301335 hsrStartTime :: UTCTime
302336 }
303− deriving (Eq, Show)
337+ deriving stock (Show, Eq)
304338
305339instance FromJSON HealStartResp where
306340 parseJSON = withObject "HealStartResp" $ \v ->
307341 HealStartResp
308− <$> v .: "clientToken"
309− <*> v .: "clientAddress"
310− <*> v .: "startTime"
342+ <$> v
343+ .: "clientToken"
344+ <*> v
345+ .: "clientAddress"
346+ <*> v
347+ .: "startTime"
311348
312349data HealOpts = HealOpts
313350 { hoRecursive :: Bool,
314351 hoDryRun :: Bool
315352 }
316− deriving (Eq, Show)
353+ deriving stock (Show, Eq)
317354
318355instance ToJSON HealOpts where
319356 toJSON (HealOpts r d) =
@@ -324,15 +361,17 @@ instance ToJSON HealOpts where
324361instance FromJSON HealOpts where
325362 parseJSON = withObject "HealOpts" $ \v ->
326363 HealOpts
327− <$> v .: "recursive"
328− <*> v .: "dryRun"
364+ <$> v
365+ .: "recursive"
366+ <*> v
367+ .: "dryRun"
329368
330369data HealItemType
331370 = HealItemMetadata
332371 | HealItemBucket
333372 | HealItemBucketMetadata
334373 | HealItemObject
335− deriving (Eq, Show)
374+ deriving stock (Show, Eq)
336375
337376instance FromJSON HealItemType where
338377 parseJSON = withText "HealItemType" $ \v -> case v of
@@ -347,26 +386,31 @@ data NodeSummary = NodeSummary
347386 nsErrSet :: Bool,
348387 nsErrMessage :: Text
349388 }
350− deriving (Eq, Show)
389+ deriving stock (Show, Eq)
351390
352391instance FromJSON NodeSummary where
353392 parseJSON = withObject "NodeSummary" $ \v ->
354393 NodeSummary
355− <$> v .: "name"
356− <*> v .: "errSet"
357− <*> v .: "errMsg"
394+ <$> v
395+ .: "name"
396+ <*> v
397+ .: "errSet"
398+ <*> v
399+ .: "errMsg"
358400
359401data SetConfigResult = SetConfigResult
360402 { scrStatus :: Bool,
361403 scrNodeSummary :: [NodeSummary]
362404 }
363− deriving (Eq, Show)
405+ deriving stock (Show, Eq)
364406
365407instance FromJSON SetConfigResult where
366408 parseJSON = withObject "SetConfigResult" $ \v ->
367409 SetConfigResult
368− <$> v .: "status"
369− <*> v .: "nodeResults"
410+ <$> v
411+ .: "status"
412+ <*> v
413+ .: "nodeResults"
370414
371415data HealResultItem = HealResultItem
372416 { hriResultIdx :: Int,
@@ -382,21 +426,31 @@ data HealResultItem = HealResultItem
382426 hriBefore :: [DriveInfo],
383427 hriAfter :: [DriveInfo]
384428 }
385− deriving (Eq, Show)
429+ deriving stock (Show, Eq)
386430
387431instance FromJSON HealResultItem where
388432 parseJSON = withObject "HealResultItem" $ \v ->
389433 HealResultItem
390− <$> v .: "resultId"
391− <*> v .: "type"
392− <*> v .: "bucket"
393− <*> v .: "object"
394− <*> v .: "detail"
395− <*> v .:? "parityBlocks"
396− <*> v .:? "dataBlocks"
397− <*> v .: "diskCount"
398− <*> v .: "setCount"
399− <*> 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"
400454 <*> ( do
401455 before <- v .: "before"
402456 before .: "drives"
@@ -414,26 +468,34 @@ data HealStatus = HealStatus
414468 hsFailureDetail :: Maybe Text,
415469 hsItems :: Maybe [HealResultItem]
416470 }
417− deriving (Eq, Show)
471+ deriving stock (Show, Eq)
418472
419473instance FromJSON HealStatus where
420474 parseJSON = withObject "HealStatus" $ \v ->
421475 HealStatus
422− <$> v .: "Summary"
423− <*> v .: "StartTime"
424− <*> v .: "Settings"
425− <*> v .: "NumDisks"
426− <*> v .:? "Detail"
427− <*> 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"
428488
429489healPath :: Maybe Bucket -> Maybe Text -> ByteString
430490healPath bucket prefix = do
431− if (isJust bucket)
491+ if isJust bucket
432492 then
433493 encodeUtf8 $
434− "v1/heal/" <> fromMaybe "" bucket <> "/"
494+ "v1/heal/"
495+ <> fromMaybe "" bucket
496+ <> "/"
435497 <> fromMaybe "" prefix
436− else encodeUtf8 $ "v1/heal/"
498+ else encodeUtf8 ("v1/heal/" :: Text)
437499
438500-- | Get server version and uptime.
439501serviceStatus :: Minio ServiceStatus
@@ -596,15 +658,17 @@ buildAdminRequest :: AdminReqInfo -> Minio NC.Request
596658buildAdminRequest areq = do
597659 ci <- asks mcConnInfo
598660 sha256Hash <-
599− if
600− | connectIsSecure ci ->
601− -- if secure connection
602− return "UNSIGNED-PAYLOAD"
603− -- otherwise compute sha256
604− | 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)
605666
606667 timeStamp <- liftIO getCurrentTime
607668
669+ mgr <- asks mcConnManager
670+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671+
608672 let hostHeader = (hHost, getHostAddr ci)
609673 newAreq =
610674 areq
@@ -617,8 +681,10 @@ buildAdminRequest areq = do
617681 signReq = toRequest ci newAreq
618682 sp =
619683 SignParams
620− (connectAccessKey ci)
621− (connectSecretKey ci)
684+ (coerce $ cvAccessKey cv)
685+ (coerce $ cvSecretKey cv)
686+ (coerce $ cvSessionToken cv)
687+ ServiceS3
622688 timeStamp
623689 Nothing
624690 Nothing
@@ -628,7 +694,7 @@ buildAdminRequest areq = do
628694 -- Update signReq with Authorization header containing v4 signature
629695 return
630696 signReq
631− { NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
697+ { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
632698 }
633699 where
634700 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
src/Network/Minio/CopyObject.hs+7−9
@@ -45,11 +45,10 @@ copyObjectInternal b' o srcInfo = do
4545
4646 when
4747 ( isJust rangeMay
48− && or
49− [ startOffset < 0,
50− endOffset < startOffset,
51− endOffset >= fromIntegral srcSize
52− ]
48+ && ( (startOffset < 0)
49+ || (endOffset < startOffset)
50+ || (endOffset >= srcSize)
51+ )
5352 )
5453 $ throwIO
5554 $ MErrVInvalidSrcObjByteRange range
@@ -69,9 +68,8 @@ copyObjectInternal b' o srcInfo = do
6968-- used is minPartSize.
7069selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
7170selectCopyRanges (st, end) =
72− zip pns
73− $ map (\(x, y) -> (st + x, st + x + y - 1))
74− $ zip startOffsets partSizes
71+ zip pns $
72+ zipWith (\x y -> (st + x, st + x + y - 1)) startOffsets partSizes
7573 where
7674 size = end - st + 1
7775 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
@@ -88,7 +86,7 @@ multiPartCopyObject ::
8886multiPartCopyObject b o cps srcSize = do
8987 uid <- newMultipartUpload b o []
9088
91− let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $ srcRange cps
89+ let byteRange = maybe (0, srcSize - 1) identity $ srcRange cps
9290 partRanges = selectCopyRanges byteRange
9391 partSources =
9492 map
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+235−188
@@ -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,26 +16,33 @@
1616{-# LANGUAGE CPP #-}
1717{-# LANGUAGE GADTs #-}
1818{-# LANGUAGE GeneralizedNewtypeDeriving #-}
19+{-# LANGUAGE StrictData #-}
1920{-# LANGUAGE TypeFamilies #-}
2021
2122module Network.Minio.Data where
2223
2324import qualified Conduit as C
2425import qualified Control.Concurrent.MVar as M
26+import Control.Monad.Trans.Except (throwE)
2527import Control.Monad.Trans.Resource
28+ ( MonadResource,
29+ MonadThrow (..),
30+ MonadUnliftIO,
31+ ResourceT,
32+ runResourceT,
33+ )
2634import qualified Data.Aeson as A
2735import qualified Data.ByteArray as BA
2836import qualified Data.ByteString as B
2937import qualified Data.ByteString.Lazy as LB
30−import Data.CaseInsensitive (mk)
38+import Data.Default.Class (def)
3139import qualified Data.HashMap.Strict as H
3240import qualified Data.Ini as Ini
33−import Data.String (IsString (..))
41+import qualified Data.List as List
3442import qualified Data.Text as T
3543import qualified Data.Text.Encoding as TE
3644import Data.Time (defaultTimeLocale, formatTime)
37−import GHC.Show (Show (show))
38−import Lib.Prelude
45+import Lib.Prelude (UTCTime, throwIO)
3946import qualified Network.Connection as Conn
4047import Network.HTTP.Client (defaultManagerSettings)
4148import qualified Network.HTTP.Client.TLS as TLS
@@ -48,13 +55,22 @@ import Network.HTTP.Types
4855 hRange,
4956 )
5057import qualified Network.HTTP.Types as HT
58+import Network.Minio.Credentials
5159import Network.Minio.Data.Crypto
60+ ( encodeToBase64,
61+ hashMD5ToBase64,
62+ )
63+import Network.Minio.Data.Time (UrlExpiry)
5264import Network.Minio.Errors
65+ ( MErrV (MErrVInvalidEncryptionKeyLength, MErrVMissingCredentials),
66+ MinioErr (..),
67+ )
68+import Network.Minio.Utils
5369import System.Directory (doesFileExist, getHomeDirectory)
5470import qualified System.Environment as Env
5571import System.FilePath.Posix (combine)
56−import Text.XML
5772import qualified UnliftIO as U
73+import qualified UnliftIO.MVar as UM
5874
5975-- | max obj size is 5TiB
6076maxObjectSize :: Int64
@@ -79,20 +95,36 @@ maxMultipartParts = 10000
7995awsRegionMap :: H.HashMap Text Text
8096awsRegionMap =
8197 H.fromList
82− [ ("us-east-1", "s3.amazonaws.com"),
83− ("us-east-2", "s3-us-east-2.amazonaws.com"),
84− ("us-west-1", "s3-us-west-1.amazonaws.com"),
85− ("us-west-2", "s3-us-west-2.amazonaws.com"),
86− ("ca-central-1", "s3-ca-central-1.amazonaws.com"),
87− ("ap-south-1", "s3-ap-south-1.amazonaws.com"),
88− ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com"),
89− ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com"),
90− ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com"),
91− ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com"),
92− ("eu-west-1", "s3-eu-west-1.amazonaws.com"),
93− ("eu-west-2", "s3-eu-west-2.amazonaws.com"),
94− ("eu-central-1", "s3-eu-central-1.amazonaws.com"),
95− ("sa-east-1", "s3-sa-east-1.amazonaws.com")
98+ [ ("us-east-1", "s3.us-east-1.amazonaws.com"),
99+ ("us-east-2", "s3.us-east-2.amazonaws.com"),
100+ ("us-west-1", "s3.us-west-1.amazonaws.com"),
101+ ("us-west-2", "s3.us-west-2.amazonaws.com"),
102+ ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
103+ ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
104+ ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
105+ ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
106+ ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
107+ ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
108+ ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
109+ ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
110+ ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
111+ ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
112+ ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
113+ ("eu-west-3", "s3.eu-west-3.amazonaws.com"),
114+ ("eu-central-1", "s3.eu-central-1.amazonaws.com"),
115+ ("eu-central-2", "s3.eu-central-2.amazonaws.com"),
116+ ("eu-south-1", "s3.eu-south-1.amazonaws.com"),
117+ ("eu-south-2", "s3.eu-south-2.amazonaws.com"),
118+ ("af-south-1", "s3.af-south-1.amazonaws.com"),
119+ ("ap-east-1", "s3.ap-east-1.amazonaws.com"),
120+ ("cn-north-1", "s3.cn-north-1.amazonaws.com.cn"),
121+ ("cn-northwest-1", "s3.cn-northwest-1.amazonaws.com.cn"),
122+ ("eu-north-1", "s3.eu-north-1.amazonaws.com"),
123+ ("me-south-1", "s3.me-south-1.amazonaws.com"),
124+ ("me-central-1", "s3.me-central-1.amazonaws.com"),
125+ ("us-gov-east-1", "s3.us-gov-east-1.amazonaws.com"),
126+ ("us-gov-west-1", "s3.us-gov-west-1.amazonaws.com"),
127+ ("sa-east-1", "s3.sa-east-1.amazonaws.com")
96128 ]
97129
98130-- | Connection Info data type. To create a 'ConnectInfo' value,
@@ -103,14 +135,15 @@ awsRegionMap =
103135data ConnectInfo = ConnectInfo
104136 { connectHost :: Text,
105137 connectPort :: Int,
106− connectAccessKey :: Text,
107− connectSecretKey :: Text,
138+ connectCreds :: Creds,
108139 connectIsSecure :: Bool,
109140 connectRegion :: Region,
110141 connectAutoDiscoverRegion :: Bool,
111142 connectDisableTLSCertValidation :: Bool
112143 }
113− deriving (Eq, Show)
144+
145+getEndpoint :: ConnectInfo -> Endpoint
146+getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
114147
115148instance IsString ConnectInfo where
116149 fromString str =
@@ -118,86 +151,89 @@ instance IsString ConnectInfo where
118151 in ConnectInfo
119152 { connectHost = TE.decodeUtf8 $ NC.host req,
120153 connectPort = NC.port req,
121− connectAccessKey = "",
122− connectSecretKey = "",
154+ connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
123155 connectIsSecure = NC.secure req,
124156 connectRegion = "",
125157 connectAutoDiscoverRegion = True,
126158 connectDisableTLSCertValidation = False
127159 }
128160
129−-- | Contains access key and secret key to access object storage.
130−data Credentials = Credentials
131− { cAccessKey :: Text,
132− cSecretKey :: Text
133− }
134− deriving (Eq, Show)
135−
136−-- | A Provider is an action that may return Credentials. Providers
137−-- may be chained together using 'findFirst'.
138−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)
139166
140−-- | Combines the given list of providers, by calling each one in
141−-- order until Credentials are found.
142−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)
143170findFirst [] = return Nothing
144171findFirst (f : fs) = do
145172 c <- f
146173 maybe (findFirst fs) (return . Just) c
147174
148−-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
149−fromAWSConfigFile :: Provider
175+-- | This action returns a 'CredentialValue' populated from
176+-- @~\/.aws\/credentials@
177+fromAWSConfigFile :: CredentialLoader
150178fromAWSConfigFile = do
151179 credsE <- runExceptT $ do
152− homeDir <- lift $ getHomeDirectory
180+ homeDir <- lift getHomeDirectory
153181 let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
154182 fileExists <- lift $ doesFileExist awsCredsFile
155183 bool (throwE "FileNotFound") (return ()) fileExists
156184 ini <- ExceptT $ Ini.readIniFile awsCredsFile
157185 akey <-
158− ExceptT $ return $
159− Ini.lookupValue "default" "aws_access_key_id" ini
186+ ExceptT $
187+ return $
188+ Ini.lookupValue "default" "aws_access_key_id" ini
160189 skey <-
161− ExceptT $ return $
162− Ini.lookupValue "default" "aws_secret_access_key" ini
163− return $ Credentials akey skey
164− return $ hush credsE
165−
166−-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
167−-- @AWS_SECRET_ACCESS_KEY@ environment variables.
168−fromAWSEnv :: Provider
190+ ExceptT $
191+ return $
192+ Ini.lookupValue "default" "aws_secret_access_key" ini
193+ return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
194+ return $ either (const Nothing) Just credsE
195+
196+-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
197+-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
198+fromAWSEnv :: CredentialLoader
169199fromAWSEnv = runMaybeT $ do
170200 akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
171201 skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
172− return $ Credentials (T.pack akey) (T.pack skey)
202+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
173203
174−-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
175−-- @MINIO_SECRET_KEY@ environment variables.
176−fromMinioEnv :: Provider
204+-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
205+-- and @MINIO_SECRET_KEY@ environment variables.
206+fromMinioEnv :: CredentialLoader
177207fromMinioEnv = runMaybeT $ do
178208 akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
179209 skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
180− return $ Credentials (T.pack akey) (T.pack skey)
210+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
181211
182−-- | setCredsFrom retrieves access credentials from the first
183−-- `Provider` form the given list that succeeds and sets it in the
184−-- `ConnectInfo`.
185−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
186215setCredsFrom ps ci = do
187216 pMay <- findFirst ps
188217 maybe
189218 (throwIO MErrVMissingCredentials)
190− (return . (flip setCreds ci))
219+ (return . (`setCreds` ci))
191220 pMay
192221
193−-- | setCreds sets the given `Credentials` in the `ConnectInfo`.
194−setCreds :: Credentials -> ConnectInfo -> ConnectInfo
195−setCreds (Credentials accessKey secretKey) connInfo =
222+-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
223+setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
224+setCreds cv connInfo =
196225 connInfo
197− { connectAccessKey = accessKey,
198− connectSecretKey = secretKey
226+ { connectCreds = CredsStatic cv
199227 }
200228
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+
201237-- | Set the S3 region parameter in the `ConnectInfo`
202238setRegion :: Region -> ConnectInfo -> ConnectInfo
203239setRegion r connInfo =
@@ -219,15 +255,7 @@ disableTLSCertValidation :: ConnectInfo -> ConnectInfo
219255disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
220256
221257getHostAddr :: ConnectInfo -> ByteString
222−getHostAddr ci =
223− if
224− | port == 80 || port == 443 -> toUtf8 host
225− | otherwise ->
226− toUtf8 $
227− T.concat [host, ":", Lib.Prelude.show port]
228− where
229− port = connectPort ci
230− host = connectHost ci
258+getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
231259
232260-- | Default Google Compute Storage ConnectInfo. Works only for
233261-- "Simple Migration" use-case with interoperability mode enabled on
@@ -250,7 +278,7 @@ awsCI = "https://s3.amazonaws.com"
250278-- ConnectInfo. Credentials are already filled in.
251279minioPlayCI :: ConnectInfo
252280minioPlayCI =
253− let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
281+ let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
254282 in setCreds playCreds $
255283 setRegion
256284 "us-east-1"
@@ -273,16 +301,16 @@ type ETag = Text
273301-- | Data type to represent an object encryption key. Create one using
274302-- the `mkSSECKey` function.
275303newtype SSECKey = SSECKey BA.ScrubbedBytes
276− deriving (Eq, Show)
304+ deriving stock (Eq, Show)
277305
278306-- | Validates that the given ByteString is 32 bytes long and creates
279307-- an encryption key.
280−mkSSECKey :: MonadThrow m => ByteString -> m SSECKey
308+mkSSECKey :: (MonadThrow m) => ByteString -> m SSECKey
281309mkSSECKey keyBytes
282310 | B.length keyBytes /= 32 =
283− throwM MErrVInvalidEncryptionKeyLength
311+ throwM MErrVInvalidEncryptionKeyLength
284312 | otherwise =
285− return $ SSECKey $ BA.convert keyBytes
313+ return $ SSECKey $ BA.convert keyBytes
286314
287315-- | Data type to represent Server-Side-Encryption settings
288316data SSE where
@@ -294,7 +322,7 @@ data SSE where
294322 -- argument is the optional KMS context that must have a
295323 -- `A.ToJSON` instance - please refer to the AWS S3 documentation
296324 -- for detailed information.
297− SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
325+ SSEKMS :: (A.ToJSON a) => Maybe ByteString -> Maybe a -> SSE
298326 -- | Specifies server-side encryption with customer provided
299327 -- key. The argument is the encryption key to be used.
300328 SSEC :: SSECKey -> SSE
@@ -352,28 +380,10 @@ data PutObjectOptions = PutObjectOptions
352380defaultPutObjectOptions :: PutObjectOptions
353381defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
354382
355−-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
356−-- stripped and a Just is returned.
357−userMetadataHeaderNameMaybe :: Text -> Maybe Text
358−userMetadataHeaderNameMaybe k =
359− let prefix = T.toCaseFold "X-Amz-Meta-"
360− n = T.length prefix
361− in if T.toCaseFold (T.take n k) == prefix
362− then Just (T.drop n k)
363− else Nothing
364−
365−addXAmzMetaPrefix :: Text -> Text
366−addXAmzMetaPrefix s
367− | isJust (userMetadataHeaderNameMaybe s) = s
368− | otherwise = "X-Amz-Meta-" <> s
369−
370−mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
371−mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix $ x, encodeUtf8 y))
372−
373383pooToHeaders :: PutObjectOptions -> [HT.Header]
374384pooToHeaders poo =
375385 userMetadata
376− ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
386+ ++ mapMaybe tupToMaybe (zip names values)
377387 ++ maybe [] toPutObjectHeaders (pooSSE poo)
378388 where
379389 tupToMaybe (k, Just v) = Just (k, v)
@@ -404,11 +414,34 @@ data BucketInfo = BucketInfo
404414 { biName :: Bucket,
405415 biCreationDate :: UTCTime
406416 }
407− deriving (Show, Eq)
417+ deriving stock (Show, Eq)
408418
409419-- | A type alias to represent a part-number for multipart upload
410420type PartNumber = Int16
411421
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+
412445-- | A type alias to represent an upload-id for multipart upload
413446type UploadId = Text
414447
@@ -422,7 +455,7 @@ data ListPartsResult = ListPartsResult
422455 lprNextPart :: Maybe Int,
423456 lprParts :: [ObjectPartInfo]
424457 }
425− deriving (Show, Eq)
458+ deriving stock (Show, Eq)
426459
427460-- | Represents information about an object part in an ongoing
428461-- multipart upload.
@@ -432,7 +465,7 @@ data ObjectPartInfo = ObjectPartInfo
432465 opiSize :: Int64,
433466 opiModTime :: UTCTime
434467 }
435− deriving (Show, Eq)
468+ deriving stock (Show, Eq)
436469
437470-- | Represents result from a listing of incomplete uploads to a
438471-- bucket.
@@ -443,7 +476,7 @@ data ListUploadsResult = ListUploadsResult
443476 lurUploads :: [(Object, UploadId, UTCTime)],
444477 lurCPrefixes :: [Text]
445478 }
446− deriving (Show, Eq)
479+ deriving stock (Show, Eq)
447480
448481-- | Represents information about a multipart upload.
449482data UploadInfo = UploadInfo
@@ -452,7 +485,7 @@ data UploadInfo = UploadInfo
452485 uiInitTime :: UTCTime,
453486 uiSize :: Int64
454487 }
455− deriving (Show, Eq)
488+ deriving stock (Show, Eq)
456489
457490-- | Represents result from a listing of objects in a bucket.
458491data ListObjectsResult = ListObjectsResult
@@ -461,7 +494,7 @@ data ListObjectsResult = ListObjectsResult
461494 lorObjects :: [ObjectInfo],
462495 lorCPrefixes :: [Text]
463496 }
464− deriving (Show, Eq)
497+ deriving stock (Show, Eq)
465498
466499-- | Represents result from a listing of objects version 1 in a bucket.
467500data ListObjectsV1Result = ListObjectsV1Result
@@ -470,7 +503,7 @@ data ListObjectsV1Result = ListObjectsV1Result
470503 lorObjects' :: [ObjectInfo],
471504 lorCPrefixes' :: [Text]
472505 }
473− deriving (Show, Eq)
506+ deriving stock (Show, Eq)
474507
475508-- | Represents information about an object.
476509data ObjectInfo = ObjectInfo
@@ -494,7 +527,7 @@ data ObjectInfo = ObjectInfo
494527 -- user-metadata pairs)
495528 oiMetadata :: H.HashMap Text Text
496529 }
497− deriving (Show, Eq)
530+ deriving stock (Show, Eq)
498531
499532-- | Represents source object in server-side copy object
500533data SourceInfo = SourceInfo
@@ -526,7 +559,7 @@ data SourceInfo = SourceInfo
526559 -- given time.
527560 srcIfUnmodifiedSince :: Maybe UTCTime
528561 }
529− deriving (Show, Eq)
562+ deriving stock (Show, Eq)
530563
531564-- | Provide a default for `SourceInfo`
532565defaultSourceInfo :: SourceInfo
@@ -539,7 +572,7 @@ data DestinationInfo = DestinationInfo
539572 -- | Destination object key
540573 dstObject :: Text
541574 }
542− deriving (Show, Eq)
575+ deriving stock (Show, Eq)
543576
544577-- | Provide a default for `DestinationInfo`
545578defaultDestinationInfo :: DestinationInfo
@@ -573,7 +606,8 @@ defaultGetObjectOptions =
573606
574607gooToHeaders :: GetObjectOptions -> [HT.Header]
575608gooToHeaders goo =
576− rangeHdr ++ zip names values
609+ rangeHdr
610+ ++ zip names values
577611 ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
578612 where
579613 names =
@@ -616,18 +650,18 @@ data Event
616650 | ObjectRemovedDelete
617651 | ObjectRemovedDeleteMarkerCreated
618652 | ReducedRedundancyLostObject
619− deriving (Eq)
620−
621−instance Show Event where
622− show ObjectCreated = "s3:ObjectCreated:*"
623− show ObjectCreatedPut = "s3:ObjectCreated:Put"
624− show ObjectCreatedPost = "s3:ObjectCreated:Post"
625− show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
626− show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
627− show ObjectRemoved = "s3:ObjectRemoved:*"
628− show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
629− show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
630− show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
653+ deriving stock (Eq, Show)
654+
655+instance ToText Event where
656+ toText ObjectCreated = "s3:ObjectCreated:*"
657+ toText ObjectCreatedPut = "s3:ObjectCreated:Put"
658+ toText ObjectCreatedPost = "s3:ObjectCreated:Post"
659+ toText ObjectCreatedCopy = "s3:ObjectCreated:Copy"
660+ toText ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
661+ toText ObjectRemoved = "s3:ObjectRemoved:*"
662+ toText ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
663+ toText ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
664+ toText ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
631665
632666textToEvent :: Text -> Maybe Event
633667textToEvent t = case t of
@@ -643,10 +677,10 @@ textToEvent t = case t of
643677 _ -> Nothing
644678
645679-- | Filter data type - part of notification configuration
646−data Filter = Filter
680+newtype Filter = Filter
647681 { fFilter :: FilterKey
648682 }
649− deriving (Show, Eq)
683+ deriving stock (Show, Eq)
650684
651685-- | defaultFilter is empty, used to create a notification
652686-- configuration.
@@ -654,10 +688,10 @@ defaultFilter :: Filter
654688defaultFilter = Filter defaultFilterKey
655689
656690-- | FilterKey contains FilterRules, and is part of a Filter.
657−data FilterKey = FilterKey
691+newtype FilterKey = FilterKey
658692 { fkKey :: FilterRules
659693 }
660− deriving (Show, Eq)
694+ deriving stock (Show, Eq)
661695
662696-- | defaultFilterKey is empty, used to create notification
663697-- configuration.
@@ -665,10 +699,10 @@ defaultFilterKey :: FilterKey
665699defaultFilterKey = FilterKey defaultFilterRules
666700
667701-- | FilterRules represents a collection of `FilterRule`s.
668−data FilterRules = FilterRules
702+newtype FilterRules = FilterRules
669703 { frFilterRules :: [FilterRule]
670704 }
671− deriving (Show, Eq)
705+ deriving stock (Show, Eq)
672706
673707-- | defaultFilterRules is empty, used to create notification
674708-- configuration.
@@ -688,7 +722,7 @@ data FilterRule = FilterRule
688722 { frName :: Text,
689723 frValue :: Text
690724 }
691− deriving (Show, Eq)
725+ deriving stock (Show, Eq)
692726
693727-- | Arn is an alias of Text
694728type Arn = Text
@@ -702,7 +736,7 @@ data NotificationConfig = NotificationConfig
702736 ncEvents :: [Event],
703737 ncFilter :: Filter
704738 }
705− deriving (Show, Eq)
739+ deriving stock (Show, Eq)
706740
707741-- | A data-type to represent bucket notification configuration. It is
708742-- a collection of queue, topic or lambda function configurations. The
@@ -714,7 +748,7 @@ data Notification = Notification
714748 nTopicConfigurations :: [NotificationConfig],
715749 nCloudFunctionConfigurations :: [NotificationConfig]
716750 }
717− deriving (Eq, Show)
751+ deriving stock (Show, Eq)
718752
719753-- | The default notification configuration is empty.
720754defaultNotification :: Notification
@@ -733,10 +767,10 @@ data SelectRequest = SelectRequest
733767 srOutputSerialization :: OutputSerialization,
734768 srRequestProgressEnabled :: Maybe Bool
735769 }
736− deriving (Eq, Show)
770+ deriving stock (Show, Eq)
737771
738772data ExpressionType = SQL
739− deriving (Eq, Show)
773+ deriving stock (Show, Eq)
740774
741775-- | InputSerialization represents format information of the input
742776-- object being queried. Use one of the smart constructors such as
@@ -746,7 +780,7 @@ data InputSerialization = InputSerialization
746780 { isCompressionType :: Maybe CompressionType,
747781 isFormatInfo :: InputFormatInfo
748782 }
749− deriving (Eq, Show)
783+ deriving stock (Show, Eq)
750784
751785-- | Data type representing the compression setting in a Select
752786-- request.
@@ -754,7 +788,7 @@ data CompressionType
754788 = CompressionTypeNone
755789 | CompressionTypeGzip
756790 | CompressionTypeBzip2
757− deriving (Eq, Show)
791+ deriving stock (Show, Eq)
758792
759793-- | Data type representing input object format information in a
760794-- Select request.
@@ -762,7 +796,7 @@ data InputFormatInfo
762796 = InputFormatCSV CSVInputProp
763797 | InputFormatJSON JSONInputProp
764798 | InputFormatParquet
765− deriving (Eq, Show)
799+ deriving stock (Show, Eq)
766800
767801-- | defaultCsvInput returns InputSerialization with default CSV
768802-- format, and without any compression setting.
@@ -841,20 +875,17 @@ type CSVInputProp = CSVProp
841875
842876-- | CSVProp represents CSV format properties. It is built up using
843877-- the Monoid instance.
844−data CSVProp = CSVProp (H.HashMap Text Text)
845− deriving (Eq, Show)
878+newtype CSVProp = CSVProp (H.HashMap Text Text)
879+ deriving stock (Show, Eq)
846880
847−#if (__GLASGOW_HASKELL__ >= 804)
848881instance Semigroup CSVProp where
849− (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
850−#endif
882+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
851883
852884instance Monoid CSVProp where
853885 mempty = CSVProp mempty
854886
855−#if (__GLASGOW_HASKELL__ < 804)
856− mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
857−#endif
887+csvPropsList :: CSVProp -> [(Text, Text)]
888+csvPropsList (CSVProp h) = sort $ H.toList h
858889
859890defaultCSVProp :: CSVProp
860891defaultCSVProp = mempty
@@ -884,15 +915,15 @@ data FileHeaderInfo
884915 FileHeaderUse
885916 | -- | Header are present, but should be ignored
886917 FileHeaderIgnore
887− deriving (Eq, Show)
918+ deriving stock (Show, Eq)
888919
889920-- | Specify the CSV file header info property.
890921fileHeaderInfo :: FileHeaderInfo -> CSVProp
891−fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString
922+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toStr
892923 where
893− toString FileHeaderNone = "NONE"
894− toString FileHeaderUse = "USE"
895− toString FileHeaderIgnore = "IGNORE"
924+ toStr FileHeaderNone = "NONE"
925+ toStr FileHeaderUse = "USE"
926+ toStr FileHeaderIgnore = "IGNORE"
896927
897928-- | Specify the CSV comment character property. Lines starting with
898929-- this character are ignored by the server.
@@ -909,13 +940,13 @@ setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
909940
910941-- | Set the CSV format properties in the OutputSerialization.
911942outputCSVFromProps :: CSVProp -> OutputSerialization
912−outputCSVFromProps p = OutputSerializationCSV p
943+outputCSVFromProps = OutputSerializationCSV
913944
914−data JSONInputProp = JSONInputProp {jsonipType :: JSONType}
915− deriving (Eq, Show)
945+newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
946+ deriving stock (Show, Eq)
916947
917948data JSONType = JSONTypeDocument | JSONTypeLines
918− deriving (Eq, Show)
949+ deriving stock (Show, Eq)
919950
920951-- | OutputSerialization represents output serialization settings for
921952-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
@@ -923,23 +954,24 @@ data JSONType = JSONTypeDocument | JSONTypeLines
923954data OutputSerialization
924955 = OutputSerializationJSON JSONOutputProp
925956 | OutputSerializationCSV CSVOutputProp
926− deriving (Eq, Show)
957+ deriving stock (Show, Eq)
927958
928959type CSVOutputProp = CSVProp
929960
930961-- | quoteFields is an output serialization parameter
931962quoteFields :: QuoteFields -> CSVProp
932−quoteFields q = CSVProp $ H.singleton "QuoteFields" $
933− case q of
934− QuoteFieldsAsNeeded -> "ASNEEDED"
935− QuoteFieldsAlways -> "ALWAYS"
963+quoteFields q = CSVProp $
964+ H.singleton "QuoteFields" $
965+ case q of
966+ QuoteFieldsAsNeeded -> "ASNEEDED"
967+ QuoteFieldsAlways -> "ALWAYS"
936968
937969-- | Represent the QuoteField setting.
938970data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
939− deriving (Eq, Show)
971+ deriving stock (Show, Eq)
940972
941−data JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
942− deriving (Eq, Show)
973+newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
974+ deriving stock (Show, Eq)
943975
944976-- | Set the output record delimiter for JSON format
945977outputJSONFromRecordDelimiter :: Text -> OutputSerialization
@@ -950,14 +982,15 @@ outputJSONFromRecordDelimiter t =
950982
951983-- | An EventMessage represents each kind of message received from the server.
952984data EventMessage
953− = ProgressEventMessage {emProgress :: Progress}
954− | StatsEventMessage {emStats :: Stats}
985+ = ProgressEventMessage Progress
986+ | StatsEventMessage Stats
955987 | RequestLevelErrorMessage
956− { emErrorCode :: Text,
957− emErrorMessage :: Text
958− }
959− | RecordPayloadEventMessage {emPayloadBytes :: ByteString}
960− deriving (Eq, Show)
988+ Text
989+ -- ^ Error code
990+ Text
991+ -- ^ Error message
992+ | RecordPayloadEventMessage ByteString
993+ deriving stock (Show, Eq)
961994
962995data MsgHeaderName
963996 = MessageType
@@ -965,7 +998,7 @@ data MsgHeaderName
965998 | ContentType
966999 | ErrorCode
9671000 | ErrorMessage
968− deriving (Eq, Show)
1001+ deriving stock (Show, Eq)
9691002
9701003msgHeaderValueType :: Word8
9711004msgHeaderValueType = 7
@@ -978,7 +1011,7 @@ data Progress = Progress
9781011 pBytesProcessed :: Int64,
9791012 pBytesReturned :: Int64
9801013 }
981− deriving (Eq, Show)
1014+ deriving stock (Show, Eq)
9821015
9831016-- | Represent the stats event returned at the end of the Select
9841017-- response.
@@ -1016,7 +1049,8 @@ data S3ReqInfo = S3ReqInfo
10161049 riPayload :: Payload,
10171050 riPayloadHash :: Maybe ByteString,
10181051 riRegion :: Maybe Region,
1019− riNeedsLocation :: Bool
1052+ riNeedsLocation :: Bool,
1053+ riPresignExpirySecs :: Maybe UrlExpiry
10201054 }
10211055
10221056defaultS3ReqInfo :: S3ReqInfo
@@ -1031,16 +1065,13 @@ defaultS3ReqInfo =
10311065 Nothing
10321066 Nothing
10331067 True
1068+ Nothing
10341069
10351070getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
10361071getS3Path b o =
1037− let segments = map toUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
1072+ let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
10381073 in B.concat ["/", B.intercalate "/" segments]
10391074
1040−-- | Time to expire for a presigned URL. It interpreted as a number of
1041−-- seconds. The maximum duration that can be specified is 7 days.
1042−type UrlExpiry = Int
1043−
10441075type RegionMap = H.HashMap Bucket Region
10451076
10461077-- | The Minio Monad - all computations accessing object storage
@@ -1048,7 +1079,7 @@ type RegionMap = H.HashMap Bucket Region
10481079newtype Minio a = Minio
10491080 { unMinio :: ReaderT MinioConn (ResourceT IO) a
10501081 }
1051− deriving
1082+ deriving newtype
10521083 ( Functor,
10531084 Applicative,
10541085 Monad,
@@ -1072,11 +1103,10 @@ class HasSvcNamespace env where
10721103instance HasSvcNamespace MinioConn where
10731104 getSvcNamespace env =
10741105 let host = connectHost $ mcConnInfo env
1075− in if
1076− | host == "storage.googleapis.com" ->
1077− "http://doc.s3.amazonaws.com/2006-03-01"
1078− | otherwise ->
1079− "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+ )
10801110
10811111-- | Takes connection information and returns a connection object to
10821112-- be passed to 'runMinio'. The returned value can be kept in the
@@ -1086,8 +1116,12 @@ connect :: ConnectInfo -> IO MinioConn
10861116connect ci = do
10871117 let settings
10881118 | connectIsSecure ci && connectDisableTLSCertValidation ci =
1089− let badTlsSettings = Conn.TLSSettingsSimple True False False
1090− in TLS.mkManagerSettings badTlsSettings Nothing
1119+ let badTlsSettings =
1120+ case def of
1121+ tlsSettings@Conn.TLSSettingsSimple {} ->
1122+ tlsSettings {Conn.settingDisableCertificateValidation = True}
1123+ _ -> def
1124+ in TLS.mkManagerSettings badTlsSettings Nothing
10911125 | connectIsSecure ci = NC.tlsManagerSettings
10921126 | otherwise = defaultManagerSettings
10931127 mgr <- NC.newManager settings
@@ -1136,9 +1170,22 @@ runMinioRes ci m = do
11361170 conn <- liftIO $ connect ci
11371171 runMinioResWith conn m
11381172
1139−s3Name :: Text -> Text -> Name
1140−s3Name ns s = Name s (Just ns) Nothing
1141−
11421173-- | Format as per RFC 1123.
11431174formatRFC1123 :: UTCTime -> T.Text
11441175formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1176+
1177+lookupRegionCache :: Bucket -> Minio (Maybe Region)
1178+lookupRegionCache b = do
1179+ rMVar <- asks mcRegionMap
1180+ rMap <- UM.readMVar rMVar
1181+ return $ H.lookup b rMap
1182+
1183+addToRegionCache :: Bucket -> Region -> Minio ()
1184+addToRegionCache b region = do
1185+ rMVar <- asks mcRegionMap
1186+ UM.modifyMVar_ rMVar $ return . H.insert b region
1187+
1188+deleteFromRegionCache :: Bucket -> Minio ()
1189+deleteFromRegionCache b = do
1190+ rMVar <- asks mcRegionMap
1191+ UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/Data/ByteString.hs+7−6
@@ -25,9 +25,8 @@ import qualified Data.ByteString as B
2525import qualified Data.ByteString.Builder as BB
2626import qualified Data.ByteString.Char8 as BC8
2727import qualified Data.ByteString.Lazy as LB
28−import Data.Char (isAsciiLower, isAsciiUpper)
28+import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace, toUpper)
2929import qualified Data.Text as T
30−import Lib.Prelude
3130import Numeric (showHex)
3231
3332stripBS :: ByteString -> ByteString
@@ -38,8 +37,10 @@ class UriEncodable s where
3837
3938instance UriEncodable [Char] where
4039 uriEncode encodeSlash payload =
41− LB.toStrict $ BB.toLazyByteString $ mconcat $
42− map (`uriEncodeChar` encodeSlash) payload
40+ LB.toStrict $
41+ BB.toLazyByteString $
42+ mconcat $
43+ map (`uriEncodeChar` encodeSlash) payload
4344
4445instance UriEncodable ByteString where
4546 -- assumes that uriEncode is passed ASCII encoded strings.
@@ -64,11 +65,11 @@ uriEncodeChar ch _
6465 || (ch == '-')
6566 || (ch == '.')
6667 || (ch == '~') =
67− BB.char7 ch
68+ BB.char7 ch
6869 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
6970 where
7071 f :: Word8 -> BB.Builder
7172 f n = BB.char7 '%' <> BB.string7 hexStr
7273 where
7374 hexStr = map toUpper $ showHex q $ showHex r ""
74− (q, r) = divMod (fromIntegral n) (16 :: Word8)
75+ (q, r) = divMod n (16 :: Word8)
src/Network/Minio/Data/Crypto.hs+8−9
@@ -39,31 +39,30 @@ import Crypto.MAC.HMAC (HMAC, hmac)
3939import Data.ByteArray (ByteArrayAccess, convert)
4040import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
4141import qualified Data.Conduit as C
42−import Lib.Prelude
4342
4443hashSHA256 :: ByteString -> ByteString
4544hashSHA256 = digestToBase16 . hashWith SHA256
4645
47−hashSHA256FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
46+hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
4847hashSHA256FromSource src = do
4948 digest <- C.connect src sinkSHA256Hash
5049 return $ digestToBase16 digest
5150 where
5251 -- To help with type inference
53− sinkSHA256Hash :: Monad m => C.ConduitM ByteString Void m (Digest SHA256)
52+ sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
5453 sinkSHA256Hash = sinkHash
5554
5655-- Returns MD5 hash hex encoded.
5756hashMD5 :: ByteString -> ByteString
5857hashMD5 = digestToBase16 . hashWith MD5
5958
60−hashMD5FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
59+hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
6160hashMD5FromSource src = do
6261 digest <- C.connect src sinkMD5Hash
6362 return $ digestToBase16 digest
6463 where
6564 -- To help with type inference
66− sinkMD5Hash :: Monad m => C.ConduitM ByteString Void m (Digest MD5)
65+ sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
6766 sinkMD5Hash = sinkHash
6867
6968hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -72,15 +71,15 @@ hmacSHA256 message key = hmac key message
7271hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
7372hmacSHA256RawBS message key = convert $ hmacSHA256 message key
7473
75−digestToBS :: ByteArrayAccess a => a -> ByteString
74+digestToBS :: (ByteArrayAccess a) => a -> ByteString
7675digestToBS = convert
7776
78−digestToBase16 :: ByteArrayAccess a => a -> ByteString
77+digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
7978digestToBase16 = convertToBase Base16
8079
8180-- Returns MD5 hash base 64 encoded.
82−hashMD5ToBase64 :: ByteArrayAccess a => a -> ByteString
81+hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
8382hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
8483
85−encodeToBase64 :: ByteArrayAccess a => a -> ByteString
84+encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
8685encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+7−1
@@ -21,13 +21,19 @@ module Network.Minio.Data.Time
2121 awsDateFormatBS,
2222 awsParseTime,
2323 iso8601TimeFormat,
24+ UrlExpiry,
2425 )
2526where
2627
2728import Data.ByteString.Char8 (pack)
2829import qualified Data.Time as Time
30+import Data.Time.Format.ISO8601 (iso8601Show)
2931import Lib.Prelude
3032
33+-- | Time to expire for a presigned URL. It interpreted as a number of
34+-- seconds. The maximum duration that can be specified is 7 days.
35+type UrlExpiry = Int
36+
3137awsTimeFormat :: UTCTime -> [Char]
3238awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
3339
@@ -44,4 +50,4 @@ awsParseTime :: [Char] -> Maybe UTCTime
4450awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
4551
4652iso8601TimeFormat :: UTCTime -> [Char]
47−iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
53+iso8601TimeFormat = iso8601Show
src/Network/Minio/Errors.hs+13−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.
@@ -14,10 +14,15 @@
1414-- limitations under the License.
1515--
1616
17−module Network.Minio.Errors where
17+module Network.Minio.Errors
18+ ( MErrV (..),
19+ ServiceErr (..),
20+ MinioErr (..),
21+ toServiceErr,
22+ )
23+where
1824
19−import Control.Exception
20−import Lib.Prelude
25+import Control.Exception (IOException)
2126import qualified Network.HTTP.Conduit as NC
2227
2328---------------------------------
@@ -44,7 +49,8 @@ data MErrV
4449 | MErrVInvalidEncryptionKeyLength
4550 | MErrVStreamingBodyUnexpectedEOF
4651 | MErrVUnexpectedPayload
47− deriving (Show, Eq)
52+ | MErrVSTSEndpointNotFound
53+ deriving stock (Show, Eq)
4854
4955instance Exception MErrV
5056
@@ -57,7 +63,7 @@ data ServiceErr
5763 | NoSuchKey
5864 | SelectErr Text Text
5965 | ServiceErr Text Text
60− deriving (Show, Eq)
66+ deriving stock (Show, Eq)
6167
6268instance Exception ServiceErr
6369
@@ -75,7 +81,7 @@ data MinioErr
7581 | MErrIO IOException
7682 | MErrService ServiceErr
7783 | MErrValidation MErrV
78− deriving (Show)
84+ deriving stock (Show)
7985
8086instance Eq MinioErr where
8187 MErrHTTP _ == MErrHTTP _ = True
src/Network/Minio/JsonParser.hs+3−3
@@ -20,11 +20,11 @@ module Network.Minio.JsonParser
2020where
2121
2222import Data.Aeson
23− ( (.:),
24− FromJSON,
23+ ( FromJSON,
2524 eitherDecode,
2625 parseJSON,
2726 withObject,
27+ (.:),
2828 )
2929import qualified Data.Text as T
3030import Lib.Prelude
@@ -34,7 +34,7 @@ data AdminErrJSON = AdminErrJSON
3434 { aeCode :: Text,
3535 aeMessage :: Text
3636 }
37− deriving (Eq, Show)
37+ deriving stock (Eq, Show)
3838
3939instance FromJSON AdminErrJSON where
4040 parseJSON = withObject "AdminErrJSON" $ \v ->
src/Network/Minio/ListOps.hs+56−21
@@ -19,16 +19,47 @@ module Network.Minio.ListOps where
1919import qualified Data.Conduit as C
2020import qualified Data.Conduit.Combinators as CC
2121import qualified Data.Conduit.List as CL
22−import Lib.Prelude
2322import Network.Minio.Data
23+ ( Bucket,
24+ ListObjectsResult
25+ ( lorCPrefixes,
26+ lorHasMore,
27+ lorNextToken,
28+ lorObjects
29+ ),
30+ ListObjectsV1Result
31+ ( lorCPrefixes',
32+ lorHasMore',
33+ lorNextMarker,
34+ lorObjects'
35+ ),
36+ ListPartsResult (lprHasMore, lprNextPart, lprParts),
37+ ListUploadsResult
38+ ( lurHasMore,
39+ lurNextKey,
40+ lurNextUpload,
41+ lurUploads
42+ ),
43+ Minio,
44+ Object,
45+ ObjectInfo,
46+ ObjectPartInfo (opiSize),
47+ UploadId,
48+ UploadInfo (UploadInfo),
49+ )
2450import Network.Minio.S3API
51+ ( listIncompleteParts',
52+ listIncompleteUploads',
53+ listObjects',
54+ listObjectsV1',
55+ )
2556
2657-- | Represents a list output item - either an object or an object
2758-- prefix (i.e. a directory).
2859data ListItem
2960 = ListItemObject ObjectInfo
3061 | ListItemPrefix Text
31− deriving (Show, Eq)
62+ deriving stock (Show, Eq)
3263
3364-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
3465-- similar to a file system tree traversal.
@@ -51,10 +82,10 @@ listObjects bucket prefix recurse = loop Nothing
5182
5283 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
5384 CL.sourceList $ map ListItemObject $ lorObjects res
54− unless recurse
55− $ CL.sourceList
56− $ map ListItemPrefix
57− $ lorCPrefixes res
85+ unless recurse $
86+ CL.sourceList $
87+ map ListItemPrefix $
88+ lorCPrefixes res
5889 when (lorHasMore res) $
5990 loop (lorNextToken res)
6091
@@ -73,10 +104,10 @@ listObjectsV1 bucket prefix recurse = loop Nothing
73104
74105 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
75106 CL.sourceList $ map ListItemObject $ lorObjects' res
76− unless recurse
77− $ CL.sourceList
78− $ map ListItemPrefix
79− $ lorCPrefixes' res
107+ unless recurse $
108+ CL.sourceList $
109+ map ListItemPrefix $
110+ lorCPrefixes' res
80111 when (lorHasMore' res) $
81112 loop (lorNextMarker res)
82113
@@ -104,19 +135,23 @@ listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
104135 nextUploadIdMarker
105136 Nothing
106137
107− aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do
108− partInfos <-
109− C.runConduit $
110− listIncompleteParts bucket uKey uId
111− C..| CC.sinkList
112− return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
138+ aggrSizes <- lift $
139+ forM (lurUploads res) $ \(uKey, uId, _) -> do
140+ partInfos <-
141+ C.runConduit $
142+ listIncompleteParts bucket uKey uId
143+ C..| CC.sinkList
144+ return $ foldl' (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
113145
114− CL.sourceList
115− $ map
116− ( \((uKey, uId, uInitTime), size) ->
117− UploadInfo uKey uId uInitTime size
146+ CL.sourceList $
147+ zipWith
148+ ( curry
149+ ( \((uKey, uId, uInitTime), size) ->
150+ UploadInfo uKey uId uInitTime size
151+ )
118152 )
119− $ zip (lurUploads res) aggrSizes
153+ (lurUploads res)
154+ aggrSizes
120155
121156 when (lurHasMore res) $
122157 loop (lurNextKey res) (lurNextUpload res)
src/Network/Minio/PresignedOperations.hs+73−66
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,6 +13,7 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16+{-# LANGUAGE CPP #-}
1617
1718module Network.Minio.PresignedOperations
1819 ( UrlExpiry,
@@ -42,13 +43,21 @@ import qualified Data.HashMap.Strict as H
4243import qualified Data.Text as T
4344import qualified Data.Time as Time
4445import Lib.Prelude
45−import qualified Network.HTTP.Conduit as NC
46+import qualified Network.HTTP.Client as NClient
4647import qualified Network.HTTP.Types as HT
47−import Network.HTTP.Types.Header (hHost)
48+import Network.Minio.API (buildRequest)
49+import Network.Minio.Credentials
4850import Network.Minio.Data
4951import Network.Minio.Data.Time
5052import Network.Minio.Errors
5153import Network.Minio.Sign.V4
54+import Network.URI (uriToString)
55+
56+{- ORMOLU_DISABLE -}
57+#if MIN_VERSION_aeson(2,0,0)
58+import qualified Data.Aeson.Key as A
59+#endif
60+{- ORMOLU_ENABLE -}
5261
5362-- | Generate a presigned URL. This function allows for advanced usage
5463-- - for simple cases prefer the `presigned*Url` functions.
@@ -68,46 +77,26 @@ makePresignedUrl ::
6877 HT.RequestHeaders ->
6978 Minio ByteString
7079makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
71− when (expiry > 7 * 24 * 3600 || expiry < 0)
72− $ throwIO
73− $ MErrVInvalidUrlExpiry expiry
74−
75− ci <- asks mcConnInfo
80+ when (expiry > 7 * 24 * 3600 || expiry < 0) $
81+ throwIO $
82+ MErrVInvalidUrlExpiry expiry
7683
77− let hostHeader = (hHost, getHostAddr ci)
78− req =
79− NC.defaultRequest
80− { NC.method = method,
81− NC.secure = connectIsSecure ci,
82− NC.host = encodeUtf8 $ connectHost ci,
83− NC.port = connectPort ci,
84− NC.path = getS3Path bucket object,
85− NC.requestHeaders = hostHeader : extraHeaders,
86− NC.queryString = HT.renderQuery True extraQuery
84+ let s3ri =
85+ defaultS3ReqInfo
86+ { riPresignExpirySecs = Just expiry,
87+ riMethod = method,
88+ riBucket = bucket,
89+ riObject = object,
90+ riRegion = region,
91+ riQueryParams = extraQuery,
92+ riHeaders = extraHeaders
8793 }
88− ts <- liftIO Time.getCurrentTime
8994
90− let sp =
91− SignParams
92− (connectAccessKey ci)
93− (connectSecretKey ci)
94− ts
95− region
96− (Just expiry)
97− Nothing
98− signPairs = signV4 sp req
99− qpToAdd = (fmap . fmap) Just signPairs
100− queryStr =
101− HT.renderQueryBuilder
102− True
103− ((HT.parseQuery $ NC.queryString req) ++ qpToAdd)
104− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
95+ req <- buildRequest s3ri
96+ let uri = NClient.getUri req
97+ uriString = uriToString identity uri ""
10598
106− return $ toStrictBS $ toLazyByteString $
107− scheme
108− <> byteString (getHostAddr ci)
109− <> byteString (getS3Path bucket object)
110− <> queryStr
99+ return $ encodeUtf8 uriString
111100
112101-- | Generate a URL with authentication signature to PUT (upload) an
113102-- object. Any extra headers if passed, are signed, and so they are
@@ -189,29 +178,39 @@ data PostPolicyCondition
189178 = PPCStartsWith Text Text
190179 | PPCEquals Text Text
191180 | PPCRange Text Int64 Int64
192− deriving (Show, Eq)
181+ deriving stock (Show, Eq)
193182
183+{- ORMOLU_DISABLE -}
194184instance Json.ToJSON PostPolicyCondition where
195185 toJSON (PPCStartsWith k v) = Json.toJSON ["starts-with", k, v]
186+#if MIN_VERSION_aeson(2,0,0)
187+ toJSON (PPCEquals k v) = Json.object [(A.fromText k) .= v]
188+#else
196189 toJSON (PPCEquals k v) = Json.object [k .= v]
190+#endif
197191 toJSON (PPCRange k minVal maxVal) =
198192 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
199193
200194 toEncoding (PPCStartsWith k v) = Json.foldable ["starts-with", k, v]
195+#if MIN_VERSION_aeson(2,0,0)
196+ toEncoding (PPCEquals k v) = Json.pairs ((A.fromText k) .= v)
197+#else
201198 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
199+#endif
202200 toEncoding (PPCRange k minVal maxVal) =
203201 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202+{- ORMOLU_ENABLE -}
204203
205204-- | A PostPolicy is required to perform uploads via browser forms.
206205data PostPolicy = PostPolicy
207206 { expiration :: UTCTime,
208207 conditions :: [PostPolicyCondition]
209208 }
210− deriving (Show, Eq)
209+ deriving stock (Show, Eq)
211210
212211instance Json.ToJSON PostPolicy where
213212 toJSON (PostPolicy e c) =
214− Json.object $
213+ Json.object
215214 [ "expiration" .= iso8601TimeFormat e,
216215 "conditions" .= c
217216 ]
@@ -224,7 +223,7 @@ data PostPolicyError
224223 | PPEBucketNotSpecified
225224 | PPEConditionKeyEmpty
226225 | PPERangeInvalid
227− deriving (Eq, Show)
226+ deriving stock (Show, Eq)
228227
229228-- | Set the bucket name that the upload should use.
230229ppCondBucket :: Bucket -> PostPolicyCondition
@@ -265,19 +264,19 @@ newPostPolicy ::
265264newPostPolicy expirationTime conds
266265 -- object name condition must be present
267266 | not $ any (keyEquals "key") conds =
268− Left PPEKeyNotSpecified
267+ Left PPEKeyNotSpecified
269268 -- bucket name condition must be present
270269 | not $ any (keyEquals "bucket") conds =
271− Left PPEBucketNotSpecified
270+ Left PPEBucketNotSpecified
272271 -- a condition with an empty key is invalid
273272 | any (keyEquals "") conds || any isEmptyRangeKey conds =
274− Left PPEConditionKeyEmpty
273+ Left PPEConditionKeyEmpty
275274 -- invalid range check
276275 | any isInvalidRange conds =
277− Left PPERangeInvalid
276+ Left PPERangeInvalid
278277 -- all good!
279278 | otherwise =
280− return $ PostPolicy expirationTime conds
279+ return $ PostPolicy expirationTime conds
281280 where
282281 keyEquals k' (PPCStartsWith k _) = k == k'
283282 keyEquals k' (PPCEquals k _) = k == k'
@@ -299,50 +298,58 @@ presignedPostPolicy ::
299298 Minio (ByteString, H.HashMap Text ByteString)
300299presignedPostPolicy p = do
301300 ci <- asks mcConnInfo
302− signTime <- liftIO $ Time.getCurrentTime
301+ signTime <- liftIO Time.getCurrentTime
302+ mgr <- asks mcConnManager
303+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
303304
304− let extraConditions =
305− [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime),
305+ let extraConditions signParams =
306+ [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
306307 PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
307308 PPCEquals
308309 "x-amz-credential"
309310 ( T.intercalate
310311 "/"
311− [ connectAccessKey ci,
312− decodeUtf8 $ mkScope signTime region
312+ [ coerce $ cvAccessKey cv,
313+ decodeUtf8 $ credentialScope signParams
313314 ]
314315 )
315316 ]
316− ppWithCreds =
317+ ppWithCreds signParams =
317318 p
318− { conditions = conditions p ++ extraConditions
319+ { conditions = conditions p ++ extraConditions signParams
319320 }
320321 sp =
321322 SignParams
322− (connectAccessKey ci)
323− (connectSecretKey ci)
323+ (coerce $ cvAccessKey cv)
324+ (coerce $ cvSecretKey cv)
325+ (coerce $ cvSessionToken cv)
326+ ServiceS3
324327 signTime
325328 (Just $ connectRegion ci)
326329 Nothing
327330 Nothing
328− signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
331+ signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
329332 -- compute form-data
330333 mkPair (PPCStartsWith k v) = Just (k, v)
331334 mkPair (PPCEquals k v) = Just (k, v)
332335 mkPair _ = Nothing
333336 formFromPolicy =
334− H.map toUtf8 $ H.fromList $ catMaybes $
335− mkPair <$> conditions ppWithCreds
337+ H.map encodeUtf8 $
338+ H.fromList $
339+ mapMaybe
340+ mkPair
341+ (conditions $ ppWithCreds sp)
336342 formData = formFromPolicy `H.union` signData
337343 -- compute POST upload URL
338344 bucket = H.lookupDefault "" "bucket" formData
339345 scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
340− region = connectRegion ci
341346 url =
342− toStrictBS $ toLazyByteString $
343− scheme <> byteString (getHostAddr ci)
344− <> byteString "/"
345− <> byteString bucket
346− <> byteString "/"
347+ toStrictBS $
348+ toLazyByteString $
349+ scheme
350+ <> byteString (getHostAddr ci)
351+ <> byteString "/"
352+ <> byteString bucket
353+ <> byteString "/"
347354
348355 return (url, formData)
src/Network/Minio/PutObject.hs+7−7
@@ -71,13 +71,13 @@ putObjectInternal b o opts (ODStream src sizeMay) = do
7171 Just size ->
7272 if
7373 | size <= 64 * oneMiB -> do
74− bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
75− putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
74+ bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
75+ putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
7676 | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
7777 | otherwise -> sequentialMultipartUpload b o opts (Just size) src
7878putObjectInternal b o opts (ODFile fp sizeMay) = do
7979 hResE <- withNewHandle fp $ \h ->
80− liftM2 (,) (isHandleSeekable h) (getFileSize h)
80+ liftA2 (,) (isHandleSeekable h) (getFileSize h)
8181
8282 (isSeekable, handleSizeMay) <-
8383 either
@@ -95,13 +95,13 @@ putObjectInternal b o opts (ODFile fp sizeMay) = do
9595 Just size ->
9696 if
9797 | size <= 64 * oneMiB ->
98− either throwIO return
99− =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
98+ either throwIO return
99+ =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
100100 | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
101101 | isSeekable -> parallelMultipartUpload b o opts fp size
102102 | otherwise ->
103− sequentialMultipartUpload b o opts (Just size) $
104− CB.sourceFile fp
103+ sequentialMultipartUpload b o opts (Just size) $
104+ CB.sourceFile fp
105105
106106parallelMultipartUpload ::
107107 Bucket ->
src/Network/Minio/S3API.hs+96−74
@@ -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,15 +14,25 @@
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,
2028
2129 -- * Listing buckets
30+
2231 --------------------
2332 getService,
2433
2534 -- * Listing objects
35+
2636 --------------------
2737 ListObjectsResult (..),
2838 ListObjectsV1Result (..),
@@ -33,11 +43,13 @@ module Network.Minio.S3API
3343 headBucket,
3444
3545 -- * Retrieving objects
46+
3647 -----------------------
3748 getObject',
3849 headObject,
3950
4051 -- * Creating buckets and objects
52+
4153 ---------------------------------
4254 putBucket,
4355 ETag,
@@ -47,6 +59,7 @@ module Network.Minio.S3API
4759 copyObjectSingle,
4860
4961 -- * Multipart Upload APIs
62+
5063 --------------------------
5164 UploadId,
5265 PartTuple,
@@ -63,11 +76,13 @@ module Network.Minio.S3API
6376 listIncompleteParts',
6477
6578 -- * Deletion APIs
79+
6680 --------------------------
6781 deleteBucket,
6882 deleteObject,
6983
7084 -- * Presigned Operations
85+
7186 -----------------------------
7287 module Network.Minio.PresignedOperations,
7388
@@ -76,6 +91,7 @@ module Network.Minio.S3API
7691 setBucketPolicy,
7792
7893 -- * Bucket Notifications
94+
7995 -------------------------
8096 Notification (..),
8197 NotificationConfig (..),
@@ -123,7 +139,8 @@ parseGetObjectHeaders object headers =
123139 let metadataPairs = getMetadata headers
124140 userMetadata = getUserMetadataMap metadataPairs
125141 metadata = getNonUserMetadataMap metadataPairs
126− in ObjectInfo <$> Just object
142+ in ObjectInfo
143+ <$> Just object
127144 <*> getLastModifiedHeader headers
128145 <*> getETagHeader headers
129146 <*> getContentLength headers
@@ -157,24 +174,26 @@ getObject' bucket object queryParams headers = do
157174 { riBucket = Just bucket,
158175 riObject = Just object,
159176 riQueryParams = queryParams,
160− riHeaders = headers
161− -- This header is required for safety as otherwise http-client,
162− -- sends Accept-Encoding: gzip, and the server may actually gzip
163− -- body. In that case Content-Length header will be missing.
164− <> [("Accept-Encoding", "identity")]
177+ riHeaders =
178+ headers
179+ -- This header is required for safety as otherwise http-client,
180+ -- sends Accept-Encoding: gzip, and the server may actually gzip
181+ -- body. In that case Content-Length header will be missing.
182+ <> [("Accept-Encoding", "identity")]
165183 }
166184
167185-- | Creates a bucket via a PUT bucket call.
168186putBucket :: Bucket -> Region -> Minio ()
169187putBucket bucket location = do
170188 ns <- asks getSvcNamespace
171− void $ executeRequest $
172− defaultS3ReqInfo
173− { riMethod = HT.methodPut,
174− riBucket = Just bucket,
175− riPayload = PayloadBS $ mkCreateBucketConfig ns location,
176− riNeedsLocation = False
177− }
189+ void $
190+ executeRequest $
191+ defaultS3ReqInfo
192+ { riMethod = HT.methodPut,
193+ riBucket = Just bucket,
194+ riPayload = PayloadBS $ mkCreateBucketConfig ns location,
195+ riNeedsLocation = False
196+ }
178197
179198-- | Single PUT object size.
180199maxSinglePutObjectSizeBytes :: Int64
@@ -188,9 +207,9 @@ putObjectSingle' :: Bucket -> Object -> [HT.Header] -> ByteString -> Minio ETag
188207putObjectSingle' bucket object headers bs = do
189208 let size = fromIntegral (BS.length bs)
190209 -- check length is within single PUT object size.
191− when (size > maxSinglePutObjectSizeBytes)
192− $ throwIO
193− $ MErrVSinglePUTSizeExceeded size
210+ when (size > maxSinglePutObjectSizeBytes) $
211+ throwIO $
212+ MErrVSinglePUTSizeExceeded size
194213
195214 let payload = mkStreamingPayload $ PayloadBS bs
196215 resp <-
@@ -222,9 +241,9 @@ putObjectSingle ::
222241 Minio ETag
223242putObjectSingle bucket object headers h offset size = do
224243 -- check length is within single PUT object size.
225− when (size > maxSinglePutObjectSizeBytes)
226− $ throwIO
227− $ MErrVSinglePUTSizeExceeded size
244+ when (size > maxSinglePutObjectSizeBytes) $
245+ throwIO $
246+ MErrVSinglePUTSizeExceeded size
228247
229248 -- content-length header is automatically set by library.
230249 let payload = mkStreamingPayload $ PayloadH h offset size
@@ -301,23 +320,23 @@ listObjects' bucket prefix nextToken delimiter maxKeys = do
301320-- | DELETE a bucket from the service.
302321deleteBucket :: Bucket -> Minio ()
303322deleteBucket bucket =
304− void
305− $ executeRequest
306− $ defaultS3ReqInfo
307− { riMethod = HT.methodDelete,
308− riBucket = Just bucket
309− }
323+ void $
324+ executeRequest $
325+ defaultS3ReqInfo
326+ { riMethod = HT.methodDelete,
327+ riBucket = Just bucket
328+ }
310329
311330-- | DELETE an object from the service.
312331deleteObject :: Bucket -> Object -> Minio ()
313332deleteObject bucket object =
314− void
315− $ executeRequest
316− $ defaultS3ReqInfo
317− { riMethod = HT.methodDelete,
318− riBucket = Just bucket,
319− riObject = Just object
320− }
333+ void $
334+ executeRequest $
335+ defaultS3ReqInfo
336+ { riMethod = HT.methodDelete,
337+ riBucket = Just bucket,
338+ riObject = Just object
339+ }
321340
322341-- | Create a new multipart upload.
323342newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
@@ -370,7 +389,7 @@ putObjectPart bucket object uploadId partNumber headers payload = do
370389srcInfoToHeaders :: SourceInfo -> [HT.Header]
371390srcInfoToHeaders srcInfo =
372391 ( "x-amz-copy-source",
373− toUtf8 $
392+ encodeUtf8 $
374393 T.concat
375394 [ "/",
376395 srcBucket srcInfo,
@@ -396,8 +415,7 @@ srcInfoToHeaders srcInfo =
396415 fmap formatRFC1123 . srcIfModifiedSince
397416 ]
398417 rangeHdr =
399− maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) $
400− toByteRange <$> srcRange srcInfo
418+ maybe [] ((\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) . toByteRange) (srcRange srcInfo)
401419 toByteRange :: (Int64, Int64) -> HT.ByteRange
402420 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
403421
@@ -477,14 +495,14 @@ completeMultipartUpload bucket object uploadId partTuple = do
477495-- | Abort a multipart upload.
478496abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
479497abortMultipartUpload bucket object uploadId =
480− void
481− $ executeRequest
482− $ defaultS3ReqInfo
483− { riMethod = HT.methodDelete,
484− riBucket = Just bucket,
485− riObject = Just object,
486− riQueryParams = mkOptionalParams params
487− }
498+ void $
499+ executeRequest $
500+ defaultS3ReqInfo
501+ { riMethod = HT.methodDelete,
502+ riBucket = Just bucket,
503+ riObject = Just object,
504+ riQueryParams = mkOptionalParams params
505+ }
488506 where
489507 params = [("uploadId", Just uploadId)]
490508
@@ -553,15 +571,16 @@ headObject bucket object reqHeaders = do
553571 { riMethod = HT.methodHead,
554572 riBucket = Just bucket,
555573 riObject = Just object,
556− riHeaders = reqHeaders
557− -- This header is required for safety as otherwise http-client,
558− -- sends Accept-Encoding: gzip, and the server may actually gzip
559− -- body. In that case Content-Length header will be missing.
560− <> [("Accept-Encoding", "identity")]
574+ riHeaders =
575+ reqHeaders
576+ -- This header is required for safety as otherwise http-client,
577+ -- sends Accept-Encoding: gzip, and the server may actually gzip
578+ -- body. In that case Content-Length header will be missing.
579+ <> [("Accept-Encoding", "identity")]
561580 }
562− maybe (throwIO MErrVInvalidObjectInfoResponse) return
563− $ parseGetObjectHeaders object
564− $ NC.responseHeaders resp
581+ maybe (throwIO MErrVInvalidObjectInfoResponse) return $
582+ parseGetObjectHeaders object $
583+ NC.responseHeaders resp
565584
566585-- | Query the object store if a given bucket exists.
567586headBucket :: Bucket -> Minio Bool
@@ -594,15 +613,16 @@ headBucket bucket =
594613putBucketNotification :: Bucket -> Notification -> Minio ()
595614putBucketNotification bucket ncfg = do
596615 ns <- asks getSvcNamespace
597− void $ executeRequest $
598− defaultS3ReqInfo
599− { riMethod = HT.methodPut,
600− riBucket = Just bucket,
601− riQueryParams = [("notification", Nothing)],
602− riPayload =
603− PayloadBS $
604− mkPutNotificationRequest ns ncfg
605− }
616+ void $
617+ executeRequest $
618+ defaultS3ReqInfo
619+ { riMethod = HT.methodPut,
620+ riBucket = Just bucket,
621+ riQueryParams = [("notification", Nothing)],
622+ riPayload =
623+ PayloadBS $
624+ mkPutNotificationRequest ns ncfg
625+ }
606626
607627-- | Retrieve the notification configuration on a bucket.
608628getBucketNotification :: Bucket -> Minio Notification
@@ -644,20 +664,22 @@ setBucketPolicy bucket policy = do
644664-- | Save a new policy on a bucket.
645665putBucketPolicy :: Bucket -> Text -> Minio ()
646666putBucketPolicy bucket policy = do
647− void $ executeRequest $
648− defaultS3ReqInfo
649− { riMethod = HT.methodPut,
650− riBucket = Just bucket,
651− riQueryParams = [("policy", Nothing)],
652− riPayload = PayloadBS $ encodeUtf8 policy
653− }
667+ void $
668+ executeRequest $
669+ defaultS3ReqInfo
670+ { riMethod = HT.methodPut,
671+ riBucket = Just bucket,
672+ riQueryParams = [("policy", Nothing)],
673+ riPayload = PayloadBS $ encodeUtf8 policy
674+ }
654675
655676-- | Delete any policy set on a bucket.
656677deleteBucketPolicy :: Bucket -> Minio ()
657678deleteBucketPolicy bucket = do
658− void $ executeRequest $
659− defaultS3ReqInfo
660− { riMethod = HT.methodDelete,
661− riBucket = Just bucket,
662− riQueryParams = [("policy", Nothing)]
663− }
679+ void $
680+ executeRequest $
681+ defaultS3ReqInfo
682+ { riMethod = HT.methodDelete,
683+ riBucket = Just bucket,
684+ riQueryParams = [("policy", Nothing)]
685+ }
src/Network/Minio/SelectAPI.hs+11−11
@@ -111,7 +111,7 @@ data EventStreamException
111111 | ESEInvalidHeaderType
112112 | ESEInvalidHeaderValueType
113113 | ESEInvalidMessageType
114− deriving (Eq, Show)
114+ deriving stock (Eq, Show)
115115
116116instance Exception EventStreamException
117117
@@ -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
@@ -186,7 +186,7 @@ crcCheck = do
186186 -- 12 bytes have been read off the current message. Now read the
187187 -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
188188 let startCrc = crc32 b
189− finalCrc <- accumulateYield (fromIntegral n -16) startCrc
189+ finalCrc <- accumulateYield (fromIntegral n - 16) startCrc
190190
191191 bs <- readNBytes 4
192192 expectedCrc :: Word32 <- liftIO $ parseBinary bs
@@ -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
@@ -219,7 +219,7 @@ handleMessage = do
219219 hs <- parseHeaders hdrLen
220220
221221 let payloadLen = msgLen - hdrLen - 16
222− getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)
222+ getHdrVal h = fmap snd . find ((h ==) . fst)
223223 eventHdrValue = getHdrVal EventType hs
224224 msgHdrValue = getHdrVal MessageType hs
225225 errCode = getHdrVal ErrorCode hs
@@ -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
@@ -276,12 +276,12 @@ selectObjectContent b o r = do
276276 riNeedsLocation = False,
277277 riQueryParams = [("select", Nothing), ("select-type", Just "2")]
278278 }
279− --print $ mkSelectRequest r
279+ -- print $ mkSelectRequest r
280280 resp <- mkStreamRequest reqInfo
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+241−156
@@ -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,50 +62,31 @@ 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 (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,
68− spExpirySecs :: Maybe Int,
79+ spExpirySecs :: Maybe UrlExpiry,
6980 spPayloadHash :: Maybe ByteString
7081 }
71− deriving (Show)
72−
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 ""
82+ deriving stock (Show)
8983
9084mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
9185mkAuthHeader accessKey scope signedHeaderKeys sign =
9286 let authValue =
9387 B.concat
9488 [ "AWS4-HMAC-SHA256 Credential=",
95− toUtf8 accessKey,
89+ encodeUtf8 accessKey,
9690 "/",
9791 scope,
9892 ", SignedHeaders=",
@@ -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 = toUtf8 $ spAccessKey sp
123− secretKey = toUtf8 $ 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− [ toUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts,
183− toUtf8 region,
184− "s3",
185− "aws4_request"
186− ]
194+ in authHeader : extraHeaders
195+
196+credentialScope :: SignParams -> ByteString
197+credentialScope sp =
198+ let region = fromMaybe "" $ spRegion sp
199+ in B.intercalate
200+ "/"
201+ [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" $ spTimeStamp sp,
202+ encodeUtf8 region,
203+ toByteString $ spService sp,
204+ "aws4_request"
205+ ]
187206
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 =
201− B.intercalate "&"
202− $ map (\(x, y) -> B.concat [x, "=", y])
203− $ sort
204− $ map
205− ( \(x, y) ->
206− (uriEncode True x, maybe "" (uriEncode True) y)
207− )
208− $ (parseQuery $ NC.queryString req)
277+ let httpMethod = NC.method req
278+ canonicalUri = uriEncode False $ NC.path req
279+ canonicalQueryString =
280+ B.intercalate "&" $
281+ map (\(x, y) -> B.concat [x, "=", y]) $
282+ sortBy (\a b -> compare (fst a) (fst b)) $
283+ map
284+ ( bimap (uriEncode True) (maybe "" (uriEncode True))
285+ )
286+ (parseQuery $ NC.queryString req)
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 (toUtf8 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 $ toUtf8 $ 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 = headMay $ filter (\(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 $ toUtf8 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
@@ -365,41 +443,42 @@ signV4Stream !payloadLength !sp !req =
365443 -- 'chunkSizeConstant'.
366444 if
367445 | n > 0 -> do
368− bs <- mustTakeN chunkSizeConstant
369− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
370− nextSign = computeSignature strToSign signingKey
371− chunkBS =
372− toHexStr chunkSizeConstant
373− <> ";chunk-signature="
374− <> nextSign
375− <> "\r\n"
376− <> bs
377− <> "\r\n"
378− C.yield chunkBS
379− signerConduit (n -1) lps nextSign
446+ bs <- mustTakeN chunkSizeConstant
447+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
448+ nextSign = computeSignature strToSign signingKey
449+ chunkBS =
450+ toHexStr chunkSizeConstant
451+ <> ";chunk-signature="
452+ <> nextSign
453+ <> "\r\n"
454+ <> bs
455+ <> "\r\n"
456+ C.yield chunkBS
457+ signerConduit (n - 1) lps nextSign
380458
381459 -- Second case encodes the last chunk which is smaller than
382460 -- 'chunkSizeConstant'
383461 | lps > 0 -> do
384− bs <- mustTakeN $ fromIntegral lps
385− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
386− nextSign = computeSignature strToSign signingKey
387− chunkBS =
388− toHexStr lps <> ";chunk-signature="
389− <> nextSign
390− <> "\r\n"
391− <> bs
392− <> "\r\n"
393− C.yield chunkBS
394− signerConduit 0 0 nextSign
462+ bs <- mustTakeN $ fromIntegral lps
463+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
464+ nextSign = computeSignature strToSign signingKey
465+ chunkBS =
466+ toHexStr lps
467+ <> ";chunk-signature="
468+ <> nextSign
469+ <> "\r\n"
470+ <> bs
471+ <> "\r\n"
472+ C.yield chunkBS
473+ signerConduit 0 0 nextSign
395474
396475 -- Last case encodes the final signature chunk that has no
397476 -- data.
398477 | otherwise -> do
399− let strToSign = chunkStrToSign prevSign (hashSHA256 "")
400− nextSign = computeSignature strToSign signingKey
401− lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
402− C.yield lastChunkBS
478+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")
479+ nextSign = computeSignature strToSign signingKey
480+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
481+ C.yield lastChunkBS
403482 in \src ->
404483 req
405484 { NC.requestHeaders = finalReqHeaders,
@@ -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+45−61
@@ -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) =>
@@ -52,7 +49,7 @@ allocateReadFile ::
5249 m (R.ReleaseKey, Handle)
5350allocateReadFile fp = do
5451 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
55− either (\(e :: IOException) -> throwIO e) (return . (rk,)) hdlE
52+ either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
5653 where
5754 openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
5855 cleanup = either (const $ return ()) IO.hClose
@@ -60,25 +57,25 @@ allocateReadFile fp = do
6057-- | Queries the file size from the handle. Catches any file operation
6158-- exceptions and returns Nothing instead.
6259getFileSize ::
63− (MonadUnliftIO m, R.MonadResource m) =>
60+ (MonadUnliftIO m) =>
6461 Handle ->
6562 m (Maybe Int64)
6663getFileSize h = do
6764 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
6865 case resE of
69− Left (_ :: IOException) -> return Nothing
66+ Left (_ :: U.IOException) -> return Nothing
7067 Right s -> return $ Just s
7168
7269-- | Queries if handle is seekable. Catches any file operation
7370-- exceptions and return False instead.
7471isHandleSeekable ::
75− (R.MonadResource m, MonadUnliftIO m) =>
72+ (R.MonadResource m) =>
7673 Handle ->
7774 m Bool
7875isHandleSeekable h = do
7976 resE <- liftIO $ try $ IO.hIsSeekable h
8077 case resE of
81− Left (_ :: IOException) -> return False
78+ Left (_ :: U.IOException) -> return False
8279 Right v -> return v
8380
8481-- | Helper function that opens a handle to the filepath and performs
@@ -89,7 +86,7 @@ withNewHandle ::
8986 (MonadUnliftIO m, R.MonadResource m) =>
9087 FilePath ->
9188 (Handle -> m a) ->
92− m (Either IOException a)
89+ m (Either U.IOException a)
9390withNewHandle fp fileAction = do
9491 -- opening a handle can throw MError exception.
9592 handleE <- try $ allocateReadFile fp
@@ -103,17 +100,27 @@ 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
109−lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
106+lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
110107
111108getETagHeader :: [HT.Header] -> Maybe Text
112109getETagHeader 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
@@ -143,7 +164,7 @@ getLastModifiedHeader hs = do
143164getContentLength :: [HT.Header] -> Maybe Int64
144165getContentLength hs = do
145166 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
146− fst <$> hush (decimal nbs)
167+ fst <$> either (const Nothing) Just (decimal nbs)
147168
148169decodeUtf8Lenient :: ByteString -> Text
149170decodeUtf8Lenient = decodeUtf8With lenientDecode
@@ -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)
@@ -170,8 +191,9 @@ httpLbs req mgr = do
170191 sErr <- parseErrResponseJSON $ NC.responseBody resp
171192 throwIO sErr
172193 _ ->
173− throwIO $ NC.HttpExceptionRequest req $
174− NC.StatusCodeException (void resp) (showBS resp)
194+ throwIO $
195+ NC.HttpExceptionRequest req $
196+ NC.StatusCodeException (void resp) (showBS resp)
175197
176198 return resp
177199 where
@@ -199,8 +221,9 @@ http req mgr = do
199221 throwIO sErr
200222 _ -> do
201223 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
202− throwIO $ NC.HttpExceptionRequest req $
203− NC.StatusCodeException (void resp) content
224+ throwIO $
225+ NC.HttpExceptionRequest req $
226+ NC.StatusCodeException (void resp) content
204227
205228 return resp
206229 where
@@ -216,7 +239,7 @@ http req mgr = do
216239-- Similar to mapConcurrently but limits the number of threads that
217240-- can run using a quantity semaphore.
218241limitedMapConcurrently ::
219− MonadUnliftIO m =>
242+ (MonadUnliftIO m) =>
220243 Int ->
221244 (t -> m a) ->
222245 [t] ->
@@ -233,7 +256,7 @@ limitedMapConcurrently count act args = do
233256 waitSem t = U.atomically $ do
234257 v <- U.readTVar t
235258 if v > 0
236− then U.writeTVar t (v -1)
259+ then U.writeTVar t (v - 1)
237260 else U.retrySTM
238261 signalSem t = U.atomically $ do
239262 v <- U.readTVar t
@@ -260,42 +283,3 @@ chunkBSConduit (s : ss) = do
260283 | B.length bs == s -> C.yield bs >> chunkBSConduit ss
261284 | B.length bs > 0 -> C.yield bs
262285 | otherwise -> return ()
263−
264−-- | Select part sizes - the logic is that the minimum part-size will
265−-- be 64MiB.
266−selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
267−selectPartSizes size =
268− uncurry (List.zip3 [1 ..])
269− $ List.unzip
270− $ loop 0 size
271− where
272− ceil :: Double -> Int64
273− ceil = ceiling
274− partSize =
275− max
276− minPartSize
277− ( ceil $
278− fromIntegral size
279− / fromIntegral maxMultipartParts
280− )
281− m = fromIntegral partSize
282− loop st sz
283− | st > sz = []
284− | st + m >= sz = [(st, sz - st)]
285− | otherwise = (st, m) : loop (st + m) sz
286−
287−lookupRegionCache :: Bucket -> Minio (Maybe Region)
288−lookupRegionCache b = do
289− rMVar <- asks mcRegionMap
290− rMap <- UM.readMVar rMVar
291− return $ H.lookup b rMap
292−
293−addToRegionCache :: Bucket -> Region -> Minio ()
294−addToRegionCache b region = do
295− rMVar <- asks mcRegionMap
296− UM.modifyMVar_ rMVar $ return . H.insert b region
297−
298−deleteFromRegionCache :: Bucket -> Minio ()
299−deleteFromRegionCache b = do
300− rMVar <- asks mcRegionMap
301− 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+23−22
@@ -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.
@@ -23,10 +23,9 @@ module Network.Minio.XmlGenerator
2323where
2424
2525import qualified Data.ByteString.Lazy as LBS
26−import qualified Data.HashMap.Strict as H
2726import qualified Data.Text as T
28−import Lib.Prelude
2927import Network.Minio.Data
28+import Network.Minio.XmlCommon
3029import Text.XML
3130
3231-- | Create a bucketConfig request body XML
@@ -73,12 +72,13 @@ mkCompleteMultipartUploadRequest partInfo =
7372data XNode
7473 = XNode Text [XNode]
7574 | XLeaf Text Text
76− deriving (Eq, Show)
75+ deriving stock (Eq, Show)
7776
7877toXML :: Text -> XNode -> ByteString
7978toXML ns node =
80− LBS.toStrict $ renderLBS def $
81− Document (Prologue [] Nothing []) (xmlNode node) []
79+ LBS.toStrict $
80+ renderLBS def $
81+ Document (Prologue [] Nothing []) (xmlNode node) []
8282 where
8383 xmlNode :: XNode -> Element
8484 xmlNode (XNode name nodes) =
@@ -94,7 +94,7 @@ class ToXNode a where
9494 toXNode :: a -> XNode
9595
9696instance ToXNode Event where
97− toXNode = XLeaf "Event" . show
97+ toXNode = XLeaf "Event" . toText
9898
9999instance ToXNode Notification where
100100 toXNode (Notification qc tc lc) =
@@ -104,9 +104,10 @@ instance ToXNode Notification where
104104 ++ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
105105
106106toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
107−toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
107+toXNodesWithArnName eltName arnName (NotificationConfig itemId arn events fRule) =
108108 XNode eltName $
109− [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events
109+ [XLeaf "Id" itemId, XLeaf arnName arn]
110+ ++ map toXNode events
110111 ++ [toXNode fRule]
111112
112113instance ToXNode Filter where
@@ -143,14 +144,14 @@ mkSelectRequest r = LBS.toStrict $ renderLBS def sr
143144 [NodeContent $ show $ srExpressionType r]
144145 ),
145146 NodeElement
146− ( Element "InputSerialization" mempty
147− $ inputSerializationNodes
148− $ srInputSerialization r
147+ ( Element "InputSerialization" mempty $
148+ inputSerializationNodes $
149+ srInputSerialization r
149150 ),
150151 NodeElement
151− ( Element "OutputSerialization" mempty
152− $ outputSerializationNodes
153− $ srOutputSerialization r
152+ ( Element "OutputSerialization" mempty $
153+ outputSerializationNodes $
154+ srOutputSerialization r
154155 )
155156 ]
156157 ++ maybe [] reqProgElem (srRequestProgressEnabled r)
@@ -186,11 +187,11 @@ mkSelectRequest r = LBS.toStrict $ renderLBS def sr
186187 ]
187188 comprTypeNode Nothing = []
188189 kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
189− formatNode (InputFormatCSV (CSVProp h)) =
190+ formatNode (InputFormatCSV c) =
190191 Element
191192 "CSV"
192193 mempty
193− (map NodeElement $ map kvElement $ H.toList h)
194+ (map (NodeElement . kvElement) (csvPropsList c))
194195 formatNode (InputFormatJSON p) =
195196 Element
196197 "JSON"
@@ -208,17 +209,17 @@ mkSelectRequest r = LBS.toStrict $ renderLBS def sr
208209 formatNode InputFormatParquet = Element "Parquet" mempty []
209210 outputSerializationNodes (OutputSerializationJSON j) =
210211 [ NodeElement
211− ( Element "JSON" mempty
212− $ rdElem
213− $ jsonopRecordDelimiter j
212+ ( Element "JSON" mempty $
213+ rdElem $
214+ jsonopRecordDelimiter j
214215 )
215216 ]
216− outputSerializationNodes (OutputSerializationCSV (CSVProp h)) =
217+ outputSerializationNodes (OutputSerializationCSV c) =
217218 [ NodeElement $
218219 Element
219220 "CSV"
220221 mempty
221− (map NodeElement $ map kvElement $ H.toList h)
222+ (map (NodeElement . kvElement) (csvPropsList c))
222223 ]
223224 rdElem Nothing = []
224225 rdElem (Just t) =
src/Network/Minio/XmlParser.hs+23−62
@@ -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.
@@ -32,50 +32,13 @@ where
3232
3333import qualified Data.ByteString.Lazy as LB
3434import qualified Data.HashMap.Strict as H
35−import Data.List (zip3, zip4, zip6)
35+import 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
@@ -132,7 +95,7 @@ parseListObjectsV1Response xmldata = do
13295 ns <- asks getSvcNamespace
13396 let s3Elem' = s3Elem ns
13497 hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
135− nextMarker = headMay $ r $/ s3Elem' "NextMarker" &/ content
98+ nextMarker = listToMaybe $ r $/ s3Elem' "NextMarker" &/ content
13699 prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
137100 keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
138101 modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
@@ -158,7 +121,7 @@ parseListObjectsResponse xmldata = do
158121 ns <- asks getSvcNamespace
159122 let s3Elem' = s3Elem ns
160123 hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
161− nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ content
124+ nextToken = listToMaybe $ r $/ s3Elem' "NextContinuationToken" &/ content
162125 prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
163126 keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
164127 modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
@@ -185,8 +148,8 @@ parseListUploadsResponse xmldata = do
185148 let s3Elem' = s3Elem ns
186149 hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
187150 prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
188− nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content
189− nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content
151+ nextKey = listToMaybe $ r $/ s3Elem' "NextKeyMarker" &/ content
152+ nextUpload = listToMaybe $ r $/ s3Elem' "NextUploadIdMarker" &/ content
190153 uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
191154 uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
192155 uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
@@ -203,7 +166,7 @@ parseListPartsResponse xmldata = do
203166 ns <- asks getSvcNamespace
204167 let s3Elem' = s3Elem ns
205168 hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
206− nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content
169+ nextPartNumStr = listToMaybe $ r $/ s3Elem' "NextPartNumberMarker" &/ content
207170 partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
208171 partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
209172 partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
@@ -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
@@ -245,25 +202,29 @@ parseNotification xmldata = do
245202 in FilterRule name value
246203 parseNode ns arnName nodeData = do
247204 let c = fromNode nodeData
248− id = T.concat $ c $/ s3Elem ns "Id" &/ content
205+ 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
256− id
216+ itemId
257217 arn
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+3−5
@@ -15,7 +15,7 @@
1515# resolver:
1616# name: custom-snapshot
1717# location: "./custom-snapshot.yaml"
18−resolver: lts-16.0
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,19 +36,17 @@ resolver: lts-16.0
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)
4242extra-deps:
43−- unliftio-core-0.2.0.1
44−- protolude-0.3.0
43+ - crypton-connection-0.3.2
4544
4645# Override default flag values for local packages and extra-deps
4746flags: {}
4847
4948# Extra package databases containing global packages
5049extra-package-dbs: []
51−
5250# Control whether we use the GHC we find on the path
5351# system-ghc: true
5452#
stack.yaml.lock+8−15
@@ -5,22 +5,15 @@
55
66packages:
77- completed:
8− hackage: unliftio-core-0.2.0.1@sha256:9b3e44ea9aacacbfc35b3b54015af450091916ac3618a41868ebf6546977659a,1082
8+ hackage: crypton-connection-0.3.2@sha256:c7937edc25ab022bcf167703f2ec5ab73b62908e545bb587d2aa42b33cd6f6cc,1581
99 pantry-tree:
10− size: 328
11− sha256: e81c5a1e82ec2cd68cbbbec9cd60567363abe02257fa1370a906f6754b6818b8
10+ sha256: f986ad29b008cbe5732606e9cde1897191c486a2f1f169a4cb75fd915bce397c
11+ size: 394
1212 original:
13− hackage: unliftio-core-0.2.0.1
14−- completed:
15− hackage: protolude-0.3.0@sha256:8361b811b420585b122a7ba715aa5923834db6e8c36309bf267df2dbf66b95ef,2693
16− pantry-tree:
17− size: 1644
18− sha256: babf32b414f25f790b7a4ce6bae5c960bc51a11a289e7c47335b222e6762560c
19− original:
20− hackage: protolude-0.3.0
13+ hackage: crypton-connection-0.3.2
2114snapshots:
2215- completed:
23− size: 531237
24− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/0.yaml
25− sha256: 210e15b7043e2783115afe16b0d54914b1611cdaa73f3ca3ca7f8e0847ff54e5
26− original: lts-16.0
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+217−110
@@ -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,18 +30,18 @@ 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
37−import Network.Minio.PutObject
3836import Network.Minio.S3API
3937import Network.Minio.Utils
4038import System.Directory (getTemporaryDirectory)
41−import System.Environment (lookupEnv)
42−import qualified System.IO as SIO
39+import qualified System.Environment as Env
4340import qualified Test.QuickCheck as Q
4441import Test.Tasty
4542import Test.Tasty.HUnit
4643import Test.Tasty.QuickCheck as QC
44+import qualified UnliftIO.IO as UIO
4745
4846main :: IO ()
4947main = defaultMain tests
@@ -51,9 +49,54 @@ main = defaultMain tests
5149tests :: TestTree
5250tests = testGroup "Tests" [liveServerUnitTests]
5351
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+
5497-- conduit that generates random binary stream of given length
55−randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
56−randomDataSrc s' = genBS s'
98+randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
99+randomDataSrc = genBS
57100 where
58101 concatIt bs n =
59102 BS.concat $
@@ -70,22 +113,32 @@ randomDataSrc s' = genBS s'
70113 yield $ concatIt byteArr64 oneMiB
71114 genBS (s - oneMiB)
72115
73−mkRandFile :: R.MonadResource m => Int64 -> m FilePath
116+mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
74117mkRandFile size = do
75− dir <- liftIO $ getTemporaryDirectory
118+ dir <- liftIO getTemporaryDirectory
76119 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
77120
78121funTestBucketPrefix :: Text
79122funTestBucketPrefix = "miniohstest-"
80123
81−loadTestServer :: IO ConnectInfo
82−loadTestServer = do
83− val <- lookupEnv "MINIO_LOCAL"
84− isSecure <- lookupEnv "MINIO_SECURE"
85− return $ case (val, isSecure) of
86− (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"
87− (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"
88− (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)
89142
90143funTestWithBucket ::
91144 TestName ->
@@ -96,7 +149,7 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
96149 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
97150 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
98151 liftStep = liftIO . step
99− connInfo <- loadTestServer
152+ connInfo <- loadTestServerConnInfo
100153 ret <- runMinio connInfo $ do
101154 liftStep $ "Creating bucket for test - " ++ t
102155 foundBucket <- bucketExists b
@@ -106,6 +159,17 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
106159 deleteBucket b
107160 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
108161
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+
109173liveServerUnitTests :: TestTree
110174liveServerUnitTests =
111175 testGroup
@@ -126,7 +190,8 @@ liveServerUnitTests =
126190 presignedUrlFunTest,
127191 presignedPostPolicyFunTest,
128192 bucketPolicyFunTest,
129− getNPutSSECTest
193+ getNPutSSECTest,
194+ assumeRoleRequestTest
130195 ]
131196
132197basicTests :: TestTree
@@ -134,12 +199,13 @@ basicTests = funTestWithBucket "Basic tests" $
134199 \step bucket -> do
135200 step "getService works and contains the test bucket."
136201 buckets <- getService
137− unless (length (filter (== bucket) $ map biName buckets) == 1)
138− $ liftIO
139− $ assertFailure
140− ( "The bucket " ++ show bucket
141− ++ " was expected to exist."
142− )
202+ unless (length (filter (== bucket) $ map biName buckets) == 1) $
203+ liftIO $
204+ assertFailure
205+ ( "The bucket "
206+ ++ show bucket
207+ ++ " was expected to exist."
208+ )
143209
144210 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
145211 mbE <- try $ makeBucket bucket Nothing
@@ -158,17 +224,18 @@ basicTests = funTestWithBucket "Basic tests" $
158224 liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
159225
160226 step "singlepart putObject works"
161− fPutObject bucket "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
227+ testFilepath <- mkRandFile 200
228+ fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
162229
163230 step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
164− fpE <- try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
231+ fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
165232 case fpE of
166233 Left exn -> liftIO $ exn @?= NoSuchBucket
167234 _ -> return ()
168235
169236 outFile <- mkRandFile 0
170237 step "simple fGetObject works"
171− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions
238+ fGetObject bucket "test-file" outFile defaultGetObjectOptions
172239
173240 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
174241 step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
@@ -176,10 +243,10 @@ basicTests = funTestWithBucket "Basic tests" $
176243 try $
177244 fGetObject
178245 bucket
179− "lsb-release"
246+ "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"
@@ -190,10 +257,10 @@ basicTests = funTestWithBucket "Basic tests" $
190257 try $
191258 fGetObject
192259 bucket
193− "lsb-release"
260+ "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"
@@ -204,10 +271,10 @@ basicTests = funTestWithBucket "Basic tests" $
204271 try $
205272 fGetObject
206273 bucket
207− "lsb-release"
274+ "test-file"
208275 outFile
209276 defaultGetObjectOptions
210− { gooRange = (Just $ HT.ByteRangeFromTo 100 200)
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"
@@ -216,10 +283,10 @@ basicTests = funTestWithBucket "Basic tests" $
216283 step "fGetObject on object with a valid range"
217284 fGetObject
218285 bucket
219− "lsb-release"
286+ "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,13 +297,13 @@ 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
237304
238305 step "delete object works"
239− deleteObject bucket "lsb-release"
306+ deleteObject bucket "test-file"
240307
241308 step "statObject test"
242309 let object = "sample"
@@ -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,13 +328,14 @@ 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
268335 step "put object parts 1 of 1"
269− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
270− partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
336+ partInfo <-
337+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
338+ putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
271339
272340 step "complete multipart"
273341 void $ completeMultipartUpload bucket object uid [partInfo]
@@ -277,7 +345,8 @@ lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
277345 fGetObject bucket object destFile defaultGetObjectOptions
278346 gotSize <- withNewHandle destFile getFileSize
279347 liftIO $
280− gotSize == Right (Just mb15)
348+ gotSize
349+ == Right (Just mb15)
281350 @? "Wrong file size of put file after getting"
282351
283352 step "Cleanup actions"
@@ -301,7 +370,8 @@ putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
301370 fGetObject bucket obj destFile defaultGetObjectOptions
302371 gotSize <- withNewHandle destFile getFileSize
303372 liftIO $
304− gotSize == Right (Just mb1)
373+ gotSize
374+ == Right (Just mb1)
305375 @? "Wrong file size of put file after getting"
306376
307377 step "Cleanup actions"
@@ -325,7 +395,8 @@ putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no siz
325395 fGetObject bucket obj destFile defaultGetObjectOptions
326396 gotSize <- withNewHandle destFile getFileSize
327397 liftIO $
328− gotSize == Right (Just mb70)
398+ gotSize
399+ == Right (Just mb70)
329400 @? "Wrong file size of put file after getting"
330401
331402 step "Cleanup actions"
@@ -336,30 +407,30 @@ highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
336407 \step bucket -> do
337408 step "High-level listObjects Test"
338409 step "put 3 objects"
339− let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
340− extractObjectsFromList os =
410+ let extractObjectsFromList =
341411 mapM
342− ( \t -> case t of
412+ ( \case
343413 ListItemObject o -> Just $ oiObject o
344414 _ -> Nothing
345415 )
346− os
347− expectedNonRecList = ["o4", "dir/"]
348− extractObjectsAndDirsFromList os =
416+ extractObjectsAndDirsFromList =
349417 map
350− ( \t -> case t of
418+ ( \case
351419 ListItemObject o -> oiObject o
352420 ListItemPrefix d -> d
353421 )
354− os
422+ expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
423+ expectedNonRecList = ["o4", "dir/"]
355424
425+ testFilepath <- mkRandFile 200
356426 forM_ expectedObjects $
357− \obj -> fPutObject bucket obj "/etc/lsb-release" defaultPutObjectOptions
427+ \obj -> fPutObject bucket obj testFilepath defaultPutObjectOptions
358428
359429 step "High-level listing of objects"
360430 items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
361− liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
362− extractObjectsAndDirsFromList items
431+ liftIO $
432+ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
433+ extractObjectsAndDirsFromList items
363434
364435 step "High-level recursive listing of objects"
365436 objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
@@ -372,8 +443,9 @@ highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
372443
373444 step "High-level listing of objects (version 1)"
374445 itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
375− liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
376− extractObjectsAndDirsFromList itemsV1
446+ liftIO $
447+ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
448+ extractObjectsAndDirsFromList itemsV1
377449
378450 step "High-level recursive listing of objects (version 1)"
379451 objectsV1 <-
@@ -430,7 +502,7 @@ highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
430502 step "create 10 multipart uploads"
431503 forM_ [1 .. 10 :: Int] $ \_ -> do
432504 uid <- newMultipartUpload bucket object []
433− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
505+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
434506
435507 step "High-level listing of incomplete multipart uploads"
436508 uploads <-
@@ -452,9 +524,9 @@ highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
452524
453525 step "put object parts 1..10"
454526 inputFile <- mkRandFile mb5
455− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
456− forM_ [1 .. 10] $ \pnum ->
457− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
527+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
528+ forM_ [1 .. 10] $ \pnum ->
529+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
458530
459531 step "fetch list parts"
460532 incompleteParts <-
@@ -470,10 +542,11 @@ listingTest :: TestTree
470542listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
471543 step "listObjects' test"
472544 step "put 10 objects"
473− let objects = (\s -> T.concat ["lsb-release", T.pack (show s)]) <$> [1 .. 10 :: Int]
545+ let objects = (\s -> T.concat ["test-file-", T.pack (show s)]) <$> [1 .. 10 :: Int]
474546
547+ testFilepath <- mkRandFile 200
475548 forM_ [1 .. 10 :: Int] $ \s ->
476− fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release" defaultPutObjectOptions
549+ fPutObject bucket (T.concat ["test-file-", T.pack (show s)]) testFilepath defaultPutObjectOptions
477550
478551 step "Simple list"
479552 res <- listObjects' bucket Nothing Nothing Nothing Nothing
@@ -490,8 +563,8 @@ listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
490563 sort $
491564 map
492565 ( T.concat
493− . ("lsb-release" :)
494− . (\x -> [x])
566+ . ("test-file-" :)
567+ . (: [])
495568 . T.pack
496569 . show
497570 )
@@ -510,7 +583,7 @@ listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
510583 let object = "newmpupload"
511584 forM_ [1 .. 10 :: Int] $ \_ -> do
512585 uid <- newMultipartUpload bucket object []
513− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
586+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
514587
515588 step "list incomplete multipart uploads"
516589 incompleteUploads <-
@@ -521,7 +594,7 @@ listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
521594 Nothing
522595 Nothing
523596 Nothing
524− liftIO $ (length $ lurUploads incompleteUploads) @?= 10
597+ liftIO $ length (lurUploads incompleteUploads) @?= 10
525598
526599 step "cleanup"
527600 forM_ (lurUploads incompleteUploads) $
@@ -532,17 +605,17 @@ listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
532605
533606 step "create a multipart upload"
534607 uid <- newMultipartUpload bucket object []
535− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
608+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
536609
537610 step "put object parts 1..10"
538611 inputFile <- mkRandFile mb5
539− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
540− forM_ [1 .. 10] $ \pnum ->
541− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
612+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
613+ forM_ [1 .. 10] $ \pnum ->
614+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
542615
543616 step "fetch list parts"
544617 listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
545− liftIO $ (length $ lprParts listPartsResult) @?= 10
618+ liftIO $ length (lprParts listPartsResult) @?= 10
546619 abortMultipartUpload bucket object uid
547620
548621presignedUrlFunTest :: TestTree
@@ -565,6 +638,7 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
565638 []
566639 []
567640
641+ -- print putUrl
568642 let size1 = 1000 :: Int64
569643 inputFile <- mkRandFile size1
570644
@@ -611,7 +685,7 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
611685 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
612686
613687 headResp <- do
614− let req = NC.parseRequest_ $ toS $ decodeUtf8 headUrl
688+ let req = NC.parseRequest_ $ decodeUtf8 headUrl
615689 NC.httpLbs (req {NC.method = HT.methodHead}) mgr
616690 liftIO $
617691 (NC.responseStatus headResp == HT.status200)
@@ -639,7 +713,7 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
639713 mapM_ (removeObject bucket) [obj, obj2]
640714 where
641715 putR size filePath mgr url = do
642− let req = NC.parseRequest_ $ toS $ decodeUtf8 url
716+ let req = NC.parseRequest_ $ decodeUtf8 url
643717 let req' =
644718 req
645719 { NC.method = HT.methodPut,
@@ -649,14 +723,14 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
649723 }
650724 NC.httpLbs req' mgr
651725 getR mgr url = do
652− let req = NC.parseRequest_ $ toS $ decodeUtf8 url
726+ let req = NC.parseRequest_ $ decodeUtf8 url
653727 NC.httpLbs req mgr
654728
655729presignedPostPolicyFunTest :: TestTree
656−presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
730+presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
657731 \step bucket -> do
658732 step "presignedPostPolicy basic test"
659− now <- liftIO $ Time.getCurrentTime
733+ now <- liftIO Time.getCurrentTime
660734
661735 let key = "presignedPostPolicyTest/myfile"
662736 policyConds =
@@ -685,9 +759,9 @@ presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
685759 mapM_ (removeObject bucket) [key]
686760 where
687761 postForm url formData inputFile = do
688− req <- NC.parseRequest $ toS $ decodeUtf8 url
762+ req <- NC.parseRequest $ decodeUtf8 url
689763 let parts =
690− map (\(x, y) -> Form.partBS x y) $
764+ map (uncurry Form.partBS) $
691765 H.toList formData
692766 parts' = parts ++ [Form.partFile "file" inputFile]
693767 req' <- Form.formDataBody parts' req
@@ -734,17 +808,17 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
734808 [ proto,
735809 getHostAddr connInfo,
736810 "/",
737− toUtf8 bucket,
811+ encodeUtf8 bucket,
738812 "/",
739− toUtf8 obj
813+ encodeUtf8 obj
740814 ]
741815 respE <-
742816 liftIO $
743− (fmap (Right . toStrictBS) $ NC.simpleHttp $ toS $ decodeUtf8 url)
817+ fmap (Right . toStrictBS) (NC.simpleHttp $ decodeUtf8 url)
744818 `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
745819 case respE of
746820 Left err -> liftIO $ assertFailure $ show err
747− Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")
821+ Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
748822
749823 deleteObject bucket obj
750824
@@ -754,26 +828,28 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
754828multipartTest :: TestTree
755829multipartTest = funTestWithBucket "Multipart Tests" $
756830 \step bucket -> do
757− step "Prepare for putObjectInternal with non-seekable file, with size."
758− step "Upload multipart file."
759− let mb80 = 80 * 1024 * 1024
760− obj = "mpart"
831+ -- Commenting out test since it's platform specific.
832+ -- FIXME: Need to find a platform agnostic way to test this.
833+ -- step "Prepare for putObjectInternal with non-seekable file, with size."
834+ -- step "Upload multipart file."
835+ -- let mb80 = 80 * 1024 * 1024
836+ -- obj = "mpart"
761837
762− void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
838+ -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
763839
764− step "Retrieve and verify file size"
765− destFile <- mkRandFile 0
766− fGetObject bucket obj destFile defaultGetObjectOptions
767− gotSize <- withNewHandle destFile getFileSize
768− liftIO $
769− gotSize == Right (Just mb80)
770− @? "Wrong file size of put file after getting"
840+ -- step "Retrieve and verify file size"
841+ -- destFile <- mkRandFile 0
842+ -- fGetObject bucket obj destFile defaultGetObjectOptions
843+ -- gotSize <- withNewHandle destFile getFileSize
844+ -- liftIO $
845+ -- gotSize == Right (Just mb80)
846+ -- @? "Wrong file size of put file after getting"
771847
772− step "Cleanup actions"
773− removeObject bucket obj
848+ -- step "Cleanup actions"
849+ -- removeObject bucket obj
774850
775− step "cleanup"
776− removeObject bucket "big"
851+ -- step "cleanup"
852+ -- removeObject bucket "big"
777853
778854 step "Prepare for removeIncompleteUpload"
779855 -- low-level multipart operation tests.
@@ -788,8 +864,8 @@ multipartTest = funTestWithBucket "Multipart Tests" $
788864
789865 step "upload 2 parts"
790866 forM_ [1, 2] $ \partNum -> do
791− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
792− void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
867+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
868+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
793869
794870 step "remove ongoing upload"
795871 removeIncompleteUpload bucket object
@@ -797,7 +873,7 @@ multipartTest = funTestWithBucket "Multipart Tests" $
797873 C.runConduit $
798874 listIncompleteUploads bucket (Just object) False
799875 C..| sinkList
800− liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
876+ liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
801877
802878putObjectContentTypeTest :: TestTree
803879putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
@@ -904,8 +980,9 @@ putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
904980 let m = oiUserMetadata oi
905981 -- need to do a case-insensitive comparison
906982 sortedMeta =
907− sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
908− H.toList m
983+ sort $
984+ map (bimap T.toLower T.toLower) $
985+ H.toList m
909986 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
910987
911988 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -938,8 +1015,9 @@ getObjectTest = funTestWithBucket "getObject test" $
9381015 let m = oiUserMetadata $ gorObjectInfo gor
9391016 -- need to do a case-insensitive comparison
9401017 sortedMeta =
941− sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
942− H.toList m
1018+ sort $
1019+ map (bimap T.toLower T.toLower) $
1020+ H.toList m
9431021 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
9441022
9451023 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -1067,7 +1145,7 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
10671145 copyObjectPart
10681146 dstInfo'
10691147 srcInfo'
1070− { srcRange = Just $ (,) ((p -1) * mb5) ((p -1) * mb5 + (mb5 - 1))
1148+ { srcRange = Just $ (,) ((p - 1) * mb5) ((p - 1) * mb5 + (mb5 - 1))
10711149 }
10721150 uid
10731151 (fromIntegral p)
@@ -1168,9 +1246,38 @@ getNPutSSECTest =
11681246
11691247 gotSize <- withNewHandle dstFile getFileSize
11701248 liftIO $
1171− gotSize == Right (Just mb1)
1249+ gotSize
1250+ == Right (Just mb1)
11721251 @? "Wrong file size of object when getting"
11731252
11741253 step "Cleanup"
11751254 deleteObject bucket obj
11761255 else step "Skipping encryption test as server is not using TLS"
1256+
1257+assumeRoleRequestTest :: TestTree
1258+assumeRoleRequestTest = testCaseSteps "Assume Role STS API" $ \step -> do
1259+ step "Load credentials"
1260+ (_, (ak, sk)) <- lookupTestEnv
1261+ val <- Env.lookupEnv "MINIO_LOCAL"
1262+ isSecure <- Env.lookupEnv "MINIO_SECURE"
1263+ let localMinioCred = Just $ CredentialValue ak sk mempty
1264+ playCreds =
1265+ case connectCreds minioPlayCI of
1266+ CredsStatic c -> Just c
1267+ _ -> Nothing
1268+ (cvMay, loc) =
1269+ case (val, isSecure) of
1270+ (Just _, Just _) -> (localMinioCred, "https://localhost:9000")
1271+ (Just _, Nothing) -> (localMinioCred, "http://localhost:9000")
1272+ (Nothing, _) -> (playCreds, "https://play.min.io:9000")
1273+ cv <- maybe (assertFailure "bad creds") return cvMay
1274+ let assumeRole =
1275+ STSAssumeRole cv $
1276+ defaultSTSAssumeRoleOptions
1277+ { saroLocation = Just "us-east-1",
1278+ saroEndpoint = Just loc
1279+ }
1280+ step "AssumeRole request"
1281+ res <- requestSTSCredential assumeRole
1282+ let v = credentialValueText $ fst res
1283+ print (v, snd res)
test/Network/Minio/API/Test.hs+9−7
@@ -24,7 +24,6 @@ module Network.Minio.API.Test
2424where
2525
2626import Data.Aeson (eitherDecode)
27−import Lib.Prelude
2827import Network.Minio.API
2928import Network.Minio.AdminAPI
3029import Test.Tasty
@@ -63,8 +62,9 @@ parseServerInfoJSONTest =
6362 testGroup "Parse MinIO Admin API ServerInfo JSON test" $
6463 map
6564 ( \(tName, tDesc, tfn, tVal) ->
66− testCase tName $ assertBool tDesc $
67− tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
65+ testCase tName $
66+ assertBool tDesc $
67+ tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
6868 )
6969 testCases
7070 where
@@ -82,8 +82,9 @@ parseHealStatusTest =
8282 testGroup "Parse MinIO Admin API HealStatus JSON test" $
8383 map
8484 ( \(tName, tDesc, tfn, tVal) ->
85− testCase tName $ assertBool tDesc $
86− tfn (eitherDecode tVal :: Either [Char] HealStatus)
85+ testCase tName $
86+ assertBool tDesc $
87+ tfn (eitherDecode tVal :: Either [Char] HealStatus)
8788 )
8889 testCases
8990 where
@@ -101,8 +102,9 @@ parseHealStartRespTest =
101102 testGroup "Parse MinIO Admin API HealStartResp JSON test" $
102103 map
103104 ( \(tName, tDesc, tfn, tVal) ->
104− testCase tName $ assertBool tDesc $
105− tfn (eitherDecode tVal :: Either [Char] HealStartResp)
105+ testCase tName $
106+ assertBool tDesc $
107+ tfn (eitherDecode tVal :: Either [Char] HealStartResp)
106108 )
107109 testCases
108110 where
test/Network/Minio/JsonParser/Test.hs+4−4
@@ -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
@@ -43,9 +43,9 @@ testParseErrResponseJSON :: Assertion
4343testParseErrResponseJSON = do
4444 -- 1. Test parsing of an invalid error json.
4545 parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
46− when (isRight parseResE)
47− $ assertFailure
48− $ "Parsing should have failed => " ++ show parseResE
46+ when (isRight parseResE) $
47+ assertFailure $
48+ "Parsing should have failed => " ++ show parseResE
4949
5050 forM_ cases $ \(jsondata, sErr) -> do
5151 parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
test/Network/Minio/TestHelpers.hs−1
@@ -19,7 +19,6 @@ module Network.Minio.TestHelpers
1919 )
2020where
2121
22−import Lib.Prelude
2322import Network.Minio.Data
2423
2524newtype TestNS = TestNS {testNamespace :: Text}
test/Network/Minio/Utils/Test.hs−1
@@ -19,7 +19,6 @@ module Network.Minio.Utils.Test
1919 )
2020where
2121
22−import Lib.Prelude
2322import Network.Minio.Utils
2423import Test.Tasty
2524import Test.Tasty.HUnit
test/Network/Minio/XmlGenerator/Test.hs+38−29
@@ -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 =
@@ -90,11 +92,12 @@ testMkPutNotificationRequest =
9092 "1"
9193 "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
9294 [ObjectCreatedPut]
93− ( Filter $ FilterKey $
94− FilterRules
95− [ FilterRule "prefix" "images/",
96− FilterRule "suffix" ".jpg"
97− ]
95+ ( Filter $
96+ FilterKey $
97+ FilterRules
98+ [ FilterRule "prefix" "images/",
99+ FilterRule "suffix" ".jpg"
100+ ]
98101 ),
99102 NotificationConfig
100103 ""
@@ -119,7 +122,13 @@ testMkPutNotificationRequest =
119122testMkSelectRequest :: Assertion
120123testMkSelectRequest = mapM_ assertFn cases
121124 where
122− 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
123132 cases =
124133 [ ( SelectRequest
125134 "Select * from S3Object"
@@ -142,32 +151,32 @@ testMkSelectRequest = mapM_ assertFn cases
142151 <> quoteEscapeCharacter "\""
143152 )
144153 (Just False),
145− [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><QuoteCharacter>&#34;</QuoteCharacter><RecordDelimiter>
146−</RecordDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
147−</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
154+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><FieldDelimiter>,</FieldDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><RecordDelimiter>
155+</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
156+</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
148157 ),
149− ( setRequestProgressEnabled False
150− $ setInputCompressionType CompressionTypeGzip
151− $ selectRequest
152− "Select * from S3Object"
153− documentJsonInput
154− (outputJSONFromRecordDelimiter "\n"),
158+ ( setRequestProgressEnabled False $
159+ setInputCompressionType CompressionTypeGzip $
160+ selectRequest
161+ "Select * from S3Object"
162+ documentJsonInput
163+ (outputJSONFromRecordDelimiter "\n"),
155164 [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter>
156165</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
157166 ),
158− ( setRequestProgressEnabled False
159− $ setInputCompressionType CompressionTypeNone
160− $ selectRequest
161− "Select * from S3Object"
162− defaultParquetInput
163− ( outputCSVFromProps $
164− quoteFields QuoteFieldsAsNeeded
165− <> recordDelimiter "\n"
166− <> fieldDelimiter ","
167− <> quoteCharacter "\""
168− <> quoteEscapeCharacter "\""
169− ),
170− [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
171−</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
167+ ( setRequestProgressEnabled False $
168+ setInputCompressionType CompressionTypeNone $
169+ selectRequest
170+ "Select * from S3Object"
171+ defaultParquetInput
172+ ( outputCSVFromProps $
173+ quoteFields QuoteFieldsAsNeeded
174+ <> recordDelimiter "\n"
175+ <> fieldDelimiter ","
176+ <> quoteCharacter "\""
177+ <> quoteEscapeCharacter "\""
178+ ),
179+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
180+</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
172181 )
173182 ]
test/Network/Minio/XmlParser/Test.hs+10−9
@@ -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
@@ -62,9 +62,9 @@ testParseLocation :: Assertion
6262testParseLocation = do
6363 -- 1. Test parsing of an invalid location constraint xml.
6464 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
65− when (isRight parseResE)
66− $ assertFailure
67− $ "Parsing should have failed => " ++ show parseResE
65+ when (isRight parseResE) $
66+ assertFailure $
67+ "Parsing should have failed => " ++ show parseResE
6868
6969 forM_ cases $ \(xmldata, expectedLocation) -> do
7070 parseLocE <- tryValidationErr $ parseLocation xmldata
@@ -344,11 +344,12 @@ testParseNotification = do
344344 "1"
345345 "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
346346 [ObjectCreatedPut]
347− ( Filter $ FilterKey $
348− FilterRules
349− [ FilterRule "prefix" "images/",
350− FilterRule "suffix" ".jpg"
351− ]
347+ ( Filter $
348+ FilterKey $
349+ FilterRules
350+ [ FilterRule "prefix" "images/",
351+ FilterRule "suffix" ".jpg"
352+ ]
352353 ),
353354 NotificationConfig
354355 ""
test/Spec.hs+18−16
@@ -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,31 +54,33 @@ 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
7271 isMinPartSizeOk =
7372 if
7473 | nparts > 1 -> -- last part can be smaller but > 0
75− all (>= minPartSize) (take (nparts - 1) sizes)
76− && all (\s -> s > 0) (drop (nparts - 1) sizes)
74+ all (>= minPartSize) (take (nparts - 1) sizes)
75+ && all (> 0) (drop (nparts - 1) sizes)
7776 | nparts == 1 -> -- size may be 0 here.
78− maybe True (\x -> x >= 0 && x <= minPartSize) $
79− headMay sizes
77+ maybe True (\x -> x >= 0 && x <= minPartSize) $
78+ listToMaybe sizes
8079 | otherwise -> False
8180 in n < 0
82− || ( isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk
81+ || ( isPNumsAscendingFrom1
82+ && isOffsetsAsc
83+ && isSumSizeOk
8384 && isSizesConstantExceptLast
8485 && isMinPartSizeOk
8586 ),
@@ -89,23 +90,24 @@ qcProps =
8990 -- is last part's snd offset end?
9091 isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
9192 -- is first part's fst offset start
92− isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
93+ 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)) $ initMay pairs
96+ initSizes = maybe [] (map (\(a, b) -> b - a + 1) . init) (nonEmpty pairs)
9697 isPartSizesOk =
9798 all (>= minPartSize) initSizes
9899 && maybe
99100 True
100101 (\k -> all (== k) initSizes)
101− (headMay initSizes)
102+ (listToMaybe initSizes)
102103 -- returned offsets are contiguous.
103104 fsts = drop 1 $ map fst pairs
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 ->
test/cert/private.keynew file+28
@@ -0,0 +1,28 @@
1+-----BEGIN PRIVATE KEY-----
2+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC3G9IiC+adjf0p
3+i/2KYc+4dizeuzUFN7wraSdhiOMdQgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwI
4+rPJ61dRGQSuN12l+mzngFJQjE0sysZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPY
5+fhcUcq03onMGq44yOfE6mIhoe0Y9wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq
6+5KjHL8WW2vGg7G9edpYdxINA/A2fdLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L
7+9yRqItqddriRxJFwOXb5OPW8xx2WGaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A8
8+4TD/RXMbAgMBAAECggEBAJ7r1oUWLyGvinn0tijUm6RNbMQjVvEgXoCO008jr3pF
9+PqxVpgEMrOa/4tmwFBus0jcCNF4t3r2zhddBw3I5A/O1vEdvHnBz6NdDBQ8sP6fP
10+1fF50iEe1Y2MBibQkXFxxVMG2QRB1Gt5nuvXA9ELdqtCovK3EsMk5ukkWb/UvjH5
11+8hcmQsaSqvzFEF4wJSY2mkeGSGIJTphPhhuA22xbhaBMInQyhZu8EHsn0h6s/Wgy
12+C4Cp2+4qZTKaaf6x3/ZjJ8CuKiSX+ZsJKjOEv8sqx7j/Y7QFOmJPewInKDhwazr/
13+xIK+N0KXPbUzeSEz6ZvExNDTxtR5ZlQP2UrRDg28yQECgYEA4Is1O2BvKVzNFOkj
14+bTVz25a/bb0Xrcfgi0Y9rdfLzlNdItFjAkxLTVRSW2Hv9ICl0RDDAG+wTlktXRdh
15+rfvDjwG2CvLQo1VEdMWTTkKVg03SwMEy2hFiWV69lENFGSaY8Y6unZDbia5HQinA
16+EgSS4sCojS+a2jtzG5FVVHJDKlkCgYEA0MKhMhD4SUhr2y1idPBrmLxuW5mVozuW
17+8bYaBeSzmfS0BRsN4fP9JGODPBPDdNbfGfGC9ezWLgD/lmCgjIEyBOq8EmqWSsiS
18+Kihds1+Z7hXtbzGsFGAFJJTIh7blBCsK5QFuyuih2UG0fL9z6K/dy+UUJkzrYqph
19+vSfKixyM8pMCgYEAmUPLsNyw4325aeV8TeWnUCJERaZFDFQa21W1cfyS2yEhuEtN
20+llr3JzBACqn9vFk3VU1onNqfb8sE4L696KCpKeqUFEMK0AG6eS4Gzus53Gb5TKJS
21+kHA/PhshsZp9Bp7G1FJ8s4YVo5N2hh2zQVkn3Wh9Y+kzfHQJrK51nO9lEvkCgYBi
22+BuKWle1gzAcJdnhDHRoJMIJJtQbVDYhFnBMALXJAmu1lcFzGe0GlMq1PKqCfXr6I
23+eiXawQmZtJJP1LPPBmOsd2U06KQGHcS00xucvQmVCOrjSdnZ/3SqxsqbH8DOgj+t
24+ZUzXLwHA+N99rJEK9Hob4kfh7ECjpgobPnIXfKKazQKBgQChAuiXHtf/Qq18hY3u
25+x48zFWjGgfd6GpOBZYkXOwGdCJgnYjZbE26LZEnYbwPh8ZUA2vp7mgHRJkD5e3Fj
26+ERuJLCw86WqyYZmLEuBciYGjCZqR5nbavfwsziWD00jeNruds2ZwKxRfFm4V7o2S
27+WLd/RUatd2Uu9f3B2J78OUdnxg==
28+-----END PRIVATE KEY-----
test/cert/public.crtnew file+19
@@ -0,0 +1,19 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDCzCCAfOgAwIBAgIUaIUOMI78LCu+r1zl0mmFHK8n5/AwDQYJKoZIhvcNAQEL
3+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE5MTAyNDE5NTMxOVoYDzIxMTkw
4+OTMwMTk1MzE5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
5+AQUAA4IBDwAwggEKAoIBAQC3G9IiC+adjf0pi/2KYc+4dizeuzUFN7wraSdhiOMd
6+QgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwIrPJ61dRGQSuN12l+mzngFJQjE0sy
7+sZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPYfhcUcq03onMGq44yOfE6mIhoe0Y9
8+wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq5KjHL8WW2vGg7G9edpYdxINA/A2f
9+dLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L9yRqItqddriRxJFwOXb5OPW8xx2W
10+GaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A84TD/RXMbAgMBAAGjUzBRMB0GA1Ud
11+DgQWBBSEWXQ2JRD+OK7/KTmlD+OW16pGmzAfBgNVHSMEGDAWgBSEWXQ2JRD+OK7/
12+KTmlD+OW16pGmzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCF
13+0zYRaabB3X0jzGI9/Lr3Phrb90GvoL1DFLRuiOuTlDkz0vrm/HrZskwHCgMNrkCj
14+OTD9Vpas4D1QZBbQbRzfnf3OOoG4bgmcCwLFZl3dy27yIDAhrmbUP++g9l1Jmy4v
15+vBR/M4lt2scQ8LcZYEPqhEaE5EzFQEjtaxDcKdWDNKY9W1NUzSIABhF9eHiAUNdH
16+AFNJlYeBlCHxcWIeqgon184Dqp/CsvKtz3z3Ni+rlwPM/zuJCFHh1VF+z++0LJjG
17+roBCV0Tro4XyiEz9yp7Cb5kQYMaj1KL9TqBG0tZx0pmv7y+lXc4TT6DEllXz6USy
18+rbIba9/uUet3BqeIMTqj
19+-----END CERTIFICATE-----