git.iko.soy/typeable/minio-hs
on master help sign in
71 files changed+9121−3607
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml+266 .gitignore+1 .travis.yml−40 CHANGELOG.md+115−1 CNAME+1 README.md+128−43 docs/API.md+352−227 examples/AssumeRole.hs+47 examples/BucketExists.hs+5−8 examples/CopyObject.hs+17−21 examples/FileUploader.hs+31−31 Setup.hs → examples/GetConfig.hs+15−3 examples/GetObject.hs+11−15 examples/HeadObject.hs+11−14 examples/Heal.hs+37 examples/ListBuckets.hs+6−8 examples/ListIncompleteUploads.hs+20−23 examples/ListObjects.hs+16−20 examples/Makebucket.hs → examples/MakeBucket.hs+10−14 examples/PresignedGetObject.hs+39−34 examples/PresignedPostPolicy.hs+45−42 examples/PresignedPutObject.hs+23−24 examples/PutObject.hs+16−18 examples/README.md+17 examples/Removebucket.hs → examples/RemoveBucket.hs+6−11 examples/RemoveIncompleteUpload.hs+10−13 examples/RemoveObject.hs+9−10 examples/SelectObject.hs+47 examples/ServerInfo.hs+30 examples/ServiceSendRestart.hs+29 examples/ServiceSendStop.hs+29 examples/ServiceStatus.hs+30 examples/SetConfig.hs+31 minio-hs.cabal+283−182 src/Lib/Prelude.hs+34−25 src/Network/Minio.hs+280−160 src/Network/Minio/API.hs+284−140 src/Network/Minio/APICommon.hs+80 src/Network/Minio/AdminAPI.hs+711 src/Network/Minio/CopyObject.hs+106 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+1038−330 src/Network/Minio/Data/ByteString.hs+20−19 src/Network/Minio/Data/Crypto.hs+39−28 src/Network/Minio/Data/Time.hs+18−14 src/Network/Minio/Errors.hs+59−45 src/Network/Minio/JsonParser.hs+49 src/Network/Minio/ListOps.hs+120−38 src/Network/Minio/PresignedOperations.hs+221−147 src/Network/Minio/PutObject.hs+102−160 src/Network/Minio/S3API.hs+544−281 src/Network/Minio/SelectAPI.hs+294 src/Network/Minio/Sign/V4.hs+446−199 src/Network/Minio/Utils.hs+167−87 src/Network/Minio/XmlCommon.hs+65 src/Network/Minio/XmlGenerator.hs+183−54 src/Network/Minio/XmlParser.hs+142−154 stack.yaml+4−4 stack.yaml.lock+19 test/LiveServer.hs+1135−478 test/Network/Minio/API/Test.hs+91−25 test/Network/Minio/JsonParser/Test.hs+63 test/Network/Minio/TestHelpers.hs+32 test/Network/Minio/Utils/Test.hs+14−16 test/Network/Minio/XmlGenerator/Test.hs+148−61 test/Network/Minio/XmlParser/Test.hs+319−274 test/Spec.hs+81−66 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
.gitignore+1
@@ -17,3 +17,4 @@ cabal.sandbox.config
1717*.eventlog
1818.stack-work/
1919cabal.project.local
20+*~
.travis.ymldeleted−40
@@ -1,40 +0,0 @@
1−# This is the simple Travis configuration, which is intended for use
2−# on applications which do not require cross-platform and
3−# multiple-GHC-version support. For more information and other
4−# options, see:
5−#
6−# https://docs.haskellstack.org/en/stable/travis_ci/
7−#
8−# Copy these contents into the root directory of your Github project in a file
9−# named .travis.yml
10−
11−# Use new container infrastructure to enable caching
12−sudo: false
13−
14−# Do not choose a language; we provide our own build tools.
15−language: generic
16−
17−# Caching so the next build will be fast too.
18−cache:
19− directories:
20− - $HOME/.stack
21−
22−# Ensure necessary system libraries are present
23−addons:
24− apt:
25− packages:
26− - libgmp-dev
27−
28−before_install:
29−# Download and unpack the stack executable
30−- mkdir -p ~/.local/bin
31−- export PATH=$HOME/.local/bin:$PATH
32−- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
33−
34−install:
35−# Build dependencies
36−- stack --no-terminal --install-ghc test --only-dependencies
37−
38−script:
39−# Build the package, its tests, and its docs and run the tests
40−- stack --no-terminal test --haddock --no-haddock-deps --test-arguments --num-threads=1
CHANGELOG.md+115−1
@@ -1,6 +1,120 @@
11Changelog
22==========
33
4+## Version 1.7.0 -- Unreleased
5+
6+* Fix data type `EventMessage` to not export partial fields (#179)
7+* Bump up min bound on time dep and fix deprecation warnings (#181)
8+* Add `dev` flag to cabal for building with warnings as errors (#182)
9+* Fix AWS region map (#185)
10+* Fix XML generator tests (#187)
11+* Add support for STS Assume Role API (#188)
12+
13+### Breaking changes in 1.7.0
14+
15+* `Credentials` type has been removed. Use `CredentialValue` instead.
16+* `Provider` type has been replaced with `CredentialLoader`.
17+* `EventMessage` data type is updated.
18+
19+## Version 1.6.0
20+
21+* HLint fixes - some types were changed to newtype (#173)
22+* Fix XML generation test for S3 SELECT (#161)
23+* Use region specific endpoints for AWS S3 in presigned Urls (#164)
24+* Replace protolude with relude and build with GHC 9.0.2 (#168)
25+* Support aeson 2 (#169)
26+* CI updates and code formatting changes with ormolu 0.5.0.0
27+
28+## Version 1.5.3
29+
30+* Fix windows build
31+* Fix support for Yandex Storage (#147)
32+* Fix for HEAD requests to S3/Minio (#155)
33+* Bump up some dependencies, new code formatting, Github CI, example fixes and other minor improvements.
34+
35+## Version 1.5.2
36+
37+* Fix region `us-west-2` for AWS S3 (#139)
38+* Build examples in CI
39+* Disable live-server tests by default, but run them in CI
40+* Drop support for GHC 8.2.x
41+
42+## Version 1.5.1
43+
44+* Add support for GHC 8.8
45+
46+## Version 1.5.0
47+
48+* Switch to faster map data type - all previous usage of
49+ Data.Map.Strict and Data.Set is replaced with Data.HashMap.Strict
50+ and Data.HashSet.
51+* Add `oiUserMetadata` to parse and return user metadata stored with
52+ an object.
53+* Add `GetObjectResponse` data type for the value returned by
54+ `getObject`. It now contains parsed ObjectInfo along with the
55+ conduit of object bytes.
56+
57+## Version 1.4.0
58+
59+* Expose runMinioRes and runMinioResWith (#129)
60+* Improve Haddocks (#127)
61+* Fix list objects APIs to return directory prefixes when run with
62+ recurse set to False (#126)
63+* Use streaming signature for streaming payloads when on an insecure
64+ connection (#123)
65+
66+## Version 1.3.1
67+
68+* Add TLS helpers to check if server uses TLS, and to disable
69+ certificate validation for easier testing (#121)
70+
71+## Version 1.3.0
72+
73+* Retry requests that timeout using full-jitter backoff (#119)
74+* Accept GetObjectOptions in statObject (#112)
75+* Add encryption options to GetObjectOptions and PutObjectOptions (#111)
76+* Add missing Haddock documentation (#110)
77+* Add support for S3Select API (#108)
78+* New travis with support for multiple GHCs (#106)
79+* Fix region setting in presigned url functions (#107)
80+
81+## Version 1.2.0
82+
83+* Export Provider and findFirst to look for credentials (#103)
84+
85+## Version 1.1.0
86+
87+This version brings the following changes:
88+
89+* Adds experimental Admin APIs (#88, #91, #93, #94, #95, #100)
90+* Adds support for using Google Compute Storage service when S3
91+ compatibility mode is enabled (#96, #99)
92+
93+This version also brings some breaking changes (via #101):
94+
95+* Adds IsString instance to load server address, and updates
96+ initialization API to be more user friendly
97+* Drops usage of data-default package and exposes explicit default
98+ values for various types used in the library.
99+
100+## Version 1.0.1
101+
102+This version brings the following (non-breaking) changes:
103+
104+* Remove dependency on text-format (#86)
105+* Remove direct dependency on exceptions (#87)
106+* Adds lower-bounds on dependencies.
107+
108+## Version 1.0.0
109+
110+This new release changes the following APIs to add new capabilities:
111+
112+* Copy Object API now supports more options for source and destination (#73)
113+* get/put Object functions now support a wider set of options via a
114+ separate settings parameter (#71, #72)
115+* getBucketPolicy and setBucketPolicy APIs are added (#82)
116+* The library now uses UnliftIO (#83)
117+
4118## Version 0.3.2
5119
6120This release brings the following changes:
@@ -19,7 +133,7 @@ This release brings the following changes:
19133This is a bug-fix release:
20134
21135* Fix concurrency bug in `limitedMapConcurrently` (#53)
22−* Fix tests related to listing incomplete uploads to accommodate Minio
136+* Fix tests related to listing incomplete uploads to accommodate MinIO
23137 server's changed behaviour to not list incomplete uploads. Note that
24138 running these tests against AWS S3 are expected to fail. (#54)
25139
CNAMEnew file+1
@@ -0,0 +1 @@
1+minio-hs.min.io
README.md+128−43
@@ -1,64 +1,94 @@
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.minio.io/slack?type=svg)](https://slack.minio.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://minio.io) and Amazon S3 compatible object storage server.
3+The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
44
5−## Minimum Requirements
6−
7−- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
5+This guide assumes that you have a working [Haskell development environment](https://www.haskell.org/downloads/).
86
97## Installation
108
11−```sh
12−git clone https://github.com/minio/minio-hs.git
9+### Add to your project
1310
14−cd minio-hs/
11+Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
1512
16−stack install
17−```
13+### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
1814
19−Tests can be run with:
15+#### For a cabal based environment
2016
21−```sh
17+Download the library source and change to the extracted directory:
18+
19+``` sh
20+$ cabal get minio-hs
21+$ cd minio-hs-1.6.0/ # directory name could be different
22+```
2223
23−stack test
24+Then load the `ghci` REPL environment with the library and browse the available APIs:
2425
26+``` sh
27+$ cabal repl
28+ghci> :browse Network.Minio
2529```
2630
27−A section of the tests use the remote Minio Play server at
28−`https://play.minio.io:9000` by default. For library development,
29−using this remote server maybe slow. To run the tests against a
30−locally running Minio live server at `http://localhost:9000`, just set
31−the environment `MINIO_LOCAL` to any value (and unset it to switch
32−back to Play).
31+#### For a stack based environment
3332
34−Documentation can be locally built with:
33+From your home folder or any non-haskell project directory, just run:
3534
3635```sh
36+stack install minio-hs
37+```
3738
38−stack haddock
39+Then start an interpreter session and browse the available APIs with:
3940
41+```sh
42+$ stack ghci
43+> :browse Network.Minio
4044```
4145
42−## Quick-Start Example - File Uploader
46+## Examples
47+
48+The [examples](https://github.com/minio/minio-hs/tree/master/examples) folder contains many examples that you can try out and use to learn and to help with developing your own projects.
49+
50+### Quick-Start Example - File Uploader
51+
52+This example program connects to a MinIO object storage server, makes a bucket on the server and then uploads a file to the bucket.
53+
54+We will use the MinIO server running at https://play.min.io in this example. Feel free to use this service for testing and development. Access credentials are present in the library and are open to the public.
4355
4456### FileUploader.hs
4557``` haskell
4658#!/usr/bin/env stack
47−-- stack --resolver lts-9.1 runghc --package minio-hs --package optparse-applicative --package filepath
59+-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
60+
61+--
62+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
63+--
64+-- Licensed under the Apache License, Version 2.0 (the "License");
65+-- you may not use this file except in compliance with the License.
66+-- You may obtain a copy of the License at
67+--
68+-- http://www.apache.org/licenses/LICENSE-2.0
69+--
70+-- Unless required by applicable law or agreed to in writing, software
71+-- distributed under the License is distributed on an "AS IS" BASIS,
72+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
73+-- See the License for the specific language governing permissions and
74+-- limitations under the License.
75+--
4876
49−{-# Language OverloadedStrings, ScopedTypeVariables #-}
50−import Network.Minio
5177
52−import Control.Monad.Catch (catchIf)
53−import Control.Monad.IO.Class (liftIO)
54−import Data.Monoid ((<>))
55−import Data.Text (pack)
56−import Options.Applicative
57−import Prelude
58−import System.FilePath.Posix
78+{-# LANGUAGE OverloadedStrings #-}
79+{-# LANGUAGE ScopedTypeVariables #-}
80+import Network.Minio
81+
82+import Data.Monoid ((<>))
83+import Data.Text (pack)
84+import Options.Applicative
85+import System.FilePath.Posix
86+import UnliftIO (throwIO, try)
87+
88+import Prelude
5989
6090-- | The following example uses minio's play server at
61−-- https://play.minio.io:9000. The endpoint and associated
91+-- https://play.min.io. The endpoint and associated
6292-- credentials are provided via the libary constant,
6393--
6494-- > minioPlayCI :: ConnectInfo
@@ -68,7 +98,7 @@ import System.FilePath.Posix
6898fileNameArgs :: Parser FilePath
6999fileNameArgs = strArgument
70100 (metavar "FILENAME"
71− <> help "Name of file to upload to AWS S3 or a Minio server")
101+ <> help "Name of file to upload to AWS S3 or a MinIO server")
72102
73103cmdParser = info
74104 (helper <*> fileNameArgs)
@@ -77,27 +107,30 @@ cmdParser = info
77107 <> header
78108 "FileUploader - a simple file-uploader program using minio-hs")
79109
80−ignoreMinioErr :: ServiceErr -> Minio ()
81−ignoreMinioErr = return . const ()
82−
83−
84110main :: IO ()
85111main = do
86112 let bucket = "my-bucket"
87113
88− -- Parse command line argument, namely --filename.
114+ -- Parse command line argument
89115 filepath <- execParser cmdParser
90116 let object = pack $ takeBaseName filepath
91117
92118 res <- runMinio minioPlayCI $ do
93119 -- Make a bucket; catch bucket already exists exception if thrown.
94− catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
120+ bErr <- try $ makeBucket bucket Nothing
121+
122+ -- If the bucket already exists, we would get a specific
123+ -- `ServiceErr` exception thrown.
124+ case bErr of
125+ Left BucketAlreadyOwnedByYou -> return ()
126+ Left e -> throwIO e
127+ Right _ -> return ()
95128
96− -- Upload filepath to bucket; object is derived from filepath.
97− fPutObject bucket object filepath
129+ -- Upload filepath to bucket; object name is derived from filepath.
130+ fPutObject bucket object filepath defaultPutObjectOptions
98131
99132 case res of
100− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
133+ Left e -> putStrLn $ "file upload failed due to " ++ show e
101134 Right () -> putStrLn "file upload succeeded."
102135```
103136
@@ -111,3 +144,55 @@ main = do
111144## Contribute
112145
113146[Contributors Guide](https://github.com/minio/minio-hs/blob/master/CONTRIBUTING.md)
147+
148+### Development
149+
150+#### Download the source
151+
152+```sh
153+$ git clone https://github.com/minio/minio-hs.git
154+$ cd minio-hs/
155+```
156+
157+#### Build the package:
158+
159+With `cabal`:
160+
161+```sh
162+$ # Configure cabal for development enabling all optional flags defined by the package.
163+$ cabal configure --enable-tests --test-show-details=direct -fexamples -fdev -flive-test
164+$ cabal build
165+```
166+
167+With `stack`:
168+
169+``` sh
170+$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
171+```
172+#### Running tests:
173+
174+A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000` with the credentials `access_key=minio` and `secret_key=minio123`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
175+
176+With `cabal`:
177+
178+```sh
179+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
180+$ cabal test
181+```
182+
183+With `stack`:
184+
185+``` sh
186+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
187+stack test --flag minio-hs:live-test --flag minio-hs:dev
188+```
189+
190+This will run all the test suites.
191+
192+#### Building documentation:
193+
194+```sh
195+$ cabal haddock
196+$ # OR
197+$ stack haddock
198+```
docs/API.md+352−227
@@ -1,8 +1,8 @@
1−# Minio Haskell SDK API Reference
1+# MinIO Haskell SDK API Reference
22
3−## Initialize Minio Client object.
3+## Initialize MinIO Client object.
44
5−### Minio - for public Play server
5+### MinIO - for public Play server
66
77```haskell
88minioPlayCI :: ConnectInfo
@@ -20,20 +20,20 @@ awsCI { connectAccesskey = "your-access-key"
2020
2121```
2222
23−|Bucket operations|Object Operations|Presigned Operations|
24−|:---|:---|:---|
25−|[`listBuckets`](#listBuckets) |[`getObject`](#getObject)|[`presignedGetObjectUrl`](#presignedGetObjectUrl)|
26−|[`makeBucket`](#makeBucket)|[`putObject`](#putObject)|[`presignedPutObjectUrl`](#presignedPutObjectUrl)|
27−|[`removeBucket`](#removeBucket)|[`fGetObject`](#fGetObject)|[`presignedPostPolicy`](#presignedPostPolicy)|
28−|[`listObjects`](#listObjects)|[`fPutObject`](#fPutObject)||
29−|[`listObjectsV1`](#listObjectsV1)|[`copyObject`](#copyObject)||
30−|[`listIncompleteUploads`](#listIncompleteUploads)|[`removeObject`](#removeObject)||
31−|[`bucketExists`](#bucketExists)|||
23+| Bucket operations | Object Operations | Presigned Operations |
24+|:--------------------------------------------------|:----------------------------------------------|:--------------------------------------------------|
25+| [`listBuckets`](#listBuckets) | [`getObject`](#getObject) | [`presignedGetObjectUrl`](#presignedGetObjectUrl) |
26+| [`makeBucket`](#makeBucket) | [`putObject`](#putObject) | [`presignedPutObjectUrl`](#presignedPutObjectUrl) |
27+| [`removeBucket`](#removeBucket) | [`fGetObject`](#fGetObject) | [`presignedPostPolicy`](#presignedPostPolicy) |
28+| [`listObjects`](#listObjects) | [`fPutObject`](#fPutObject) | |
29+| [`listObjectsV1`](#listObjectsV1) | [`copyObject`](#copyObject) | |
30+| [`listIncompleteUploads`](#listIncompleteUploads) | [`removeObject`](#removeObject) | |
31+| [`bucketExists`](#bucketExists) | [`selectObjectContent`](#selectObjectContent) | |
3232
3333## 1. Connecting and running operations on the storage service
3434
35−The Haskell Minio SDK provides high-level functionality to perform
36−operations on a Minio server or any AWS S3-like API compatible storage
35+The Haskell MinIO SDK provides high-level functionality to perform
36+operations on a MinIO server or any AWS S3-like API compatible storage
3737service.
3838
3939### The `ConnectInfo` type
@@ -69,42 +69,42 @@ enable/disable the automatic region discovery behaviour.
6969
7070The parameters in the expression `awsWithRegion region autoDiscover` are:
7171
72−|Parameter|Type|Description|
73−|:---|:---|:---|
74−| `region` | _Region_ (alias for `Text`) | The region to connect to by default for all requests. |
75−| `autoDiscover` | _Bool_ | If `True`, region discovery will be enabled. If `False`, discovery is disabled, and all requests go the given region only.|
72+| Parameter | Type | Description |
73+|:---------------|:----------------------------|:---------------------------------------------------------------------------------------------------------------------------|
74+| `region` | _Region_ (alias for `Text`) | The region to connect to by default for all requests. |
75+| `autoDiscover` | _Bool_ | If `True`, region discovery will be enabled. If `False`, discovery is disabled, and all requests go the given region only. |
7676
7777#### minioPlayCI :: ConnectInfo
7878
7979This constructor provides connection and authentication information to
80−connect to the public Minio Play server at
81−`https://play.minio.io:9000/`.
80+connect to the public MinIO Play server at
81+`https://play.min.io/`.
8282
8383#### minioCI :: Text -> Int -> Bool -> ConnectInfo
8484
85−Use to connect to a Minio server.
85+Use to connect to a MinIO server.
8686
8787The parameters in the expression `minioCI host port isSecure` are:
8888
89−|Parameter|Type|Description|
90−|:---|:---|:---|
91−| `host` | _Text_ | Hostname of the Minio or other S3-API compatible server |
92−| `port` | _Int_ | Port number to connect to|
93−| `isSecure` | _Bool_ | Does the server use HTTPS? |
89+| Parameter | Type | Description |
90+|:-----------|:-------|:--------------------------------------------------------|
91+| `host` | _Text_ | Hostname of the MinIO or other S3-API compatible server |
92+| `port` | _Int_ | Port number to connect to |
93+| `isSecure` | _Bool_ | Does the server use HTTPS? |
9494
9595#### The ConnectInfo fields and Default instance
9696
9797The following table shows the fields in the `ConnectInfo` record-type:
9898
99−| Field | Type | Description |
100−|:---|:---|:---|
101−| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
102−| `connectPort` | _Int_ | Port number on which the server listens. Defaults to `9000`. |
103−| `connectAccessKey` | _Text_ | Access key to use in authentication. Defaults to `minio`. |
104−| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
105−| `connectIsSecure` | _Bool_ | Specifies if the server used TLS. Defaults to `False` |
106−| `connectRegion` | _Region_ (alias for `Text`) | Specifies the region to use. Defaults to 'us-east-1' |
107−| `connectAutoDiscoverRegion` | _Bool_ | Specifies if the library should automatically discover the region of a bucket. Defaults to `True`|
99+| Field | Type | Description |
100+|:----------------------------|:----------------------------|:--------------------------------------------------------------------------------------------------|
101+| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
102+| `connectPort` | _Int_ | Port number on which the server listens. Defaults to `9000`. |
103+| `connectAccessKey` | _Text_ | Access key to use in authentication. Defaults to `minio`. |
104+| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
105+| `connectIsSecure` | _Bool_ | Specifies if the server used TLS. Defaults to `False` |
106+| `connectRegion` | _Region_ (alias for `Text`) | Specifies the region to use. Defaults to 'us-east-1' |
107+| `connectAutoDiscoverRegion` | _Bool_ | Specifies if the library should automatically discover the region of a bucket. Defaults to `True` |
108108
109109The `def` value of type `ConnectInfo` has all the above default
110110values.
@@ -112,7 +112,7 @@ values.
112112### The Minio Monad
113113
114114This monad provides the required environment to perform requests
115−against a Minio or other S3 API compatible server. It uses the
115+against a MinIO or other S3 API compatible server. It uses the
116116connection information from the `ConnectInfo` value provided to it. It
117117performs connection pooling, bucket location caching, error handling
118118and resource clean-up actions.
@@ -148,17 +148,17 @@ Lists buckets.
148148
149149__Return Value__
150150
151−|Return type |Description |
152−|:---|:---|
153−| _Minio [BucketInfo]_| List of buckets |
151+| Return type | Description |
152+|:---------------------|:----------------|
153+| _Minio [BucketInfo]_ | List of buckets |
154154
155155
156156__BucketInfo record type__
157157
158−|Field |Type |Description |
159−|:---|:---| :---|
160−| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
161−| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
158+| Field | Type | Description |
159+|:-----------------|:---------------------------|:----------------------------|
160+| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
161+| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
162162
163163
164164<a name="makeBucket"></a>
@@ -171,10 +171,10 @@ __Parameters__
171171
172172In the expression `makeBucket bucketName region` the arguments are:
173173
174−| Param | Type | Description |
175−|---|---|---|
176−|`bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
177−| `region` | _Maybe Region_ | Region where the bucket is to be created. If not specified, default to the region in `ConnectInfo`.|
174+| Param | Type | Description |
175+|--------------|-----------------------------|-----------------------------------------------------------------------------------------------------|
176+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
177+| `region` | _Maybe Region_ | Region where the bucket is to be created. If not specified, default to the region in `ConnectInfo`. |
178178
179179__Example__
180180
@@ -185,7 +185,6 @@ main :: IO ()
185185main = do
186186 res <- runMinio minioPlayCI $ do
187187 makeBucket bucketName (Just "us-east-1")
188−
189188 case res of
190189 Left err -> putStrLn $ "Failed to make bucket: " ++ (show res)
191190 Right _ -> putStrLn $ "makeBucket successful."
@@ -201,9 +200,9 @@ __Parameters__
201200
202201In the expression `removeBucket bucketName` the arguments are:
203202
204−| Param | Type | Description |
205−|---|---|---|
206−|`bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
203+| Param | Type | Description |
204+|--------------|-----------------------------|--------------------|
205+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
207206
208207
209208__Example__
@@ -225,7 +224,7 @@ main = do
225224
226225
227226<a name="listObjects"></a>
228−### listObjects :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
227+### listObjects :: Bucket -> Maybe Text -> Bool -> C.ConduitM () ObjectInfo Minio ()
229228
230229List objects in the given bucket, implements version 2 of AWS S3 API.
231230
@@ -234,34 +233,44 @@ __Parameters__
234233In the expression `listObjects bucketName prefix recursive` the
235234arguments are:
236235
237−|Param |Type |Description |
238−|:---|:---| :---|
239−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
240−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
241−| `recursive` | _Bool_ |`True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
236+| Param | Type | Description |
237+|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
238+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
239+| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
240+| `recursive` | _Bool_ | `True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
242241
243242__Return Value__
244243
245−|Return type |Description |
246−|:---|:---|
247−| _C.Producer Minio ObjectInfo_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
244+| Return type | Description |
245+|:------------------------------------|:------------------------------------------------------------------------|
246+| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
248247
249248__ObjectInfo record type__
250249
251−|Field |Type |Description |
252−|:---|:---| :---|
253−|`oiObject` | _Object_ (alias for `Text`) | Name of object |
254−|`oiModTime` | _UTCTime_ | Last modified time of the object |
255−|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
256−|`oiSize` | _Int64_ | Size of the object in bytes |
250+| Field | Type | Description |
251+|:-------------|:----------------------------|:-------------------------------------|
252+| `oiObject` | _Object_ (alias for `Text`) | Name of object |
253+| `oiModTime` | _UTCTime_ | Last modified time of the object |
254+| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
255+| `oiSize` | _Int64_ | Size of the object in bytes |
256+| `oiMetadata` | _HashMap Text Text_ | Map of key-value user-metadata pairs |
257257
258258__Example__
259259
260260``` haskell
261−{-# Language OverloadedStrings #-}
261+{-# LANGUAGE OverloadedStrings #-}
262+import Network.Minio
262263
263−import Data.Conduit (($$))
264−import Conduit.Combinators (sinkList)
264+import Conduit
265+import Prelude
266+
267+
268+-- | The following example uses MinIO play server at
269+-- https://play.min.io. The endpoint and associated
270+-- credentials are provided via the libary constant,
271+--
272+-- > minioPlayCI :: ConnectInfo
273+--
265274
266275main :: IO ()
267276main = do
@@ -269,15 +278,14 @@ main = do
269278 bucket = "test"
270279
271280 -- Performs a recursive listing of all objects under bucket "test"
272− -- on play.minio.io.
273− res <- runMinio minioPlayCI $ do
274− listObjects bucket Nothing True $$ sinkList
281+ -- on play.min.io.
282+ res <- runMinio minioPlayCI $
283+ runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
275284 print res
276−
277285```
278286
279287<a name="listObjectsV1"></a>
280−### listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
288+### listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.ConduitM () ObjectInfo Minio ()
281289
282290List objects in the given bucket, implements version 1 of AWS S3 API. This API
283291is provided for legacy S3 compatible object storage endpoints.
@@ -287,34 +295,43 @@ __Parameters__
287295In the expression `listObjectsV1 bucketName prefix recursive` the
288296arguments are:
289297
290−|Param |Type |Description |
291−|:---|:---| :---|
292−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
293−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
294−| `recursive` | _Bool_ |`True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
298+| Param | Type | Description |
299+|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
300+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
301+| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
302+| `recursive` | _Bool_ | `True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
295303
296304__Return Value__
297305
298−|Return type |Description |
299−|:---|:---|
300−| _C.Producer Minio ObjectInfo_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
306+| Return type | Description |
307+|:------------------------------------|:------------------------------------------------------------------------|
308+| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
301309
302310__ObjectInfo record type__
303311
304−|Field |Type |Description |
305−|:---|:---| :---|
306−|`oiObject` | _Object_ (alias for `Text`) | Name of object |
307−|`oiModTime` | _UTCTime_ | Last modified time of the object |
308−|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
309−|`oiSize` | _Int64_ | Size of the object in bytes |
312+| Field | Type | Description |
313+|:------------|:----------------------------|:---------------------------------|
314+| `oiObject` | _Object_ (alias for `Text`) | Name of object |
315+| `oiModTime` | _UTCTime_ | Last modified time of the object |
316+| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
317+| `oiSize` | _Int64_ | Size of the object in bytes |
310318
311319__Example__
312320
313321``` haskell
314−{-# Language OverloadedStrings #-}
322+{-# LANGUAGE OverloadedStrings #-}
323+import Network.Minio
324+
325+import Conduit
326+import Prelude
315327
316−import Data.Conduit (($$))
317−import Conduit.Combinators (sinkList)
328+
329+-- | The following example uses MinIO play server at
330+-- https://play.min.io. The endpoint and associated
331+-- credentials are provided via the libary constant,
332+--
333+-- > minioPlayCI :: ConnectInfo
334+--
318335
319336main :: IO ()
320337main = do
@@ -322,11 +339,10 @@ main = do
322339 bucket = "test"
323340
324341 -- Performs a recursive listing of all objects under bucket "test"
325− -- on play.minio.io.
326− res <- runMinio minioPlayCI $ do
327− listObjectsV1 bucket Nothing True $$ sinkList
342+ -- on play.min.io.
343+ res <- runMinio minioPlayCI $
344+ runConduit $ listObjectsV1 bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
328345 print res
329−
330346```
331347
332348<a name="listIncompleteUploads"></a>
@@ -339,43 +355,51 @@ __Parameters__
339355In the expression `listIncompleteUploads bucketName prefix recursive`
340356the parameters are:
341357
342−|Param |Type |Description |
343−|:---|:---| :---|
344−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
345−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have. |
346−| `recursive` | _Bool_ |`True` indicates recursive style listing and `Talse` indicates directory style listing delimited by '/'. |
358+| Param | Type | Description |
359+|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
360+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
361+| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have. |
362+| `recursive` | _Bool_ | `True` indicates recursive style listing and `Talse` indicates directory style listing delimited by '/'. |
347363
348364__Return Value__
349365
350366|Return type |Description |
351367|:---|:---|
352−| _C.Producer Minio UploadInfo_ | A Conduit Producer of `UploadInfo` values corresponding to each incomplete multipart upload |
368+| _C.ConduitM () UploadInfo Minio ()_ | A Conduit Producer of `UploadInfo` values corresponding to each incomplete multipart upload |
353369
354370__UploadInfo record type__
355371
356−|Field |Type |Description |
357−|:---|:---| :---|
358−|`uiKey` | _Object_ |Name of incompletely uploaded object |
359−|`uiUploadId` | _String_ |Upload ID of incompletely uploaded object |
360−|`uiSize` | _Int64_ |Size of incompletely uploaded object |
372+| Field | Type | Description |
373+|:-------------|:---------|:------------------------------------------|
374+| `uiKey` | _Object_ | Name of incompletely uploaded object |
375+| `uiUploadId` | _String_ | Upload ID of incompletely uploaded object |
376+| `uiSize` | _Int64_ | Size of incompletely uploaded object |
361377
362378__Example__
363379
364380```haskell
365−{-# Language OverloadedStrings #-}
381+{-# LANGUAGE OverloadedStrings #-}
382+import Network.Minio
383+
384+import Conduit
385+import Prelude
366386
367−import Data.Conduit (($$))
368−import Conduit.Combinators (sinkList)
387+-- | The following example uses MinIO play server at
388+-- https://play.min.io. The endpoint and associated
389+-- credentials are provided via the libary constant,
390+--
391+-- > minioPlayCI :: ConnectInfo
392+--
369393
370394main :: IO ()
371395main = do
372396 let
373397 bucket = "test"
374398
375− -- Performs a recursive listing of all incompletely uploaded objects
376− -- under bucket "test" on play.minio.io.
377− res <- runMinio minioPlayCI $ do
378− listIncompleteUploads bucket Nothing True $$ sinkList
399+ -- Performs a recursive listing of incomplete uploads under bucket "test"
400+ -- on a local MinIO server.
401+ res <- runMinio minioPlayCI $
402+ runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
379403 print res
380404
381405```
@@ -383,97 +407,126 @@ main = do
383407## 3. Object operations
384408
385409<a name="getObject"></a>
386−### getObject :: Bucket -> Object -> Minio (C.ResumableSource Minio ByteString)
410+### getObject :: Bucket -> Object -> GetObjectOptions -> Minio (C.ConduitM () ByteString Minio ())
387411
388−Get an object from the service.
412+Get an object from the S3 service, optionally object ranges can be provided as well.
389413
390414__Parameters__
391415
392−In the expression `getObject bucketName objectName` the parameters
416+In the expression `getObject bucketName objectName opts` the parameters
393417are:
394418
395−|Param |Type |Description |
396−|:---|:---| :---|
397−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
398−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
419+| Param | Type | Description |
420+|:-------------|:----------------------------|:----------------------------------------------------------------------------|
421+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
422+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
423+| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
424+
425+__GetObjectOptions record type__
426+
427+| Field | Type | Description |
428+|:-----------------------|:--------------------------------|:------------------------------------------------------------------------------------------------------|
429+| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object |
430+| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
431+| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
432+| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
433+| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
399434
400435__Return Value__
401436
402437The return value can be incrementally read to process the contents of
403438the object.
404−|Return type |Description |
405−|:---|:---|
406−| _C.ResumableSource Minio ByteString_ | A Conduit ResumableSource of `ByteString` values. |
439+| Return type | Description |
440+|:--------------------------------------------|:-----------------------------------------|
441+| _Minio (C.ConduitM () ByteString Minio ())_ | A Conduit source of `ByteString` values. |
407442
408443__Example__
409444
410445```haskell
411−{-# Language OverloadedStrings #-}
446+{-# LANGUAGE OverloadedStrings #-}
447+import Network.Minio
412448
413−import Network.Minio
414−import Data.Conduit (($$+-))
415−import Data.Conduit.Binary (sinkLbs)
416−import qualified Data.ByteString.Lazy as LB
449+import qualified Data.Conduit as C
450+import qualified Data.Conduit.Binary as CB
451+
452+import Prelude
453+
454+-- | The following example uses MinIO play server at
455+-- https://play.min.io. The endpoint and associated
456+-- credentials are provided via the libary constant,
457+--
458+-- > minioPlayCI :: ConnectInfo
459+--
417460
418461main :: IO ()
419462main = do
420463 let
421− bucket = "mybucket"
422− object = "myobject"
423−
424− -- Lists the parts in an incompletely uploaded object identified by
425− -- bucket, object and upload ID.
464+ bucket = "my-bucket"
465+ object = "my-object"
426466 res <- runMinio minioPlayCI $ do
427− source <- getObject bucket object
428− source $$+- sinkLbs
467+ src <- getObject bucket object def
468+ C.connect src $ CB.sinkFileCautious "/tmp/my-object"
429469
430− -- the following the prints the contents of the object.
431− putStrLn $ either
432− (("Failed to getObject: " ++) . show)
433− (("Read an object of length: " ++) . show . LB.length)
434− res
470+ case res of
471+ Left e -> putStrLn $ "getObject failed." ++ (show e)
472+ Right _ -> putStrLn "getObject succeeded."
435473```
436474
437475<a name="putObject"></a>
438−### putObject :: Bucket -> Object -> C.Producer Minio ByteString -> Maybe Int64 -> Minio ()
476+### putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio () -> Maybe Int64 -> PutObjectOptions -> Minio ()
439477Uploads an object to a bucket in the service, from the given input
440−byte stream of optionally supplied length
478+byte stream of optionally supplied length. Optionally you can also specify
479+additional metadata for the object.
441480
442481__Parameters__
443482
444483In the expression `putObject bucketName objectName inputSrc` the parameters
445484are:
446485
447−|Param |Type |Description |
448−|:---|:---| :---|
449−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
450−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
451−| `inputSrc` | _C.Producer Minio ByteString_ | A Conduit Producer of `ByteString` values |
486+| Param | Type | Description |
487+|:-------------|:------------------------------------|:------------------------------------------------------------------|
488+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
489+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
490+| `inputSrc` | _C.ConduitM () ByteString Minio ()_ | A Conduit producer of `ByteString` values |
491+| `size` | _Int64_ | Provide stream size (optional) |
492+| `opts` | _PutObjectOptions_ | Optional parameters to provide additional metadata for the object |
452493
453494__Example__
454495
455496```haskell
456−{-# Language OverloadedStrings #-}
457−import Network.Minio
497+{-# LANGUAGE OverloadedStrings #-}
498+import Network.Minio
499+
458500import qualified Data.Conduit.Combinators as CC
459501
502+import Prelude
503+
504+-- | The following example uses MinIO play server at
505+-- https://play.min.io. The endpoint and associated
506+-- credentials are provided via the libary constant,
507+--
508+-- > minioPlayCI :: ConnectInfo
509+--
510+
460511main :: IO ()
461512main = do
462513 let
463− bucket = "mybucket"
464− object = "myobject"
465− kb15 = 15 * 1024
466−
467− res <- runMinio minioPlayCI $ do
468− putObject bucket object (CC.repeat "a") (Just kb15)
514+ bucket = "test"
515+ object = "obj"
516+ localFile = "/etc/lsb-release"
517+ kb15 = 15 * 1024
469518
519+ -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
520+ res <- runMinio minioPlayCI $
521+ putObject bucket object (CC.repeat "a") (Just kb15) def
470522 case res of
471− Left e -> putStrLn $ "Failed to putObject " ++ show bucket ++ "/" ++ show object
472− Right _ -> putStrLn "PutObject was successful"
523+ Left e -> putStrLn $ "putObject failed." ++ show e
524+ Right () -> putStrLn "putObject succeeded."
525+
473526```
474527
475528<a name="fGetObject"></a>
476−### fGetObject :: Bucket -> Object -> FilePath -> Minio ()
529+### fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
477530Downloads an object from a bucket in the service, to the given file
478531
479532__Parameters__
@@ -481,11 +534,23 @@ __Parameters__
481534In the expression `fGetObject bucketName objectName inputFile` the parameters
482535are:
483536
484−|Param |Type |Description |
485−|:---|:---| :---|
486−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
487−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
488−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
537+| Param | Type | Description |
538+|:-------------|:----------------------------|:----------------------------------------------------------------------------|
539+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
540+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
541+| `inputFile` | _FilePath_ | Path to the file to be uploaded |
542+| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
543+
544+
545+__GetObjectOptions record type__
546+
547+| Field | Type | Description |
548+|:-----------------------|:--------------------------------|:------------------------------------------------------------------------------------------------------|
549+| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object |
550+| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
551+| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
552+| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
553+| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
489554
490555``` haskell
491556
@@ -496,8 +561,8 @@ import Data.Conduit (($$+-))
496561import Data.Conduit.Binary (sinkLbs)
497562import Prelude
498563
499−-- | The following example uses minio's play server at
500−-- https://play.minio.io:9000. The endpoint and associated
564+-- | The following example uses MinIO play server at
565+-- https://play.min.io. The endpoint and associated
501566-- credentials are provided via the libary constant,
502567--
503568-- > minioPlayCI :: ConnectInfo
@@ -511,7 +576,7 @@ main = do
511576 localFile = "/etc/lsb-release"
512577
513578 res <- runMinio minioPlayCI $ do
514− src <- fGetObject bucket object localFile
579+ src <- fGetObject bucket object localFile def
515580 (src $$+- sinkLbs)
516581
517582 case res of
@@ -528,11 +593,11 @@ __Parameters__
528593In the expression `fPutObject bucketName objectName inputFile` the parameters
529594are:
530595
531−|Param |Type |Description |
532−|:---|:---| :---|
533−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
534−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
535−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
596+| Param | Type | Description |
597+|:-------------|:----------------------------|:--------------------------------|
598+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
599+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
600+| `inputFile` | _FilePath_ | Path to the file to be uploaded |
536601
537602__Example__
538603
@@ -557,31 +622,38 @@ main = do
557622```
558623
559624<a name="copyObject"></a>
560−### copyObject :: Bucket -> Object -> CopyPartSource -> Minio ()
625+### copyObject :: DestinationInfo -> SourceInfo -> Minio ()
561626Copies content of an object from the service to another
562627
563628__Parameters__
564629
565−In the expression `copyObject bucketName objectName cps` the parameters
630+In the expression `copyObject dstInfo srcInfo` the parameters
566631are:
567632
568−|Param |Type |Description |
569−|:---|:---| :---|
570−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
571−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
572−| `cps` | _CopyPartSource_ | A value representing properties of the source object |
633+| Param | Type | Description |
634+|:----------|:------------------|:----------------------------------------------------------|
635+| `dstInfo` | _DestinationInfo_ | A value representing properties of the destination object |
636+| `srcInfo` | _SourceInfo_ | A value representing properties of the source object |
637+
573638
639+__SourceInfo record type__
574640
575−__CopyPartSource record type__
641+| Field | Type | Description |
642+|:-----------------------|:-----------------------|:----------------------------------------------------------------------------------------------------------|
643+| `srcBucket` | `Bucket` | Name of source bucket |
644+| `srcObject` | `Object` | Name of source object |
645+| `srcRange` | `Maybe (Int64, Int64)` | (Optional) Represents the byte range of source object. (0, 9) represents first ten bytes of source object |
646+| `srcIfMatch` | `Maybe Text` | (Optional) ETag source object should match |
647+| `srcIfNoneMatch` | `Maybe Text` | (Optional) ETag source object shouldn't match |
648+| `srcIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since source object wasn't modified |
649+| `srcIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since source object was modified |
576650
577−|Field |Type |Description |
578−|:---|:---| :---|
579−| `cpSource` | `Text`| Name of source object formatted as "/srcBucket/srcObject" |
580−| `cpSourceRange` | `Maybe (Int64, Int64)` | Represents the byte range of source object. (0, 9) represents first ten bytes of source object|
581−| `cpSourceIfMatch` | `Maybe Text` | (Optional) ETag source object should match |
582−| `cpSourceIfNoneMatch` | `Maybe Text` | (Optional) ETag source object shouldn't match |
583−| `cpSourceIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since source object wasn't modified |
584−| `cpSourceIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since source object was modified |
651+__Destination record type__
652+
653+| Field | Type | Description |
654+|:------------|:---------|:-----------------------------------------------------|
655+| `dstBucket` | `Bucket` | Name of destination bucket in server-side copyObject |
656+| `dstObject` | `Object` | Name of destination object in server-side copyObject |
585657
586658__Example__
587659
@@ -594,13 +666,13 @@ main = do
594666 let
595667 bucket = "mybucket"
596668 object = "myobject"
597− srcObject = "/mybucket/srcObject"
669+ objectCopy = "obj-copy"
598670
599671 res <- runMinio minioPlayCI $ do
600− copyObject bucket object def { cpSource = srcObject }
672+ copyObject def { dstBucket = bucket, dstObject = objectCopy } def { srcBucket = bucket, srcObject = object }
601673
602674 case res of
603− Left e -> putStrLn $ "Failed to copyObject " ++ show srcObject"
675+ Left e -> putStrLn $ "Failed to copyObject " ++ show bucket ++ show "/" ++ show object
604676 Right _ -> putStrLn "copyObject was successful"
605677```
606678
@@ -613,10 +685,10 @@ __Parameters__
613685In the expression `removeObject bucketName objectName` the parameters
614686are:
615687
616−|Param |Type |Description |
617−|:---|:---| :---|
618−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
619−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
688+| Param | Type | Description |
689+|:-------------|:----------------------------|:-------------------|
690+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
691+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
620692
621693__Example__
622694
@@ -647,10 +719,10 @@ __Parameters__
647719In the expression `removeIncompleteUpload bucketName objectName` the parameters
648720are:
649721
650−|Param |Type |Description |
651−|:---|:---| :---|
652−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
653−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
722+| Param | Type | Description |
723+|:-------------|:----------------------------|:-------------------|
724+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
725+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
654726
655727__Example__
656728
@@ -672,6 +744,59 @@ main = do
672744 Right _ -> putStrLn "Removed incomplete upload successfully"
673745```
674746
747+<a name="selectObjectContent"></a>
748+### selectObjectContent :: Bucket -> Object -> SelectRequest -> Minio (ConduitT () EventMessage Minio ())
749+Removes an ongoing multipart upload of an object from the service
750+
751+__Parameters__
752+
753+In the expression `selectObjectContent bucketName objectName selReq`
754+the parameters are:
755+
756+| Param | Type | Description |
757+|:-------------|:----------------------------|:--------------------------|
758+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
759+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
760+| `selReq` | _SelectRequest_ | Select request parameters |
761+
762+__SelectRequest record__
763+
764+This record is created using `selectRequest`. Please refer to the Haddocks for further information.
765+
766+__Return Value__
767+
768+The return value can be used to read individual `EventMessage`s in the response. Please refer to the Haddocks for further information.
769+
770+|Return type | Description |
771+|:---|:---|
772+| _Minio (C.conduitT () EventMessage Minio ())_ | A Conduit source of `EventMessage` values. |
773+
774+__Example__
775+
776+```haskell
777+{-# Language OverloadedStrings #-}
778+import Network.Minio
779+
780+import qualified Conduit as C
781+
782+main :: IO ()
783+main = do
784+ let
785+ bucket = "mybucket"
786+ object = "myobject"
787+
788+ res <- runMinio minioPlayCI $ do
789+ let sr = selectRequest "Select * from s3object"
790+ defaultCsvInput defaultCsvOutput
791+ res <- selectObjectContent bucket object sr
792+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
793+
794+ case res of
795+ Left _ -> putStrLn "Failed!"
796+ Right _ -> putStrLn "Success!"
797+```
798+
799+
675800<a name="BucketExists"></a>
676801### bucketExists :: Bucket -> Minio Bool
677802Checks if a bucket exists.
@@ -680,9 +805,9 @@ __Parameters__
680805
681806In the expression `bucketExists bucketName` the parameters are:
682807
683−|Param |Type |Description |
684−|:---|:---| :---|
685−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
808+| Param | Type | Description |
809+|:-------------|:----------------------------|:-------------------|
810+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
686811
687812
688813## 4. Presigned operations
@@ -704,22 +829,22 @@ __Parameters__
704829In the expression `presignedGetObjectUrl bucketName objectName expiry queryParams headers`
705830the parameters are:
706831
707−|Param |Type |Description |
708−|:---|:---| :---|
709−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
710−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
711−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
712−| `queryParams` | _Query_ (from package `http-types:Network.HTTP.Types`) | Query parameters to add to the URL |
713−| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
832+| Param | Type | Description |
833+|:--------------|:---------------------------------------------------------------|:------------------------------------------------|
834+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
835+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
836+| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
837+| `queryParams` | _Query_ (from package `http-types:Network.HTTP.Types`) | Query parameters to add to the URL |
838+| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
714839
715840__Return Value__
716841
717842Returns the generated URL - it will include authentication
718843information.
719844
720−|Return type |Description |
721−|:---|:---|
722−| _ByteString_ | Generated presigned URL |
845+| Return type | Description |
846+|:-------------|:------------------------|
847+| _ByteString_ | Generated presigned URL |
723848
724849__Example__
725850
@@ -762,21 +887,21 @@ __Parameters__
762887In the expression `presignedPutObjectUrl bucketName objectName expiry headers`
763888the parameters are:
764889
765−|Param |Type |Description |
766−|:---|:---| :---|
767−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
768−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
769−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
770−| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
890+| Param | Type | Description |
891+|:-------------|:---------------------------------------------------------------|:------------------------------------------------|
892+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
893+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
894+| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
895+| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
771896
772897__Return Value__
773898
774899Returns the generated URL - it will include authentication
775900information.
776901
777−|Return type |Description |
778−|:---|:---|
779−| _ByteString_ | Generated presigned URL |
902+| Return type | Description |
903+|:-------------|:------------------------|
904+| _ByteString_ | Generated presigned URL |
780905
781906__Example__
782907
@@ -804,7 +929,7 @@ main = do
804929```
805930
806931<a name="presignedPostPolicy"></a>
807−### presignedPostPolicy :: PostPolicy -> Minio (ByteString, Map.Map Text ByteString)
932+### presignedPostPolicy :: PostPolicy -> Minio (ByteString, HashMap Text ByteString)
808933
809934Generate a presigned URL and POST policy to upload files via a POST
810935request. This is intended for browser uploads and generates form data
@@ -816,10 +941,10 @@ The `PostPolicy` argument is created using the `newPostPolicy` function:
816941
817942In the expression `newPostPolicy expirationTime conditions` the parameters are:
818943
819−|Param | Type| Description |
820−|:---|:---|:---|
821−| `expirationTime` | _UTCTime_ (from package `time:Data.Time.UTCTime`) | The expiration time for the policy |
822−| `conditions` | _[PostPolicyConditions]_ | List of conditions to be added to the policy |
944+| Param | Type | Description |
945+|:-----------------|:--------------------------------------------------|:---------------------------------------------|
946+| `expirationTime` | _UTCTime_ (from package `time:Data.Time.UTCTime`) | The expiration time for the policy |
947+| `conditions` | _[PostPolicyConditions]_ | List of conditions to be added to the policy |
823948
824949The policy conditions are created using various helper functions -
825950please refer to the Haddocks for details.
@@ -841,7 +966,7 @@ import Network.Minio
841966
842967import qualified Data.ByteString as B
843968import qualified Data.ByteString.Char8 as Char8
844−import qualified Data.Map.Strict as Map
969+import qualified Data.HashMap.Strict as H
845970import qualified Data.Text.Encoding as Enc
846971import qualified Data.Time as Time
847972
@@ -881,7 +1006,7 @@ main = do
8811006 let
8821007 formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
8831008 "'", v, "'"]
884− formOptions = B.intercalate " " $ map formFn $ Map.toList formData
1009+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
8851010
8861011
8871012 return $ B.intercalate " " $
examples/AssumeRole.hsnew file+47
@@ -0,0 +1,47 @@
1+--
2+-- MinIO Haskell SDK, (C) 2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE OverloadedStrings #-}
17+
18+import Control.Monad.IO.Class (liftIO)
19+import Network.Minio
20+import Prelude
21+
22+main :: IO ()
23+main = do
24+ -- Use play credentials for example.
25+ let assumeRole =
26+ STSAssumeRole
27+ ( CredentialValue
28+ "Q3AM3UQ867SPQQA43P2F"
29+ "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
30+ Nothing
31+ )
32+ $ defaultSTSAssumeRoleOptions
33+ { saroLocation = Just "us-east-1",
34+ saroEndpoint = Just "https://play.min.io:9000"
35+ }
36+
37+ -- Retrieve temporary credentials and print them.
38+ cv <- requestSTSCredential assumeRole
39+ print $ "Temporary credentials" ++ show (credentialValueText $ fst cv)
40+ print $ "Expiry" ++ show (snd cv)
41+
42+ -- Configure 'ConnectInfo' to request temporary credentials on demand.
43+ ci <- setSTSCredential assumeRole "https://play.min.io"
44+ res <- runMinio ci $ do
45+ buckets <- listBuckets
46+ liftIO $ print $ "Top 5 buckets: " ++ show (take 5 buckets)
47+ print res
examples/BucketExists.hs+5−8
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,20 +16,17 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
20−{-# Language OverloadedStrings #-}
21−import Network.Minio
19+{-# LANGUAGE OverloadedStrings #-}
2220
2321import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
2423import Prelude
2524
2625-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
3532 let bucket = "missingbucket"
examples/CopyObject.hs+17−21
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,44 +16,40 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.Catch (catchIf)
24−import qualified Data.Text as T
25−import Prelude
21+import Network.Minio
22+import UnliftIO.Exception (catch, throwIO)
2623
2724-- | The following example uses minio's play server at
28−-- https://play.minio.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
2926-- credentials are provided via the libary constant,
3027--
3128-- > minioPlayCI :: ConnectInfo
32−--
33−
34−ignoreMinioErr :: ServiceErr -> Minio ()
35−ignoreMinioErr = return . const ()
36−
3729main :: IO ()
3830main = do
39− let
40− bucket = "test"
31+ let bucket = "test"
4132 object = "obj"
4233 objectCopy = "obj-copy"
4334 localFile = "/etc/lsb-release"
4435
4536 res1 <- runMinio minioPlayCI $ do
4637 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
47− catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
38+ catch
39+ (makeBucket bucket Nothing)
40+ ( \e -> case e of
41+ BucketAlreadyOwnedByYou -> return ()
42+ _ -> throwIO e
43+ )
4844
4945 -- 2. Upload a file to bucket/object.
50− fPutObject bucket object localFile
46+ fPutObject bucket object localFile defaultPutObjectOptions
5147
5248 -- 3. Copy bucket/object to bucket/objectCopy.
53− copyObject bucket objectCopy def {
54− cpSource = T.concat ["/", bucket, "/", object]
55− }
49+ copyObject
50+ defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
51+ defaultSourceInfo {srcBucket = bucket, srcObject = object}
5652
5753 case res1 of
58− Left e -> putStrLn $ "copyObject failed." ++ (show e)
54+ Left e -> putStrLn $ "copyObject failed." ++ show e
5955 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+31−31
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs --package optparse-applicative --package filepath
2+-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,43 +16,39 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
20−
21−{-# LANGUAGE OverloadedStrings #-}
19+{-# LANGUAGE OverloadedStrings #-}
2220{-# LANGUAGE ScopedTypeVariables #-}
23−import Network.Minio
2421
25−import Control.Monad.Catch (catchIf)
26−import Control.Monad.IO.Class (liftIO)
27−import Data.Monoid ((<>))
28−import Data.Text (pack)
29−import Options.Applicative
30−import Prelude
31−import System.FilePath.Posix
22+import Data.Text (pack)
23+import Network.Minio
24+import Options.Applicative
25+import System.FilePath.Posix
26+import UnliftIO (throwIO, try)
27+import Prelude
3228
3329-- | The following example uses minio's play server at
34−-- https://play.minio.io:9000. The endpoint and associated
30+-- https://play.min.io. The endpoint and associated
3531-- credentials are provided via the libary constant,
3632--
3733-- > minioPlayCI :: ConnectInfo
38−--
3934
4035-- optparse-applicative package based command-line parsing.
4136fileNameArgs :: Parser FilePath
42−fileNameArgs = strArgument
43− (metavar "FILENAME"
44− <> help "Name of file to upload to AWS S3 or a Minio server")
45−
46−cmdParser = info
47− (helper <*> fileNameArgs)
48− (fullDesc
49− <> progDesc "FileUploader"
50− <> header
51− "FileUploader - a simple file-uploader program using minio-hs")
52−
53−ignoreMinioErr :: ServiceErr -> Minio ()
54−ignoreMinioErr = return . const ()
37+fileNameArgs =
38+ strArgument
39+ ( metavar "FILENAME"
40+ <> help "Name of file to upload to AWS S3 or a MinIO server"
41+ )
5542
43+cmdParser :: ParserInfo FilePath
44+cmdParser =
45+ info
46+ (helper <*> fileNameArgs)
47+ ( fullDesc
48+ <> progDesc "FileUploader"
49+ <> header
50+ "FileUploader - a simple file-uploader program using minio-hs"
51+ )
5652
5753main :: IO ()
5854main = do
@@ -64,11 +60,15 @@ main = do
6460
6561 res <- runMinio minioPlayCI $ do
6662 -- Make a bucket; catch bucket already exists exception if thrown.
67− catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
63+ bErr <- try $ makeBucket bucket Nothing
64+ case bErr of
65+ Left BucketAlreadyOwnedByYou -> return ()
66+ Left e -> throwIO e
67+ Right _ -> return ()
6868
6969 -- Upload filepath to bucket; object is derived from filepath.
70− fPutObject bucket object filepath
70+ fPutObject bucket object filepath defaultPutObjectOptions
7171
7272 case res of
73− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
73+ Left e -> putStrLn $ "file upload failed due to " ++ show e
7474 Right () -> putStrLn "file upload succeeded."
Setup.hs → examples/GetConfig.hsrenamed+15−3
@@ -1,5 +1,8 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
14--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
36--
47-- Licensed under the Apache License, Version 2.0 (the "License");
58-- you may not use this file except in compliance with the License.
@@ -14,5 +17,14 @@
1417-- limitations under the License.
1518--
1619
17−import Distribution.Simple
18−main = defaultMain
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ getConfig
30+ print res
examples/GetObject.hs+11−15
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,30 +16,26 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Data.Conduit (($$+-))
24−import Data.Conduit.Binary (sinkLbs)
25−import Prelude
21+import qualified Data.Conduit as C
22+import qualified Data.Conduit.Binary as CB
23+import Network.Minio
24+import Prelude
2625
2726-- | The following example uses minio's play server at
28−-- https://play.minio.io:9000. The endpoint and associated
27+-- https://play.min.io. The endpoint and associated
2928-- credentials are provided via the libary constant,
3029--
3130-- > minioPlayCI :: ConnectInfo
32−--
33−
3431main :: IO ()
3532main = do
36− let
37− bucket = "my-bucket"
33+ let bucket = "my-bucket"
3834 object = "my-object"
3935 res <- runMinio minioPlayCI $ do
40− src <- getObject bucket object
41− (src $$+- sinkLbs)
36+ src <- getObject bucket object defaultGetObjectOptions
37+ C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
4238
4339 case res of
44− Left e -> putStrLn $ "getObject failed." ++ (show e)
40+ Left e -> putStrLn $ "getObject failed." ++ show e
4541 Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs+11−14
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,28 +16,25 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.S3API
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.S3API
23+import Prelude
2524
2625-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
3733 object = "passwd"
38− res <- runMinio minioPlayCI $
39− headObject bucket object
34+ res <-
35+ runMinio minioPlayCI $
36+ headObject bucket object []
4037
4138 case res of
42− Left e -> putStrLn $ "headObject failed." ++ show e
39+ Left e -> putStrLn $ "headObject failed." ++ show e
4340 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
examples/Heal.hsnew file+37
@@ -0,0 +1,37 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <- runMinio minioPlayCI $
27+ do
28+ hsr <-
29+ startHeal
30+ Nothing
31+ Nothing
32+ HealOpts
33+ { hoRecursive = True,
34+ hoDryRun = False
35+ }
36+ getHealStatus Nothing Nothing (hsrClientToken hsr)
37+ print res
examples/ListBuckets.hs+6−8
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,19 +16,17 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.IO.Class (liftIO)
24−import Prelude
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
23+import Prelude
2524
2625-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
3230
3331-- This example list buckets that belongs to the user and returns
3432-- region of the first bucket returned.
examples/ListIncompleteUploads.hs+20−23
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,39 +16,36 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Data.Conduit (($$))
24−import Data.Conduit.Combinators (sinkList)
25−import Prelude
21+import Conduit
22+import Network.Minio
23+import Prelude
2624
2725-- | The following example uses minio's play server at
28−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2927-- credentials are provided via the libary constant,
3028--
3129-- > minioPlayCI :: ConnectInfo
32−--
33−
3430main :: IO ()
3531main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
3833
3934 -- Performs a recursive listing of incomplete uploads under bucket "test"
4035 -- on a local minio server.
41− res <- runMinio minioPlayCI $
42− listIncompleteUploads bucket Nothing True $$ sinkList
36+ res <-
37+ runMinio minioPlayCI $
38+ runConduit $
39+ listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
4340 print res
4441
45− {-
46− Following is the output of the above program on a local Minio server.
42+{-
43+ Following is the output of the above program on a local MinIO server.
4744
48− Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
49− , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
50− , uiInitTime = 2017-03-01 10:16:25.698 UTC
51− , uiSize = 17731794
52− }
53− ]
54− -}
45+ Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
46+ , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
47+ , uiInitTime = 2017-03-01 10:16:25.698 UTC
48+ , uiSize = 17731794
49+ }
50+ ]
51+-}
examples/ListObjects.hs+16−20
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,35 +16,31 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−
23−import qualified Data.Conduit as C
24−import qualified Data.Conduit.Combinators as CC
25−import Prelude
2620
21+import Conduit
22+import Network.Minio
23+import Prelude
2724
2825-- | The following example uses minio's play server at
29−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
3027-- credentials are provided via the libary constant,
3128--
3229-- > minioPlayCI :: ConnectInfo
33−--
34−
3530main :: IO ()
3631main = do
37− let
38− bucket = "test"
32+ let bucket = "test"
3933
4034 -- Performs a recursive listing of all objects under bucket "test"
41− -- on play.minio.io.
42− res <- runMinio minioPlayCI $
43− listObjects bucket Nothing True C.$$ CC.sinkList
35+ -- on play.min.io.
36+ res <-
37+ runMinio minioPlayCI $
38+ runConduit $
39+ listObjects bucket Nothing True .| mapM_C (liftIO . print)
4440 print res
4541
46− {-
47− Following is the output of the above program on a local Minio server.
42+{-
43+ Following is the output of the above program on a local MinIO server.
4844
49− Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]
50− -}
45+ Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]
46+-}
examples/Makebucket.hs → examples/MakeBucket.hsrenamed+10−14
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,25 +16,21 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
20−
2119{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
2320
24−import Prelude
21+import Network.Minio
22+import Prelude
2523
2624-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
2826-- credentials are provided via the libary constant,
2927--
3028-- > minioPlayCI :: ConnectInfo
31−--
32−
3329main :: IO ()
3430main = do
35− let
36− bucket = "my-bucket"
37− res <- runMinio minioPlayCI $
38− -- N B the region provided for makeBucket is optional.
39− makeBucket bucket (Just "us-east-1")
31+ let bucket = "my-bucket"
32+ res <-
33+ runMinio minioPlayCI $
34+ -- N B the region provided for makeBucket is optional.
35+ makeBucket bucket (Just "us-east-1")
4036 print res
examples/PresignedGetObject.hs+39−34
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,42 +16,41 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.IO.Class (liftIO)
24−import qualified Data.ByteString.Char8 as B
25−import Data.CaseInsensitive (original)
21+import Control.Monad.IO.Class (liftIO)
22+import qualified Data.ByteString.Char8 as B
23+import Data.CaseInsensitive (original)
2624import qualified Data.Conduit.Combinators as CC
27−import qualified Data.Text.Encoding as E
25+import qualified Data.Text.Encoding as E
26+import Network.Minio
2827
2928-- | The following example uses minio's play server at
30−-- https://play.minio.io:9000. The endpoint and associated
29+-- https://play.min.io. The endpoint and associated
3130-- credentials are provided via the libary constant,
3231--
3332-- > minioPlayCI :: ConnectInfo
34−--
35−
3633main :: IO ()
3734main = do
38− let
39− bucket = "my-bucket"
40− object = "my-object"
41− kb15 = 15*1024
42−
43− -- Set query parameter to modify content disposition response
44− -- header
45− queryParam = [("response-content-disposition",
46− Just "attachment; filename=\"your-filename.txt\"")]
35+ let bucket = "my-bucket"
36+ object = "my-object"
37+ kb15 = 15 * 1024
38+ -- Set query parameter to modify content disposition response
39+ -- header
40+ queryParam =
41+ [ ( "response-content-disposition",
42+ Just "attachment; filename=\"your-filename.txt\""
43+ )
44+ ]
4745
4846 res <- runMinio minioPlayCI $ do
4947 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
50− putObject bucket object (CC.repeat "a") (Just kb15)
51− liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
48+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
49+ liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
5250
5351 -- Extract Etag of uploaded object
54− (ObjectInfo _ _ etag _) <- statObject bucket object
52+ oi <- statObject bucket object defaultGetObjectOptions
53+ let etag = oiETag oi
5554
5655 -- Set header to add an if-match constraint - this makes sure
5756 -- the fetching fails if the object is changed on the server
@@ -60,23 +59,29 @@ main = do
6059 -- Generate a URL with 7 days expiry time - note that the headers
6160 -- used above must be added to the request with the signed URL
6261 -- generated.
63− url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)
64− queryParam headers
62+ url <-
63+ presignedGetObjectUrl
64+ "my-bucket"
65+ "my-object"
66+ (7 * 24 * 3600)
67+ queryParam
68+ headers
6569
6670 return (headers, etag, url)
6771
6872 case res of
6973 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
70− Right (headers, etag, url) -> do
71−
74+ Right (headers, _, url) -> do
7275 -- We generate a curl command to demonstrate usage of the signed
7376 -- URL.
74− let
75− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
76− curlCmd = B.intercalate " " $
77− ["curl --fail"] ++ map hdrOpt headers ++
78− ["-o /tmp/myfile", B.concat ["'", url, "'"]]
77+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
78+ curlCmd =
79+ B.intercalate " " $
80+ ["curl --fail"]
81+ ++ map hdrOpt headers
82+ ++ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
7983
80− putStrLn $ "The following curl command would use the presigned " ++
81− "URL to fetch the object and write it to \"/tmp/myfile\":"
84+ putStrLn $
85+ "The following curl command would use the presigned "
86+ ++ "URL to fetch the object and write it to \"/tmp/myfile\":"
8287 B.putStrLn curlCmd
examples/PresignedPostPolicy.hs+45−42
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,69 +16,72 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import qualified Data.ByteString as B
21+import qualified Data.ByteString as B
2422import qualified Data.ByteString.Char8 as Char8
25−import qualified Data.Map.Strict as Map
26−import qualified Data.Text.Encoding as Enc
27−import qualified Data.Time as Time
23+import qualified Data.HashMap.Strict as H
24+import qualified Data.Text.Encoding as Enc
25+import qualified Data.Time as Time
26+import Network.Minio
2827
2928-- | The following example uses minio's play server at
30−-- https://play.minio.io:9000. The endpoint and associated
29+-- https://play.min.io. The endpoint and associated
3130-- credentials are provided via the libary constant,
3231--
3332-- > minioPlayCI :: ConnectInfo
34−--
35−
3633main :: IO ()
3734main = do
3835 now <- Time.getCurrentTime
39− let
40− bucket = "my-bucket"
41− object = "my-object"
42−
43− -- set an expiration time of 10 days
44− expireTime = Time.addUTCTime (3600 * 24 * 10) now
45−
46− -- create a policy with expiration time and conditions - since the
47− -- conditions are validated, newPostPolicy returns an Either value
48− policyE = newPostPolicy expireTime
49− [ -- set the object name condition
50− ppCondKey "photos/my-object"
51− -- set the bucket name condition
52− , ppCondBucket "my-bucket"
53− -- set the size range of object as 1B to 10MiB
54− , ppCondContentLengthRange 1 (10*1024*1024)
55− -- set content type as jpg image
56− , ppCondContentType "image/jpeg"
57− -- on success set the server response code to 200
58− , ppCondSuccessActionStatus 200
59− ]
36+ let bucket = "my-bucket"
37+ object = "photos/my-object"
38+ -- set an expiration time of 10 days
39+ expireTime = Time.addUTCTime (3600 * 24 * 10) now
40+ -- create a policy with expiration time and conditions - since the
41+ -- conditions are validated, newPostPolicy returns an Either value
42+ policyE =
43+ newPostPolicy
44+ expireTime
45+ [ -- set the object name condition
46+ ppCondKey object,
47+ -- set the bucket name condition
48+ ppCondBucket bucket,
49+ -- set the size range of object as 1B to 10MiB
50+ ppCondContentLengthRange 1 (10 * 1024 * 1024),
51+ -- set content type as jpg image
52+ ppCondContentType "image/jpeg",
53+ -- on success set the server response code to 200
54+ ppCondSuccessActionStatus 200
55+ ]
6056
6157 case policyE of
62− Left err -> putStrLn $ show err
58+ Left err -> print err
6359 Right policy -> do
6460 res <- runMinio minioPlayCI $ do
6561 (url, formData) <- presignedPostPolicy policy
6662
6763 -- a curl command is output to demonstrate using the generated
6864 -- URL and form-data
69− let
70− formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
71− "'", v, "'"]
72− formOptions = B.intercalate " " $ map formFn $ Map.toList formData
73−
65+ let formFn (k, v) =
66+ B.concat
67+ [ "-F ",
68+ Enc.encodeUtf8 k,
69+ "=",
70+ "'",
71+ v,
72+ "'"
73+ ]
74+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
7475
75− return $ B.intercalate " " $
76− ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
76+ return $
77+ B.intercalate
78+ " "
79+ ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
7780
7881 case res of
79− Left e -> putStrLn $ "post-policy error: " ++ (show e)
82+ Left e -> putStrLn $ "post-policy error: " ++ show e
8083 Right cmd -> do
81− putStrLn $ "Put a photo at /tmp/photo.jpg and run command:\n"
84+ putStrLn "Put a photo at /tmp/photo.jpg and run command:\n"
8285
8386 -- print the generated curl command
8487 Char8.putStrLn cmd
examples/PresignedPutObject.hs+23−24
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,44 +16,43 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
2321import qualified Data.ByteString.Char8 as B
24−import Data.CaseInsensitive (original)
22+import Data.CaseInsensitive (original)
23+import Network.Minio
2524
2625-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
35− let
36− -- Use headers to set user-metadata - note that this header will
37− -- need to be set when the URL is used to make an upload.
38− headers = [("x-amz-meta-url-creator",
39− "minio-hs-presigned-put-example")]
32+ let -- Use headers to set user-metadata - note that this header will
33+ -- need to be set when the URL is used to make an upload.
34+ headers =
35+ [ ( "x-amz-meta-url-creator",
36+ "minio-hs-presigned-put-example"
37+ )
38+ ]
4039 res <- runMinio minioPlayCI $ do
41−
4240 -- generate a URL with 7 days expiry time
43− presignedPutObjectUrl "my-bucket" "my-object" (7*24*3600) headers
41+ presignedPutObjectUrl "my-bucket" "my-object" (7 * 24 * 3600) headers
4442
4543 case res of
4644 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
4745 Right url -> do
48−
4946 -- We generate a curl command to demonstrate usage of the signed
5047 -- URL.
51− let
52− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
53− curlCmd = B.intercalate " " $
54− ["curl "] ++ map hdrOpt headers ++
55− ["-T /tmp/myfile", B.concat ["'", url, "'"]]
56−
57− putStrLn $ "The following curl command would use the presigned " ++
58− "URL to upload the file at \"/tmp/myfile\":"
48+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
49+ curlCmd =
50+ B.intercalate " " $
51+ ["curl "]
52+ ++ map hdrOpt headers
53+ ++ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
54+
55+ putStrLn $
56+ "The following curl command would use the presigned "
57+ ++ "URL to upload the file at \"/tmp/myfile\":"
5958 B.putStrLn curlCmd
examples/PutObject.hs+16−18
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,38 +16,36 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
2321import qualified Data.Conduit.Combinators as CC
22+import Network.Minio
23+import Prelude
2424
2525-- | The following example uses minio's play server at
26−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
2727-- credentials are provided via the libary constant,
2828--
2929-- > minioPlayCI :: ConnectInfo
30−--
31−
3230main :: IO ()
3331main = do
34− let
35− bucket = "test"
32+ let bucket = "test"
3633 object = "obj"
3734 localFile = "/etc/lsb-release"
3835 kb15 = 15 * 1024
3936
40− -- Eg 1. Upload a stream of repeating "a" using putObject.
41− res1 <- runMinio minioPlayCI $
42− putObject bucket object (CC.repeat "a") (Just kb15)
37+ -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
38+ res1 <-
39+ runMinio minioPlayCI $
40+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
4341 case res1 of
44− Left e -> putStrLn $ "putObject failed." ++ show e
42+ Left e -> putStrLn $ "putObject failed." ++ show e
4543 Right () -> putStrLn "putObject succeeded."
4644
47−
48− -- Eg 2. Upload a file using fPutObject.
49− res2 <- runMinio minioPlayCI $
50− fPutObject bucket object localFile
45+ -- Eg 2. Upload a file using fPutObject with default options.
46+ res2 <-
47+ runMinio minioPlayCI $
48+ fPutObject bucket object localFile defaultPutObjectOptions
5149 case res2 of
52− Left e -> putStrLn $ "fPutObject failed." ++ show e
50+ Left e -> putStrLn $ "fPutObject failed." ++ show e
5351 Right () -> putStrLn "fPutObject succeeded."
examples/README.mdnew file+17
@@ -0,0 +1,17 @@
1+# Examples
2+
3+The examples in this directory illustrate usage of various APIs provided by this library. Each file is self-contained and can be run like a script directly.
4+
5+To build the examples, the build flag `examples` needs to be turned on:
6+
7+```sh
8+stack build --flag minio-hs:examples
9+```
10+
11+Now to run and example script [BucketExists.hs](https://github.com/minio/minio-hs/blob/master/examples/BucketExists.hs):
12+
13+```sh
14+stack exec BucketExists
15+```
16+
17+The CI system is configured to build these examples with every change, so they should be current.
examples/Removebucket.hs → examples/RemoveBucket.hsrenamed+6−11
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,23 +16,18 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
20−
2119{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
2320
24−import Prelude
21+import Network.Minio
22+import Prelude
2523
2624-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
2826-- credentials are provided via the libary constant,
2927--
3028-- > minioPlayCI :: ConnectInfo
31−--
32−
3329main :: IO ()
3430main = do
35− let
36− bucket = "my-bucket"
31+ let bucket = "my-bucket"
3732 res <- runMinio minioPlayCI $ removeBucket bucket
3833 print res
examples/RemoveIncompleteUpload.hs+10−13
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,27 +16,24 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Prelude
21+import Network.Minio
22+import Prelude
2423
2524-- | The following example uses minio's play server at
26−-- https://play.minio.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
2726-- credentials are provided via the libary constant,
2827--
2928-- > minioPlayCI :: ConnectInfo
30−--
31−
3229main :: IO ()
3330main = do
34− let
35− bucket = "mybucket"
36− object = "myobject"
31+ let bucket = "mybucket"
32+ object = "myobject"
3733
38− res <- runMinio minioPlayCI $
39− removeIncompleteUpload bucket object
34+ res <-
35+ runMinio minioPlayCI $
36+ removeIncompleteUpload bucket object
4037
4138 case res of
4239 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/RemoveObject.hs+9−10
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
33
44--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,20 +16,19 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
2120
22−import Network.Minio
23−import Prelude
21+import Network.Minio
22+import Prelude
2423
2524main :: IO ()
2625main = do
27− let
28− bucket = "mybucket"
29− object = "myobject"
26+ let bucket = "mybucket"
27+ object = "myobject"
3028
31− res <- runMinio minioPlayCI $
32− removeObject bucket object
29+ res <-
30+ runMinio minioPlayCI $
31+ removeObject bucket object
3332
3433 case res of
3534 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/SelectObject.hsnew file+47
@@ -0,0 +1,47 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2019 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+{-# LANGUAGE OverloadedStrings #-}
20+
21+import qualified Conduit as C
22+import Control.Monad (unless)
23+import Network.Minio
24+import Prelude
25+
26+main :: IO ()
27+main = do
28+ let bucket = "selectbucket"
29+ object = "1.csv"
30+ content =
31+ "Name,Place,Temperature\n"
32+ <> "James,San Jose,76\n"
33+ <> "Alicia,San Leandro,88\n"
34+ <> "Mark,San Carlos,90\n"
35+
36+ res <- runMinio minioPlayCI $ do
37+ exists <- bucketExists bucket
38+ unless exists $
39+ makeBucket bucket Nothing
40+
41+ C.liftIO $ putStrLn "Uploading csv object"
42+ putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
43+
44+ let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
45+ res <- selectObjectContent bucket object sr
46+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
47+ print res
examples/ServerInfo.hsnew file+30
@@ -0,0 +1,30 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ getServerInfo
30+ print res
examples/ServiceSendRestart.hsnew file+29
@@ -0,0 +1,29 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio minioPlayCI $
28+ serviceSendAction ServiceActionRestart
29+ print res
examples/ServiceSendStop.hsnew file+29
@@ -0,0 +1,29 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio minioPlayCI $
28+ serviceSendAction ServiceActionStop
29+ print res
examples/ServiceStatus.hsnew file+30
@@ -0,0 +1,30 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ serviceStatus
30+ print res
examples/SetConfig.hsnew file+31
@@ -0,0 +1,31 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+{-# LANGUAGE OverloadedStrings #-}
20+
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
24+
25+main :: IO ()
26+main = do
27+ res <- runMinio minioPlayCI $
28+ do
29+ let config = "{\"version\":\"25\",\"credential\":{\"accessKey\":\"minio\",\"secretKey\":\"minio123\"},\"region\":\"\",\"browser\":\"on\",\"worm\":\"off\",\"domain\":\"\",\"storageclass\":{\"standard\":\"\",\"rrs\":\"\"},\"cache\":{\"drives\":[],\"expiry\":90,\"exclude\":[]},\"notify\":{\"amqp\":{\"2\":{\"enable\":false,\"url\":\"amqp://guest:guest@localhost:5672/\",\"exchange\":\"minio\",\"routingKey\":\"minio\",\"exchangeType\":\"direct\",\"deliveryMode\":0,\"mandatory\":false,\"immediate\":false,\"durable\":false,\"internal\":false,\"noWait\":false,\"autoDeleted\":false}},\"elasticsearch\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"url\":\"http://localhost:9200\",\"index\":\"minio_events\"}},\"kafka\":{\"1\":{\"enable\":false,\"brokers\":null,\"topic\":\"\"}},\"mqtt\":{\"1\":{\"enable\":false,\"broker\":\"\",\"topic\":\"\",\"qos\":0,\"clientId\":\"\",\"username\":\"\",\"password\":\"\",\"reconnectInterval\":0,\"keepAliveInterval\":0}},\"mysql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"dsnString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"nats\":{\"1\":{\"enable\":false,\"address\":\"\",\"subject\":\"\",\"username\":\"\",\"password\":\"\",\"token\":\"\",\"secure\":false,\"pingInterval\":0,\"streaming\":{\"enable\":false,\"clusterID\":\"\",\"clientID\":\"\",\"async\":false,\"maxPubAcksInflight\":0}}},\"postgresql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"connectionString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"redis\":{\"test1\":{\"enable\":true,\"format\":\"namespace\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_ns\"},\"test2\":{\"enable\":true,\"format\":\"access\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_log\"}},\"webhook\":{\"1\":{\"enable\":true,\"endpoint\":\"http://localhost:3000\"},\"2\":{\"enable\":true,\"endpoint\":\"http://localhost:3001\"}}}}"
30+ setConfig config
31+ print res
minio-hs.cabal+283−182
@@ -1,252 +1,353 @@
1+cabal-version: 2.4
12name: minio-hs
2−version: 0.3.2
3−synopsis: A Minio Haskell Library for Amazon S3 compatible cloud
3+version: 1.7.0
4+synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
45 storage.
5−description: The Minio Haskell client library provides simple APIs to
6− access Minio, Amazon S3 and other API compatible cloud
6+description: The MinIO Haskell client library provides simple APIs to
7+ access MinIO, Amazon S3 and other API compatible cloud
78 storage servers.
89homepage: https://github.com/minio/minio-hs#readme
910license: Apache-2.0
1011license-file: LICENSE
11−author: Aditya Manthramurthy, Krishnan Parthasarathi
12−maintainer: dev@minio.io
12+author: MinIO Dev Team
13+maintainer: dev@min.io
1314category: Network, AWS, Object Storage
1415build-type: Simple
1516stability: Experimental
16−extra-source-files:
17+extra-doc-files:
1718 CHANGELOG.md
1819 CONTRIBUTING.md
1920 docs/API.md
20− examples/*.hs
2121 README.md
22+extra-source-files:
23+ examples/*.hs
2224 stack.yaml
25+tested-with: GHC == 8.10.7
26+ , GHC == 9.0.2
27+ , GHC == 9.2.8
28+ , GHC == 9.4.8
29+ , GHC == 9.6.5
30+ , GHC == 9.8.2
31+
32+source-repository head
33+ type: git
34+ location: https://github.com/minio/minio-hs.git
2335
24−cabal-version: >=1.10
36+Flag dev
37+ Description: Build package in development mode
38+ Default: False
39+ Manual: True
2540
26−library
27− hs-source-dirs: src
41+common base-settings
2842 ghc-options: -Wall
29− exposed-modules: Network.Minio
30− , Network.Minio.S3API
43+ -Wcompat
44+ -Widentities
45+ -Wincomplete-uni-patterns
46+ -Wincomplete-record-updates
47+ -haddock
48+ if impl(ghc >= 8.0)
49+ ghc-options: -Wredundant-constraints
50+ if impl(ghc >= 8.2)
51+ ghc-options: -fhide-source-paths
52+ if impl(ghc >= 8.4)
53+ ghc-options: -Wpartial-fields
54+ -- -Wmissing-export-lists
55+ if impl(ghc >= 8.8)
56+ ghc-options: -Wmissing-deriving-strategies
57+ -Werror=missing-deriving-strategies
58+ -- if impl(ghc >= 8.10)
59+ -- ghc-options: -Wunused-packages -- disabled due to bug related to mixin config
60+ if impl(ghc >= 9.0)
61+ ghc-options: -Winvalid-haddock
62+ if impl(ghc >= 9.2)
63+ ghc-options: -Wredundant-bang-patterns
64+ if flag(dev)
65+ ghc-options: -Werror
66+
67+ default-language: Haskell2010
68+
69+ default-extensions: BangPatterns
70+ , DerivingStrategies
71+ , FlexibleContexts
72+ , FlexibleInstances
73+ , LambdaCase
74+ , MultiParamTypeClasses
75+ , MultiWayIf
76+ , OverloadedStrings
77+ , RankNTypes
78+ , ScopedTypeVariables
79+ , TupleSections
80+
3181 other-modules: Lib.Prelude
3282 , Network.Minio.API
83+ , Network.Minio.APICommon
3384 , Network.Minio.Data
3485 , Network.Minio.Data.ByteString
3586 , Network.Minio.Data.Crypto
3687 , Network.Minio.Data.Time
88+ , Network.Minio.CopyObject
3789 , Network.Minio.Errors
3890 , Network.Minio.ListOps
3991 , Network.Minio.PresignedOperations
4092 , Network.Minio.PutObject
93+ , Network.Minio.SelectAPI
4194 , Network.Minio.Sign.V4
4295 , Network.Minio.Utils
4396 , Network.Minio.XmlGenerator
4497 , Network.Minio.XmlParser
98+ , Network.Minio.XmlCommon
99+ , 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+
45108 build-depends: base >= 4.7 && < 5
46− , protolude >= 0.1.6
47− , aeson
48− , async
49− , base64-bytestring
50− , bytestring
51− , case-insensitive
52− , conduit
53− , conduit-combinators
54− , conduit-extra
55− , containers
56− , cryptonite
57− , cryptonite-conduit
58− , data-default
59− , exceptions
60− , filepath
61− , http-client
62− , http-conduit
63− , http-types
64− , lifted-async
65− , lifted-base
66− , memory
67− , monad-control
68− , resourcet
69− , text
70− , text-format
71− , time
72− , transformers
73− , transformers-base
74− , vector
75− , xml-conduit
76− default-language: Haskell2010
77− default-extensions: FlexibleContexts
78− , FlexibleInstances
79− , BangPatterns
80− , MultiParamTypeClasses
81− , MultiWayIf
82− , NoImplicitPrelude
83− , OverloadedStrings
84− , RankNTypes
85− , ScopedTypeVariables
86− , TypeFamilies
87− , TupleSections
109+ , relude >= 0.7 && < 2
110+ , aeson >= 1.2 && < 3
111+ , base64-bytestring >= 1.0
112+ , binary >= 0.8.5.0
113+ , bytestring >= 0.10
114+ , case-insensitive >= 1.2
115+ , conduit >= 1.3
116+ , conduit-extra >= 1.3
117+ , crypton-connection
118+ , cryptonite >= 0.25
119+ , cryptonite-conduit >= 0.2
120+ , data-default-class
121+ , digest >= 0.0.1
122+ , directory
123+ , filepath >= 1.4
124+ , http-client >= 0.5
125+ , http-client-tls
126+ , http-conduit >= 2.3
127+ , http-types >= 0.12
128+ , ini
129+ , memory >= 0.14
130+ , network-uri
131+ , resourcet >= 1.2
132+ , retry
133+ , text >= 1.2
134+ , time >= 1.9
135+ , time-units ^>= 1.0.0
136+ , transformers >= 0.5
137+ , unliftio >= 0.2 && < 0.3
138+ , unliftio-core >= 0.2 && < 0.3
139+ , unordered-containers >= 0.2
140+ , xml-conduit >= 1.8
141+
142+library
143+ import: base-settings
144+ hs-source-dirs: src
145+ exposed-modules: Network.Minio
146+ , Network.Minio.AdminAPI
147+ , Network.Minio.S3API
88148
89149Flag live-test
90− Default: True
150+ Description: Build the test suite that runs against a live MinIO server
151+ Default: False
91152 Manual: True
92153
93154test-suite minio-hs-live-server-test
155+ import: base-settings
94156 type: exitcode-stdio-1.0
95157 hs-source-dirs: test, src
96158 main-is: LiveServer.hs
97159 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
98− default-language: Haskell2010
99− default-extensions: BangPatterns
100− , FlexibleContexts
101− , FlexibleInstances
102− , OverloadedStrings
103− , NoImplicitPrelude
104− , MultiParamTypeClasses
105− , MultiWayIf
106− , ScopedTypeVariables
107− , RankNTypes
108− , TupleSections
109− , TypeFamilies
110− other-modules: Lib.Prelude
111− , Network.Minio
112− , Network.Minio.API
113− , Network.Minio.Data
114− , Network.Minio.Data.ByteString
115− , Network.Minio.Data.Crypto
116− , Network.Minio.Data.Time
117− , Network.Minio.Errors
118− , Network.Minio.ListOps
119− , Network.Minio.PresignedOperations
120− , Network.Minio.PutObject
160+ other-modules: Network.Minio
121161 , Network.Minio.S3API
122− , Network.Minio.Sign.V4
123− , Network.Minio.Utils
124− , Network.Minio.Utils.Test
162+ , Network.Minio.AdminAPI
125163 , Network.Minio.API.Test
126− , Network.Minio.XmlGenerator
164+ , Network.Minio.JsonParser.Test
165+ , Network.Minio.TestHelpers
166+ , Network.Minio.Utils.Test
127167 , Network.Minio.XmlGenerator.Test
128− , Network.Minio.XmlParser
129168 , Network.Minio.XmlParser.Test
130− build-depends: base
131− , minio-hs
132− , protolude >= 0.1.6
133− , aeson
134− , async
135− , base64-bytestring
136− , bytestring
137− , case-insensitive
138− , conduit
139− , conduit-combinators
140− , conduit-extra
141− , containers
142− , cryptonite
143− , cryptonite-conduit
144− , data-default
145− , directory
146− , exceptions
147− , filepath
148− , http-client
149− , http-conduit
150− , http-types
151− , lifted-async
152− , lifted-base
153− , memory
154− , monad-control
155− , QuickCheck
156− , resourcet
169+ , Network.Minio.Credentials
170+ build-depends: minio-hs
171+ , raw-strings-qq
157172 , tasty
158173 , tasty-hunit
159174 , tasty-quickcheck
160175 , tasty-smallcheck
161− , temporary
162− , text
163− , text-format
164− , time
165− , transformers
166− , transformers-base
167− , vector
168− , xml-conduit
176+ , QuickCheck
169177 if !flag(live-test)
170178 buildable: False
171179
172180test-suite minio-hs-test
181+ import: base-settings
173182 type: exitcode-stdio-1.0
174183 hs-source-dirs: test, src
175184 main-is: Spec.hs
176− build-depends: base
177− , minio-hs
178− , protolude >= 0.1.6
179− , aeson
180− , async
181− , base64-bytestring
182− , bytestring
183− , case-insensitive
184− , conduit
185− , conduit-combinators
186− , conduit-extra
187− , containers
188− , cryptonite
189− , cryptonite-conduit
190− , data-default
191− , directory
192− , exceptions
193− , filepath
194− , http-client
195− , http-conduit
196− , http-types
197− , lifted-async
198− , lifted-base
199− , memory
200− , monad-control
185+ build-depends: minio-hs
186+ , raw-strings-qq
201187 , QuickCheck
202− , resourcet
203188 , tasty
204189 , tasty-hunit
205190 , tasty-quickcheck
206191 , tasty-smallcheck
207− , temporary
208− , text
209− , text-format
210− , time
211− , transformers
212− , transformers-base
213− , vector
214− , xml-conduit
215192 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
216− default-language: Haskell2010
217− default-extensions: BangPatterns
218− , FlexibleContexts
219− , FlexibleInstances
220− , OverloadedStrings
221− , NoImplicitPrelude
222− , MultiParamTypeClasses
223− , MultiWayIf
224− , ScopedTypeVariables
225− , RankNTypes
226− , TupleSections
227− , TypeFamilies
228193 other-modules: Lib.Prelude
229194 , Network.Minio
230− , Network.Minio.API
231− , Network.Minio.Data
232− , Network.Minio.Data.ByteString
233− , Network.Minio.Data.Crypto
234− , Network.Minio.Data.Time
235− , Network.Minio.Errors
236− , Network.Minio.ListOps
237− , Network.Minio.PresignedOperations
238− , Network.Minio.PutObject
239195 , Network.Minio.S3API
240− , Network.Minio.Sign.V4
241− , Network.Minio.Utils
242− , Network.Minio.Utils.Test
196+ , Network.Minio.AdminAPI
197+ , Network.Minio.TestHelpers
243198 , Network.Minio.API.Test
244− , Network.Minio.XmlGenerator
199+ , Network.Minio.JsonParser.Test
200+ , Network.Minio.Utils.Test
245201 , Network.Minio.XmlGenerator.Test
246− , Network.Minio.XmlParser
247202 , Network.Minio.XmlParser.Test
203+ , Network.Minio.Credentials
248204
205+Flag examples
206+ Description: Build the examples
207+ Default: False
208+ Manual: True
249209
250−source-repository head
251− type: git
252− location: https://github.com/minio/minio-hs
210+common examples-settings
211+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
212+ default-language: Haskell2010
213+ default-extensions: OverloadedStrings
214+ build-depends: base >= 4.7 && < 5
215+ , bytestring
216+ , case-insensitive
217+ , conduit
218+ , conduit-extra
219+ , filepath
220+ , minio-hs
221+ , optparse-applicative
222+ , text
223+ , time
224+ , unliftio
225+ , unordered-containers
226+ hs-source-dirs: examples
227+ if !flag(examples)
228+ buildable: False
229+
230+executable BucketExists
231+ import: examples-settings
232+ scope: private
233+ main-is: BucketExists.hs
234+
235+executable CopyObject
236+ import: examples-settings
237+ scope: private
238+ main-is: CopyObject.hs
239+
240+executable FileUploader
241+ import: examples-settings
242+ scope: private
243+ main-is: FileUploader.hs
244+
245+executable GetConfig
246+ import: examples-settings
247+ scope: private
248+ main-is: GetConfig.hs
249+
250+executable GetObject
251+ import: examples-settings
252+ scope: private
253+ main-is: GetObject.hs
254+
255+executable HeadObject
256+ import: examples-settings
257+ scope: private
258+ main-is: HeadObject.hs
259+
260+executable Heal
261+ import: examples-settings
262+ scope: private
263+ main-is: Heal.hs
264+
265+executable ListBuckets
266+ import: examples-settings
267+ scope: private
268+ main-is: ListBuckets.hs
269+
270+executable ListIncompleteUploads
271+ import: examples-settings
272+ scope: private
273+ main-is: ListIncompleteUploads.hs
274+
275+executable ListObjects
276+ import: examples-settings
277+ scope: private
278+ main-is: ListObjects.hs
279+
280+executable MakeBucket
281+ import: examples-settings
282+ scope: private
283+ main-is: MakeBucket.hs
284+
285+executable PresignedGetObject
286+ import: examples-settings
287+ scope: private
288+ main-is: PresignedGetObject.hs
289+
290+executable PresignedPostPolicy
291+ import: examples-settings
292+ scope: private
293+ main-is: PresignedPostPolicy.hs
294+
295+executable PresignedPutObject
296+ import: examples-settings
297+ scope: private
298+ main-is: PresignedPutObject.hs
299+
300+executable PutObject
301+ import: examples-settings
302+ scope: private
303+ main-is: PutObject.hs
304+
305+executable RemoveBucket
306+ import: examples-settings
307+ scope: private
308+ main-is: RemoveBucket.hs
309+
310+executable RemoveIncompleteUpload
311+ import: examples-settings
312+ scope: private
313+ main-is: RemoveIncompleteUpload.hs
314+
315+executable RemoveObject
316+ import: examples-settings
317+ scope: private
318+ main-is: RemoveObject.hs
319+
320+executable SelectObject
321+ import: examples-settings
322+ scope: private
323+ main-is: SelectObject.hs
324+
325+executable ServerInfo
326+ import: examples-settings
327+ scope: private
328+ main-is: ServerInfo.hs
329+
330+executable ServiceSendRestart
331+ import: examples-settings
332+ scope: private
333+ main-is: ServiceSendRestart.hs
334+
335+executable ServiceSendStop
336+ import: examples-settings
337+ scope: private
338+ main-is: ServiceSendStop.hs
339+
340+executable ServiceStatus
341+ import: examples-settings
342+ scope: private
343+ main-is: ServiceStatus.hs
344+
345+executable SetConfig
346+ import: examples-settings
347+ scope: private
348+ main-is: SetConfig.hs
349+
350+executable AssumeRole
351+ import: examples-settings
352+ scope: private
353+ main-is: AssumeRole.hs
src/Lib/Prelude.hs+34−25
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,32 +15,41 @@
1515--
1616
1717module Lib.Prelude
18− ( module Exports
19− , both
18+ ( module Exports,
19+ both,
20+ showBS,
21+ toStrictBS,
22+ fromStrictBS,
23+ lastMay,
24+ )
25+where
26+
27+import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
28+import qualified Data.ByteString.Lazy as LB
29+import Data.Time as Exports
30+ ( UTCTime (..),
31+ diffUTCTime,
32+ )
33+import UnliftIO as Exports
34+ ( Handler,
35+ catch,
36+ catches,
37+ throwIO,
38+ try,
39+ )
2040
21− , format
22− , formatBS
23− ) where
24−
25−import Protolude as Exports
26−
27−import Data.Time as Exports (UTCTime(..), diffUTCTime)
28−import Control.Monad.Trans.Maybe as Exports (runMaybeT, MaybeT(..))
29−
30−import Control.Monad.Catch as Exports (throwM, MonadThrow, MonadCatch)
31−
32−import Data.Text.Format as Exports (Shown(..))
33−import qualified Data.Text.Format as TF
34−import Data.Text.Format.Params (Params)
41+-- | Apply a function on both elements of a pair
42+both :: (a -> b) -> (a, a) -> (b, b)
43+both f (a, b) = (f a, f b)
3544
36−format :: Params ps => TF.Format -> ps -> Text
37−format f args = toS $ TF.format f args
45+showBS :: (Show a) => a -> ByteString
46+showBS a = encodeUtf8 (show a :: Text)
3847
39−formatBS :: Params ps => TF.Format -> ps -> ByteString
40−formatBS f args = toS $ TF.format f args
48+toStrictBS :: LByteString -> ByteString
49+toStrictBS = LB.toStrict
4150
42−-- import Data.Tuple as Exports (uncurry)
51+fromStrictBS :: ByteString -> LByteString
52+fromStrictBS = LB.fromStrict
4353
44−-- | Apply a function on both elements of a pair
45−both :: (a -> b) -> (a, a) -> (b, b)
46−both f (a, b) = (f a, f b)
54+lastMay :: [a] -> Maybe a
55+lastMay a = last <$> nonEmpty a
src/Network/Minio.hs+280−160
@@ -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.
@@ -14,143 +14,239 @@
1414-- limitations under the License.
1515--
1616
17−
18−
17+-- |
18+-- Module: Network.Minio
19+-- Copyright: (c) 2017-2023 MinIO Dev Team
20+-- License: Apache 2.0
21+-- Maintainer: MinIO Dev Team <dev@min.io>
22+--
23+-- Types and functions to conveniently access S3 compatible object
24+-- storage servers like MinIO.
1925module Network.Minio
20− (
21−
22− -- * Connecting to object storage
23− ---------------------------------
24− ConnectInfo(..)
25− , awsCI
26−
27− -- ** Connection helpers
28− ------------------------
29− , awsWithRegionCI
30− , minioPlayCI
31− , minioCI
32−
33− -- * Minio Monad
34− ----------------
35− -- | The Minio monad provides connection-reuse, bucket-location
36− -- caching, resource management and simpler error handling
37− -- functionality. All actions on object storage are performed within
38− -- this Monad.
39−
40− , Minio
41− , runMinio
42− , def
43−
44− -- * Bucket Operations
45− ----------------------
46−
47− -- ** Creation, removal and querying
48− , Bucket
49− , makeBucket
50− , removeBucket
51− , bucketExists
52− , Region
53− , getLocation
54−
55− -- ** Listing
56− , BucketInfo(..)
57− , listBuckets
58− , ObjectInfo(..)
59− , listObjects
60− , listObjectsV1
61− , UploadId
62− , UploadInfo(..)
63− , listIncompleteUploads
64− , ObjectPartInfo(..)
65− , listIncompleteParts
66−
67− -- ** Notifications
68− , Notification(..)
69− , NotificationConfig(..)
70− , Arn
71− , Event(..)
72− , Filter(..)
73− , FilterKey(..)
74− , FilterRules(..)
75− , FilterRule(..)
76− , getBucketNotification
77− , putBucketNotification
78− , removeAllBucketNotification
79−
80− -- * Object Operations
81− ----------------------
82−
83− , Object
84−
85− -- ** File operations
86− , fGetObject
87− , fPutObject
88−
89− -- ** Conduit-based streaming operations
90− , putObject
91− , getObject
92−
93− -- ** Server-side copying
94− , CopyPartSource(..)
95− , copyObject
96−
97− -- ** Querying
98− , statObject
99−
100− -- ** Object removal functions
101− , removeObject
102− , removeIncompleteUpload
103−
104− -- * Presigned Operations
105− -------------------------
106− , UrlExpiry
107− , presignedPutObjectUrl
108− , presignedGetObjectUrl
109− , presignedHeadObjectUrl
110−
111− -- ** Utilities for POST (browser) uploads
112− , PostPolicy
113− , PostPolicyError(..)
114− , newPostPolicy
115− , presignedPostPolicy
116− , showPostPolicy
117−
118− -- *** Utilities to specify Post Policy conditions
119− , PostPolicyCondition
120− , ppCondBucket
121− , ppCondContentLengthRange
122− , ppCondContentType
123− , ppCondKey
124− , ppCondKeyStartsWith
125− , ppCondSuccessActionStatus
126−
127− -- * Error handling
128− -----------------------
129− -- | Data types representing various errors that may occur while working
130− -- with an object storage service.
131− , MinioErr(..)
132− , MErrV(..)
133− , ServiceErr(..)
134−
135−) where
26+ ( -- * Credentials
27+ CredentialValue (..),
28+ credentialValueText,
29+ AccessKey (..),
30+ SecretKey (..),
31+ SessionToken (..),
32+
33+ -- ** Credential Loaders
34+
35+ -- | Run actions that retrieve 'CredentialValue's from the environment or
36+ -- files or other custom sources.
37+ CredentialLoader,
38+ fromAWSConfigFile,
39+ fromAWSEnv,
40+ fromMinioEnv,
41+ findFirst,
42+
43+ -- * Connecting to object storage
44+ ConnectInfo,
45+ setRegion,
46+ setCreds,
47+ setCredsFrom,
48+ isConnectInfoSecure,
49+ disableTLSCertValidation,
50+ MinioConn,
51+ mkMinioConn,
52+
53+ -- ** Connection helpers
54+
55+ -- | These are helpers to construct 'ConnectInfo' values for common
56+ -- cases.
57+ minioPlayCI,
58+ awsCI,
59+ gcsCI,
60+
61+ -- ** STS Credential types
62+ STSAssumeRole (..),
63+ STSAssumeRoleOptions (..),
64+ defaultSTSAssumeRoleOptions,
65+ requestSTSCredential,
66+ setSTSCredential,
67+ ExpiryTime (..),
68+ STSCredentialProvider,
69+
70+ -- * Minio Monad
71+
72+ ----------------
73+
74+ -- | The Minio Monad provides connection-reuse, bucket-location
75+ -- caching, resource management and simpler error handling
76+ -- functionality. All actions on object storage are performed within
77+ -- this Monad.
78+ Minio,
79+ runMinioWith,
80+ runMinio,
81+ runMinioResWith,
82+ runMinioRes,
83+
84+ -- * Bucket Operations
85+
86+ -- ** Creation, removal and querying
87+ Bucket,
88+ makeBucket,
89+ removeBucket,
90+ bucketExists,
91+ Region,
92+ getLocation,
93+
94+ -- ** Listing buckets
95+ BucketInfo (..),
96+ listBuckets,
97+
98+ -- ** Listing objects
99+ listObjects,
100+ listObjectsV1,
101+ ListItem (..),
102+ ObjectInfo,
103+ oiObject,
104+ oiModTime,
105+ oiETag,
106+ oiSize,
107+ oiUserMetadata,
108+ oiMetadata,
109+
110+ -- ** Listing incomplete uploads
111+ listIncompleteUploads,
112+ UploadId,
113+ UploadInfo (..),
114+ listIncompleteParts,
115+ ObjectPartInfo (..),
116+
117+ -- ** Bucket Notifications
118+ getBucketNotification,
119+ putBucketNotification,
120+ removeAllBucketNotification,
121+ Notification (..),
122+ defaultNotification,
123+ NotificationConfig (..),
124+ Arn,
125+ Event (..),
126+ Filter (..),
127+ defaultFilter,
128+ FilterKey (..),
129+ defaultFilterKey,
130+ FilterRules (..),
131+ defaultFilterRules,
132+ FilterRule (..),
133+
134+ -- * Object Operations
135+ Object,
136+
137+ -- ** File-based operations
138+ fGetObject,
139+ fPutObject,
140+
141+ -- ** Conduit-based streaming operations
142+ putObject,
143+ PutObjectOptions,
144+ defaultPutObjectOptions,
145+ pooContentType,
146+ pooContentEncoding,
147+ pooContentDisposition,
148+ pooContentLanguage,
149+ pooCacheControl,
150+ pooStorageClass,
151+ pooUserMetadata,
152+ pooNumThreads,
153+ pooSSE,
154+ getObject,
155+ GetObjectOptions,
156+ defaultGetObjectOptions,
157+ gooRange,
158+ gooIfMatch,
159+ gooIfNoneMatch,
160+ gooIfModifiedSince,
161+ gooIfUnmodifiedSince,
162+ gooSSECKey,
163+ GetObjectResponse,
164+ gorObjectInfo,
165+ gorObjectStream,
166+
167+ -- ** Server-side object copying
168+ copyObject,
169+ SourceInfo,
170+ defaultSourceInfo,
171+ srcBucket,
172+ srcObject,
173+ srcRange,
174+ srcIfMatch,
175+ srcIfNoneMatch,
176+ srcIfModifiedSince,
177+ srcIfUnmodifiedSince,
178+ DestinationInfo,
179+ defaultDestinationInfo,
180+ dstBucket,
181+ dstObject,
182+
183+ -- ** Querying object info
184+ statObject,
185+
186+ -- ** Object removal operations
187+ removeObject,
188+ removeIncompleteUpload,
189+
190+ -- ** Select Object Content with SQL
191+ module Network.Minio.SelectAPI,
192+
193+ -- * Server-Side Encryption Helpers
194+ mkSSECKey,
195+ SSECKey,
196+ SSE (..),
197+
198+ -- * Presigned Operations
199+ presignedPutObjectUrl,
200+ presignedGetObjectUrl,
201+ presignedHeadObjectUrl,
202+ UrlExpiry,
203+
204+ -- ** POST (browser) upload helpers
205+
206+ -- | Please see
207+ -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
208+ -- for detailed information.
209+ newPostPolicy,
210+ presignedPostPolicy,
211+ showPostPolicy,
212+ PostPolicy,
213+ PostPolicyError (..),
214+
215+ -- *** Post Policy condition helpers
216+ PostPolicyCondition,
217+ ppCondBucket,
218+ ppCondContentLengthRange,
219+ ppCondContentType,
220+ ppCondKey,
221+ ppCondKeyStartsWith,
222+ ppCondSuccessActionStatus,
223+
224+ -- * Error handling
225+
226+ -- | Data types representing various errors that may occur while
227+ -- working with an object storage service.
228+ MinioErr (..),
229+ MErrV (..),
230+ ServiceErr (..),
231+ )
232+where
136233
137234{-
138−This module exports the high-level Minio API for object storage.
235+This module exports the high-level MinIO API for object storage.
139236-}
140237
141−import qualified Data.Conduit as C
142−import qualified Data.Conduit.Binary as CB
238+import qualified Data.Conduit as C
239+import qualified Data.Conduit.Binary as CB
143240import qualified Data.Conduit.Combinators as CC
144−import Data.Default (def)
145−import qualified Data.Map as Map
146−
147−import Lib.Prelude
148−
149−import Network.Minio.Data
150−import Network.Minio.Errors
151−import Network.Minio.ListOps
152−import Network.Minio.PutObject
153−import Network.Minio.S3API
241+import Network.Minio.API
242+import Network.Minio.CopyObject
243+import Network.Minio.Credentials
244+import Network.Minio.Data
245+import Network.Minio.Errors
246+import Network.Minio.ListOps
247+import Network.Minio.PutObject
248+import Network.Minio.S3API
249+import Network.Minio.SelectAPI
154250
155251-- | Lists buckets.
156252listBuckets :: Minio [BucketInfo]
@@ -159,43 +255,65 @@ listBuckets = getService
159255-- | Fetch the object and write it to the given file safely. The
160256-- object is first written to a temporary file in the same directory
161257-- and then moved to the given path.
162−fGetObject :: Bucket -> Object -> FilePath -> Minio ()
163−fGetObject bucket object fp = do
164− src <- getObject bucket object
165− src C.$$+- CB.sinkFileCautious fp
258+fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
259+fGetObject bucket object fp opts = do
260+ src <- getObject bucket object opts
261+ C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
166262
167263-- | Upload the given file to the given object.
168−fPutObject :: Bucket -> Object -> FilePath -> Minio ()
169−fPutObject bucket object f = void $ putObjectInternal bucket object $
170− ODFile f Nothing
264+fPutObject ::
265+ Bucket ->
266+ Object ->
267+ FilePath ->
268+ PutObjectOptions ->
269+ Minio ()
270+fPutObject bucket object f opts =
271+ void $ putObjectInternal bucket object opts $ ODFile f Nothing
171272
172273-- | Put an object from a conduit source. The size can be provided if
173274-- known; this helps the library select optimal part sizes to perform
174275-- a multipart upload. If not specified, it is assumed that the object
175276-- can be potentially 5TiB and selects multipart sizes appropriately.
176−putObject :: Bucket -> Object -> C.Producer Minio ByteString
177− -> Maybe Int64 -> Minio ()
178−putObject bucket object src sizeMay =
179− void $ putObjectInternal bucket object $ ODStream src sizeMay
180−
181−-- | Perform a server-side copy operation to create an object with the
182−-- given bucket and object name from the source specification in
183−-- CopyPartSource. This function performs a multipart copy operation
184−-- if the new object is to be greater than 5GiB in size.
185−copyObject :: Bucket -> Object -> CopyPartSource -> Minio ()
186−copyObject bucket object cps = void $ copyObjectInternal bucket object cps
277+putObject ::
278+ Bucket ->
279+ Object ->
280+ C.ConduitM () ByteString Minio () ->
281+ Maybe Int64 ->
282+ PutObjectOptions ->
283+ Minio ()
284+putObject bucket object src sizeMay opts =
285+ void $ putObjectInternal bucket object opts $ ODStream src sizeMay
286+
287+-- | Perform a server-side copy operation to create an object based on
288+-- the destination specification in DestinationInfo from the source
289+-- specification in SourceInfo. This function performs a multipart
290+-- copy operation if the new object is to be greater than 5GiB in
291+-- size.
292+copyObject :: DestinationInfo -> SourceInfo -> Minio ()
293+copyObject dstInfo srcInfo =
294+ void $
295+ copyObjectInternal
296+ (dstBucket dstInfo)
297+ (dstObject dstInfo)
298+ srcInfo
187299
188300-- | Remove an object from the object store.
189301removeObject :: Bucket -> Object -> Minio ()
190302removeObject = deleteObject
191303
192−-- | Get an object from the object store as a resumable source (conduit).
193−getObject :: Bucket -> Object -> Minio (C.ResumableSource Minio ByteString)
194−getObject bucket object = snd <$> getObject' bucket object [] []
304+-- | Get an object from the object store.
305+getObject ::
306+ Bucket ->
307+ Object ->
308+ GetObjectOptions ->
309+ Minio GetObjectResponse
310+getObject bucket object opts =
311+ getObject' bucket object [] $ gooToHeaders opts
195312
196−-- | Get an object's metadata from the object store.
197−statObject :: Bucket -> Object -> Minio ObjectInfo
198−statObject = headObject
313+-- | Get an object's metadata from the object store. It accepts the
314+-- same options as GetObject.
315+statObject :: Bucket -> Object -> GetObjectOptions -> Minio ObjectInfo
316+statObject b o opts = headObject b o $ gooToHeaders opts
199317
200318-- | Creates a new bucket in the object store. The Region can be
201319-- optionally specified. If not specified, it will use the region
@@ -205,21 +323,23 @@ makeBucket :: Bucket -> Maybe Region -> Minio ()
205323makeBucket bucket regionMay = do
206324 region <- maybe (asks $ connectRegion . mcConnInfo) return regionMay
207325 putBucket bucket region
208− modify (Map.insert bucket region)
326+ addToRegionCache bucket region
209327
210328-- | Removes a bucket from the object store.
211329removeBucket :: Bucket -> Minio ()
212330removeBucket bucket = do
213331 deleteBucket bucket
214− modify (Map.delete bucket)
332+ deleteFromRegionCache bucket
215333
216334-- | Query the object store if a given bucket is present.
217335bucketExists :: Bucket -> Minio Bool
218336bucketExists = headBucket
219337
220−
221338-- | Removes an ongoing multipart upload of an object.
222339removeIncompleteUpload :: Bucket -> Object -> Minio ()
223340removeIncompleteUpload bucket object = do
224− uploads <- listIncompleteUploads bucket (Just object) False C.$$ CC.sinkList
341+ uploads <-
342+ C.runConduit $
343+ listIncompleteUploads bucket (Just object) False
344+ C..| CC.sinkList
225345 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs+284−140
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,196 +15,340 @@
1515--
1616
1717module Network.Minio.API
18− (
19− connect
20− , RequestInfo(..)
21− , runMinio
22− , executeRequest
23− , mkStreamRequest
24− , getLocation
25−
26− , isValidBucketName
27− , checkBucketNameValidity
28− , isValidObjectName
29− , checkObjectNameValidity
30− ) where
31−
32−import qualified Data.Conduit as C
33−import Data.Conduit.Binary (sourceHandleRange)
34−import Data.Default (def)
35−import qualified Data.Map as Map
18+ ( connect,
19+ S3ReqInfo (..),
20+ runMinio,
21+ executeRequest,
22+ buildRequest,
23+ mkStreamRequest,
24+ getLocation,
25+ isValidBucketName,
26+ checkBucketNameValidity,
27+ isValidObjectName,
28+ checkObjectNameValidity,
29+ requestSTSCredential,
30+ )
31+where
32+
33+import Control.Retry
34+ ( fullJitterBackoff,
35+ limitRetriesByCumulativeDelay,
36+ retrying,
37+ )
38+import qualified Data.ByteString as B
3639import qualified Data.Char as C
40+import qualified Data.Conduit as C
41+import qualified Data.HashMap.Strict as H
3742import qualified Data.Text as T
38−import qualified Data.ByteString as B
39−
40−import Network.HTTP.Conduit (Response)
43+import qualified Data.Time.Clock as Time
44+import Lib.Prelude
45+import Network.HTTP.Client (defaultManagerSettings)
46+import qualified Network.HTTP.Client as NClient
47+import Network.HTTP.Conduit (Response)
4148import qualified Network.HTTP.Conduit as NC
49+import Network.HTTP.Types (simpleQueryToQuery)
4250import qualified Network.HTTP.Types as HT
4351import Network.HTTP.Types.Header (hHost)
44−
45−import Lib.Prelude
46−
47−import Network.Minio.Data
48−import Network.Minio.Data.Crypto
49−import Network.Minio.Errors
50−import Network.Minio.Sign.V4
51−import Network.Minio.Utils
52−import Network.Minio.XmlParser
53−
54−sha256Header :: ByteString -> HT.Header
55−sha256Header = ("x-amz-content-sha256", )
56−
57−getPayloadSHA256Hash :: (MonadIO m) => Payload -> m ByteString
58−getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
59−getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
60− sourceHandleRange h
61− (return . fromIntegral $ off)
62− (return . fromIntegral $ size)
63−
64−getRequestBody :: Payload -> NC.RequestBody
65−getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
66−getRequestBody (PayloadH h off size) =
67− NC.requestBodySource (fromIntegral size) $
68− sourceHandleRange h
69− (return . fromIntegral $ off)
70− (return . fromIntegral $ size)
71−
52+import Network.Minio.APICommon
53+import Network.Minio.Credentials
54+import Network.Minio.Data
55+import Network.Minio.Errors
56+import Network.Minio.Sign.V4
57+import Network.Minio.Utils
58+import Network.Minio.XmlParser
7259
7360-- | Fetch bucket location (region)
7461getLocation :: Bucket -> Minio Region
7562getLocation bucket = do
76− resp <- executeRequest $ def {
77− riBucket = Just bucket
78− , riQueryParams = [("location", Nothing)]
79− , riNeedsLocation = False
80− }
63+ resp <-
64+ executeRequest $
65+ defaultS3ReqInfo
66+ { riBucket = Just bucket,
67+ riQueryParams = [("location", Nothing)],
68+ riNeedsLocation = False
69+ }
8170 parseLocation $ NC.responseBody resp
8271
83−
8472-- | Looks for region in RegionMap and updates it using getLocation if
8573-- absent.
86−discoverRegion :: RequestInfo -> Minio (Maybe Region)
74+discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
8775discoverRegion ri = runMaybeT $ do
8876 bucket <- MaybeT $ return $ riBucket ri
89− regionMay <- gets (Map.lookup bucket)
90− maybe (do
91− l <- lift $ getLocation bucket
92− modify $ Map.insert bucket l
93− return l
94− ) return regionMay
77+ regionMay <- lift $ lookupRegionCache bucket
78+ maybe
79+ ( do
80+ l <- lift $ getLocation bucket
81+ lift $ addToRegionCache bucket l
82+ return l
83+ )
84+ return
85+ regionMay
86+
87+-- | Returns the region to be used for the request.
88+getRegion :: S3ReqInfo -> Minio (Maybe Region)
89+getRegion ri = do
90+ ci <- asks mcConnInfo
91+
92+ -- getService/makeBucket/getLocation -- don't need location
93+ if
94+ | not $ riNeedsLocation ri ->
95+ return $ Just $ connectRegion ci
96+ -- if autodiscovery of location is disabled by user
97+ | not $ connectAutoDiscoverRegion ci ->
98+ return $ Just $ connectRegion ci
99+ -- discover the region for the request
100+ | otherwise -> discoverRegion ri
101+
102+getRegionHost :: Region -> Minio Text
103+getRegionHost r = do
104+ ci <- asks mcConnInfo
95105
106+ if "amazonaws.com" `T.isSuffixOf` connectHost ci
107+ then
108+ maybe
109+ (throwIO $ MErrVRegionNotSupported r)
110+ return
111+ (H.lookup r awsRegionMap)
112+ else return $ connectHost ci
96113
97−buildRequest :: RequestInfo -> Minio NC.Request
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+
164+buildRequest :: S3ReqInfo -> Minio NC.Request
98165buildRequest ri = do
99166 maybe (return ()) checkBucketNameValidity $ riBucket ri
100167 maybe (return ()) checkObjectNameValidity $ riObject ri
101168
102169 ci <- asks mcConnInfo
103170
104− -- getService/makeBucket/getLocation -- don't need
105− -- location
106− region <- if | not $ riNeedsLocation ri ->
107− return $ Just $ connectRegion ci
108−
109− -- if autodiscovery of location is disabled by user
110− | not $ connectAutoDiscoverRegion ci ->
111− return $ Just $ connectRegion ci
112−
113− -- discover the region for the request
114− | otherwise -> discoverRegion ri
115−
116− regionHost <- case region of
117− Nothing -> return $ connectHost ci
118− Just r -> if "amazonaws.com" `T.isSuffixOf` connectHost ci
119− then maybe
120− (throwM $ MErrVRegionNotSupported r)
121− return
122− (Map.lookup r awsRegionMap)
123− else return $ connectHost ci
124−
125− sha256Hash <- if | connectIsSecure ci ->
126− -- if secure connection
127− return "UNSIGNED-PAYLOAD"
128−
129− -- otherwise compute sha256
130− | otherwise -> getPayloadSHA256Hash (riPayload ri)
131−
132− let hostHeader = (hHost, formatBS "{}:{}" [connectHost ci,
133− show $ connectPort ci])
134−
135− newRi = ri { riPayloadHash = Just sha256Hash
136− , riHeaders = hostHeader
137− : sha256Header sha256Hash
138− : riHeaders ri
139− , riRegion = region
140− }
141− newCi = ci { connectHost = regionHost }
142−
143− signHeaders <- liftIO $ signV4 newCi newRi Nothing
144−
145− return NC.defaultRequest {
146− NC.method = riMethod newRi
147− , NC.secure = connectIsSecure newCi
148− , NC.host = encodeUtf8 $ connectHost newCi
149− , NC.port = connectPort newCi
150− , NC.path = getPathFromRI newRi
151− , NC.queryString = HT.renderQuery False $ riQueryParams newRi
152− , NC.requestHeaders = riHeaders newRi ++ mkHeaderFromPairs signHeaders
153− , NC.requestBody = getRequestBody (riPayload newRi)
154− }
155−
156−executeRequest :: RequestInfo -> Minio (Response LByteString)
171+ (host, path, regionMay) <- getHostPathRegion ri
172+
173+ let ci' = ci {connectHost = host}
174+ hostHeader = (hHost, getHostAddr ci')
175+ ri' =
176+ ri
177+ { riHeaders = hostHeader : riHeaders ri,
178+ riRegion = regionMay
179+ }
180+ -- Does not contain body and auth info.
181+ baseRequest =
182+ NC.defaultRequest
183+ { NC.method = riMethod ri',
184+ NC.secure = connectIsSecure ci',
185+ NC.host = encodeUtf8 $ connectHost ci',
186+ NC.port = connectPort ci',
187+ NC.path = path,
188+ NC.requestHeaders = riHeaders ri',
189+ NC.queryString = HT.renderQuery False $ riQueryParams ri'
190+ }
191+
192+ timeStamp <- liftIO Time.getCurrentTime
193+
194+ mgr <- asks mcConnManager
195+ cv <- liftIO $ getCredential (connectCreds ci') (getEndpoint ci') mgr
196+
197+ let sp =
198+ SignParams
199+ (coerce $ cvAccessKey cv)
200+ (coerce $ cvSecretKey cv)
201+ (coerce $ cvSessionToken cv)
202+ ServiceS3
203+ timeStamp
204+ (riRegion ri')
205+ (riPresignExpirySecs ri')
206+ Nothing
207+
208+ -- Cases to handle:
209+ --
210+ -- 0. Handle presign URL case.
211+ --
212+ -- 1. Connection is secure: use unsigned payload
213+ --
214+ -- 2. Insecure connection, streaming signature is enabled via use of
215+ -- conduit payload: use streaming signature for request.
216+ --
217+ -- 3. Insecure connection, non-conduit payload: compute payload
218+ -- sha256hash, buffer request in memory and perform request.
219+
220+ if
221+ | isJust (riPresignExpirySecs ri') ->
222+ -- case 0 from above.
223+ do
224+ let signPairs = signV4QueryParams sp baseRequest
225+ qpToAdd = simpleQueryToQuery signPairs
226+ existingQueryParams = HT.parseQuery (NC.queryString baseRequest)
227+ updatedQueryParams = existingQueryParams ++ qpToAdd
228+ return $ NClient.setQueryString updatedQueryParams baseRequest
229+ | isStreamingPayload (riPayload ri') && not (connectIsSecure ci') ->
230+ -- case 2 from above.
231+ do
232+ (pLen, pSrc) <- case riPayload ri of
233+ PayloadC l src -> return (l, src)
234+ _ -> throwIO MErrVUnexpectedPayload
235+ let reqFn = signV4Stream pLen sp baseRequest
236+ return $ reqFn pSrc
237+ | otherwise ->
238+ do
239+ sp' <-
240+ ( if connectIsSecure ci'
241+ then -- case 1 described above.
242+ return sp
243+ else
244+ ( -- case 3 described above.
245+ do
246+ pHash <- getPayloadSHA256Hash $ riPayload ri'
247+ return $ sp {spPayloadHash = Just pHash}
248+ )
249+ )
250+
251+ let signHeaders = signV4 sp' baseRequest
252+ return $
253+ baseRequest
254+ { NC.requestHeaders =
255+ NC.requestHeaders baseRequest ++ signHeaders,
256+ NC.requestBody = getRequestBody (riPayload ri')
257+ }
258+
259+retryAPIRequest :: Minio a -> Minio a
260+retryAPIRequest apiCall = do
261+ resE <-
262+ retrying retryPolicy (const shouldRetry) $
263+ const $
264+ try apiCall
265+ either throwIO return resE
266+ where
267+ -- Retry using the full-jitter backoff method for up to 10 mins
268+ -- total
269+ retryPolicy =
270+ limitRetriesByCumulativeDelay tenMins $
271+ fullJitterBackoff oneMilliSecond
272+ oneMilliSecond = 1000 -- in microseconds
273+ tenMins = 10 * 60 * 1000000 -- in microseconds
274+ -- retry on connection related failure
275+ shouldRetry :: Either NC.HttpException a -> Minio Bool
276+ shouldRetry resE =
277+ case resE of
278+ -- API request returned successfully
279+ Right _ -> return False
280+ -- API request failed with a retryable exception
281+ Left httpExn@(NC.HttpExceptionRequest _ exn) ->
282+ case (exn :: NC.HttpExceptionContent) of
283+ NC.ResponseTimeout -> return True
284+ NC.ConnectionTimeout -> return True
285+ NC.ConnectionFailure _ -> return True
286+ -- We received an unexpected exception
287+ _ -> throwIO httpExn
288+ -- We received an unexpected exception
289+ Left someOtherExn -> throwIO someOtherExn
290+
291+executeRequest :: S3ReqInfo -> Minio (Response LByteString)
157292executeRequest ri = do
158293 req <- buildRequest ri
159294 mgr <- asks mcConnManager
160− httpLbs req mgr
161−
295+ retryAPIRequest $ httpLbs req mgr
162296
163−mkStreamRequest :: RequestInfo
164− -> Minio (Response (C.ResumableSource Minio ByteString))
297+mkStreamRequest ::
298+ S3ReqInfo ->
299+ Minio (Response (C.ConduitM () ByteString Minio ()))
165300mkStreamRequest ri = do
166301 req <- buildRequest ri
167302 mgr <- asks mcConnManager
168− http req mgr
303+ retryAPIRequest $ http req mgr
169304
170305-- Bucket name validity check according to AWS rules.
171306isValidBucketName :: Bucket -> Bool
172307isValidBucketName bucket =
173− not (or [ len < 3 || len > 63
174− , or (map labelCheck labels)
175− , or (map labelCharsCheck labels)
176− , isIPCheck
177− ])
308+ not
309+ ( or
310+ [ len < 3 || len > 63,
311+ any labelCheck labels,
312+ any labelCharsCheck labels,
313+ isIPCheck
314+ ]
315+ )
178316 where
179317 len = T.length bucket
180318 labels = T.splitOn "." bucket
181−
182319 -- does label `l` fail basic checks of length and start/end?
183320 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
184−
185321 -- does label `l` have non-allowed characters?
186− labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
187− x == '-' ||
188− C.isDigit x)) l
189−
322+ labelCharsCheck l =
323+ isJust $
324+ T.find
325+ ( \x ->
326+ not
327+ ( C.isAsciiLower x
328+ || x == '-'
329+ || C.isDigit x
330+ )
331+ )
332+ l
190333 -- does label `l` have non-digit characters?
191334 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
192335 labelAsNums = map (not . labelNonDigits) labels
193−
194336 -- check if bucket name looks like an IP
195337 isIPCheck = and labelAsNums && length labelAsNums == 4
196338
197339-- Throws exception iff bucket name is invalid according to AWS rules.
198−checkBucketNameValidity :: MonadThrow m => Bucket -> m ()
340+checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
199341checkBucketNameValidity bucket =
200− when (not $ isValidBucketName bucket) $
201− throwM $ MErrVInvalidBucketName bucket
342+ unless (isValidBucketName bucket) $
343+ throwIO $
344+ MErrVInvalidBucketName bucket
202345
203346isValidObjectName :: Object -> Bool
204347isValidObjectName object =
205348 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
206349
207−checkObjectNameValidity :: MonadThrow m => Object -> m ()
350+checkObjectNameValidity :: (MonadIO m) => Object -> m ()
208351checkObjectNameValidity object =
209− when (not $ isValidObjectName object) $
210− throwM $ MErrVInvalidObjectName object
352+ unless (isValidObjectName object) $
353+ throwIO $
354+ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hsnew file+80
@@ -0,0 +1,80 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018 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.APICommon where
18+
19+import qualified Conduit as C
20+import qualified Data.ByteString as BS
21+import qualified Data.ByteString.Lazy as LB
22+import Data.Conduit.Binary (sourceHandleRange)
23+import qualified Data.Text as T
24+import Lib.Prelude
25+import qualified Network.HTTP.Conduit as NC
26+import qualified Network.HTTP.Types as HT
27+import Network.Minio.Data
28+import Network.Minio.Data.Crypto
29+import Network.Minio.Errors
30+
31+sha256Header :: ByteString -> HT.Header
32+sha256Header = ("x-amz-content-sha256",)
33+
34+-- | This function throws an error if the payload is a conduit (as it
35+-- will not be possible to re-read the conduit after it is consumed).
36+getPayloadSHA256Hash :: Payload -> Minio ByteString
37+getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
38+getPayloadSHA256Hash (PayloadH h off size) =
39+ hashSHA256FromSource $
40+ sourceHandleRange
41+ h
42+ (return . fromIntegral $ off)
43+ (return . fromIntegral $ size)
44+getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
45+
46+getRequestBody :: Payload -> NC.RequestBody
47+getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
48+getRequestBody (PayloadH h off size) =
49+ NC.requestBodySource size $
50+ sourceHandleRange
51+ h
52+ (return . fromIntegral $ off)
53+ (return . fromIntegral $ size)
54+getRequestBody (PayloadC n src) = NC.requestBodySource n src
55+
56+mkStreamingPayload :: Payload -> Payload
57+mkStreamingPayload payload =
58+ case payload of
59+ PayloadBS bs ->
60+ PayloadC
61+ (fromIntegral $ BS.length bs)
62+ (C.sourceLazy $ LB.fromStrict bs)
63+ PayloadH h off len ->
64+ PayloadC len $
65+ sourceHandleRange
66+ h
67+ (return . fromIntegral $ off)
68+ (return . fromIntegral $ len)
69+ _ -> payload
70+
71+isStreamingPayload :: Payload -> Bool
72+isStreamingPayload (PayloadC _ _) = True
73+isStreamingPayload _ = False
74+
75+-- | Checks if the connect info is for Amazon S3.
76+isAWSConnectInfo :: ConnectInfo -> Bool
77+isAWSConnectInfo ci = ".amazonaws.com" `T.isSuffixOf` connectHost ci
78+
79+bucketHasPeriods :: Bucket -> Bool
80+bucketHasPeriods b = isJust $ T.find (== '.') b
src/Network/Minio/AdminAPI.hsnew file+711
@@ -0,0 +1,711 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018-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.AdminAPI
18+ ( -- * MinIO Admin API
19+
20+ --------------------
21+
22+ -- | Provides MinIO admin API and related types. It is in
23+ -- experimental state.
24+ DriveInfo (..),
25+ ErasureInfo (..),
26+ Backend (..),
27+ ConnStats (..),
28+ HttpStats (..),
29+ ServerProps (..),
30+ CountNAvgTime (..),
31+ StorageClass (..),
32+ StorageInfo (..),
33+ SIData (..),
34+ ServerInfo (..),
35+ getServerInfo,
36+ HealOpts (..),
37+ HealResultItem (..),
38+ HealStatus (..),
39+ HealStartResp (..),
40+ startHeal,
41+ forceStartHeal,
42+ getHealStatus,
43+ SetConfigResult (..),
44+ NodeSummary (..),
45+ setConfig,
46+ getConfig,
47+ ServerVersion (..),
48+ ServiceStatus (..),
49+ serviceStatus,
50+ ServiceAction (..),
51+ serviceSendAction,
52+ )
53+where
54+
55+import Data.Aeson
56+ ( FromJSON,
57+ ToJSON,
58+ Value (Object),
59+ eitherDecode,
60+ object,
61+ pairs,
62+ parseJSON,
63+ toEncoding,
64+ toJSON,
65+ withObject,
66+ withText,
67+ (.:),
68+ (.:?),
69+ (.=),
70+ )
71+import qualified Data.Aeson as A
72+import Data.Aeson.Types (typeMismatch)
73+import qualified Data.ByteString as B
74+import qualified Data.ByteString.Lazy as LBS
75+import qualified Data.Text as T
76+import Data.Time (NominalDiffTime, getCurrentTime)
77+import Lib.Prelude
78+import Network.HTTP.Conduit (Response)
79+import qualified Network.HTTP.Conduit as NC
80+import qualified Network.HTTP.Types as HT
81+import Network.HTTP.Types.Header (hHost)
82+import Network.Minio.APICommon
83+import Network.Minio.Credentials
84+import Network.Minio.Data
85+import Network.Minio.Errors
86+import Network.Minio.Sign.V4
87+import Network.Minio.Utils
88+
89+data DriveInfo = DriveInfo
90+ { diUuid :: Text,
91+ diEndpoint :: Text,
92+ diState :: Text
93+ }
94+ deriving stock (Show, Eq)
95+
96+instance FromJSON DriveInfo where
97+ parseJSON = withObject "DriveInfo" $ \v ->
98+ DriveInfo
99+ <$> v
100+ .: "uuid"
101+ <*> v
102+ .: "endpoint"
103+ <*> v
104+ .: "state"
105+
106+data StorageClass = StorageClass
107+ { scParity :: Int,
108+ scData :: Int
109+ }
110+ deriving stock (Show, Eq)
111+
112+data ErasureInfo = ErasureInfo
113+ { eiOnlineDisks :: Int,
114+ eiOfflineDisks :: Int,
115+ eiStandard :: StorageClass,
116+ eiReducedRedundancy :: StorageClass,
117+ eiSets :: [[DriveInfo]]
118+ }
119+ deriving stock (Show, Eq)
120+
121+instance FromJSON ErasureInfo where
122+ parseJSON = withObject "ErasureInfo" $ \v -> do
123+ onlineDisks <- v .: "OnlineDisks"
124+ offlineDisks <- v .: "OfflineDisks"
125+ stdClass <-
126+ StorageClass
127+ <$> v
128+ .: "StandardSCData"
129+ <*> v
130+ .: "StandardSCParity"
131+ rrClass <-
132+ StorageClass
133+ <$> v
134+ .: "RRSCData"
135+ <*> v
136+ .: "RRSCParity"
137+ sets <- v .: "Sets"
138+ return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
139+
140+data Backend
141+ = BackendFS
142+ | BackendErasure ErasureInfo
143+ deriving stock (Show, Eq)
144+
145+instance FromJSON Backend where
146+ parseJSON = withObject "Backend" $ \v -> do
147+ typ <- v .: "Type"
148+ case typ :: Int of
149+ 1 -> return BackendFS
150+ 2 -> BackendErasure <$> parseJSON (Object v)
151+ _ -> typeMismatch "BackendType" (Object v)
152+
153+data ConnStats = ConnStats
154+ { csTransferred :: Int64,
155+ csReceived :: Int64
156+ }
157+ deriving stock (Show, Eq)
158+
159+instance FromJSON ConnStats where
160+ parseJSON = withObject "ConnStats" $ \v ->
161+ ConnStats
162+ <$> v
163+ .: "transferred"
164+ <*> v
165+ .: "received"
166+
167+data ServerProps = ServerProps
168+ { spUptime :: NominalDiffTime,
169+ spVersion :: Text,
170+ spCommitId :: Text,
171+ spRegion :: Text,
172+ spSqsArns :: [Text]
173+ }
174+ deriving stock (Show, Eq)
175+
176+instance FromJSON ServerProps where
177+ parseJSON = withObject "SIServer" $ \v -> do
178+ uptimeNs <- v .: "uptime"
179+ let uptime = uptimeNs / 1e9
180+ ver <- v .: "version"
181+ commitId <- v .: "commitID"
182+ region <- v .: "region"
183+ arn <- v .: "sqsARN"
184+ return $ ServerProps uptime ver commitId region arn
185+
186+data StorageInfo = StorageInfo
187+ { siUsed :: Int64,
188+ siBackend :: Backend
189+ }
190+ deriving stock (Show, Eq)
191+
192+instance FromJSON StorageInfo where
193+ parseJSON = withObject "StorageInfo" $ \v ->
194+ StorageInfo
195+ <$> v
196+ .: "Used"
197+ <*> v
198+ .: "Backend"
199+
200+data CountNAvgTime = CountNAvgTime
201+ { caCount :: Int64,
202+ caAvgDuration :: Text
203+ }
204+ deriving stock (Show, Eq)
205+
206+instance FromJSON CountNAvgTime where
207+ parseJSON = withObject "CountNAvgTime" $ \v ->
208+ CountNAvgTime
209+ <$> v
210+ .: "count"
211+ <*> v
212+ .: "avgDuration"
213+
214+data HttpStats = HttpStats
215+ { hsTotalHeads :: CountNAvgTime,
216+ hsSuccessHeads :: CountNAvgTime,
217+ hsTotalGets :: CountNAvgTime,
218+ hsSuccessGets :: CountNAvgTime,
219+ hsTotalPuts :: CountNAvgTime,
220+ hsSuccessPuts :: CountNAvgTime,
221+ hsTotalPosts :: CountNAvgTime,
222+ hsSuccessPosts :: CountNAvgTime,
223+ hsTotalDeletes :: CountNAvgTime,
224+ hsSuccessDeletes :: CountNAvgTime
225+ }
226+ deriving stock (Show, Eq)
227+
228+instance FromJSON HttpStats where
229+ parseJSON = withObject "HttpStats" $ \v ->
230+ HttpStats
231+ <$> v
232+ .: "totalHEADs"
233+ <*> v
234+ .: "successHEADs"
235+ <*> v
236+ .: "totalGETs"
237+ <*> v
238+ .: "successGETs"
239+ <*> v
240+ .: "totalPUTs"
241+ <*> v
242+ .: "successPUTs"
243+ <*> v
244+ .: "totalPOSTs"
245+ <*> v
246+ .: "successPOSTs"
247+ <*> v
248+ .: "totalDELETEs"
249+ <*> v
250+ .: "successDELETEs"
251+
252+data SIData = SIData
253+ { sdStorage :: StorageInfo,
254+ sdConnStats :: ConnStats,
255+ sdHttpStats :: HttpStats,
256+ sdProps :: ServerProps
257+ }
258+ deriving stock (Show, Eq)
259+
260+instance FromJSON SIData where
261+ parseJSON = withObject "SIData" $ \v ->
262+ SIData
263+ <$> v
264+ .: "storage"
265+ <*> v
266+ .: "network"
267+ <*> v
268+ .: "http"
269+ <*> v
270+ .: "server"
271+
272+data ServerInfo = ServerInfo
273+ { siError :: Text,
274+ siAddr :: Text,
275+ siData :: SIData
276+ }
277+ deriving stock (Show, Eq)
278+
279+instance FromJSON ServerInfo where
280+ parseJSON = withObject "ServerInfo" $ \v ->
281+ ServerInfo
282+ <$> v
283+ .: "error"
284+ <*> v
285+ .: "addr"
286+ <*> v
287+ .: "data"
288+
289+data ServerVersion = ServerVersion
290+ { svVersion :: Text,
291+ svCommitId :: Text
292+ }
293+ deriving stock (Show, Eq)
294+
295+instance FromJSON ServerVersion where
296+ parseJSON = withObject "ServerVersion" $ \v ->
297+ ServerVersion
298+ <$> v
299+ .: "version"
300+ <*> v
301+ .: "commitID"
302+
303+data ServiceStatus = ServiceStatus
304+ { ssVersion :: ServerVersion,
305+ ssUptime :: NominalDiffTime
306+ }
307+ deriving stock (Show, Eq)
308+
309+instance FromJSON ServiceStatus where
310+ parseJSON = withObject "ServiceStatus" $ \v -> do
311+ serverVersion <- v .: "serverVersion"
312+ uptimeNs <- v .: "uptime"
313+ let uptime = uptimeNs / 1e9
314+ return $ ServiceStatus serverVersion uptime
315+
316+data ServiceAction
317+ = ServiceActionRestart
318+ | ServiceActionStop
319+ deriving stock (Show, Eq)
320+
321+instance ToJSON ServiceAction where
322+ toJSON a = object ["action" .= serviceActionToText a]
323+
324+serviceActionToText :: ServiceAction -> Text
325+serviceActionToText a = case a of
326+ ServiceActionRestart -> "restart"
327+ ServiceActionStop -> "stop"
328+
329+adminPath :: ByteString
330+adminPath = "/minio/admin"
331+
332+data HealStartResp = HealStartResp
333+ { hsrClientToken :: Text,
334+ hsrClientAddr :: Text,
335+ hsrStartTime :: UTCTime
336+ }
337+ deriving stock (Show, Eq)
338+
339+instance FromJSON HealStartResp where
340+ parseJSON = withObject "HealStartResp" $ \v ->
341+ HealStartResp
342+ <$> v
343+ .: "clientToken"
344+ <*> v
345+ .: "clientAddress"
346+ <*> v
347+ .: "startTime"
348+
349+data HealOpts = HealOpts
350+ { hoRecursive :: Bool,
351+ hoDryRun :: Bool
352+ }
353+ deriving stock (Show, Eq)
354+
355+instance ToJSON HealOpts where
356+ toJSON (HealOpts r d) =
357+ object ["recursive" .= r, "dryRun" .= d]
358+ toEncoding (HealOpts r d) =
359+ pairs ("recursive" .= r <> "dryRun" .= d)
360+
361+instance FromJSON HealOpts where
362+ parseJSON = withObject "HealOpts" $ \v ->
363+ HealOpts
364+ <$> v
365+ .: "recursive"
366+ <*> v
367+ .: "dryRun"
368+
369+data HealItemType
370+ = HealItemMetadata
371+ | HealItemBucket
372+ | HealItemBucketMetadata
373+ | HealItemObject
374+ deriving stock (Show, Eq)
375+
376+instance FromJSON HealItemType where
377+ parseJSON = withText "HealItemType" $ \v -> case v of
378+ "metadata" -> return HealItemMetadata
379+ "bucket" -> return HealItemBucket
380+ "object" -> return HealItemObject
381+ "bucket-metadata" -> return HealItemBucketMetadata
382+ _ -> typeMismatch "HealItemType" (A.String v)
383+
384+data NodeSummary = NodeSummary
385+ { nsName :: Text,
386+ nsErrSet :: Bool,
387+ nsErrMessage :: Text
388+ }
389+ deriving stock (Show, Eq)
390+
391+instance FromJSON NodeSummary where
392+ parseJSON = withObject "NodeSummary" $ \v ->
393+ NodeSummary
394+ <$> v
395+ .: "name"
396+ <*> v
397+ .: "errSet"
398+ <*> v
399+ .: "errMsg"
400+
401+data SetConfigResult = SetConfigResult
402+ { scrStatus :: Bool,
403+ scrNodeSummary :: [NodeSummary]
404+ }
405+ deriving stock (Show, Eq)
406+
407+instance FromJSON SetConfigResult where
408+ parseJSON = withObject "SetConfigResult" $ \v ->
409+ SetConfigResult
410+ <$> v
411+ .: "status"
412+ <*> v
413+ .: "nodeResults"
414+
415+data HealResultItem = HealResultItem
416+ { hriResultIdx :: Int,
417+ hriType :: HealItemType,
418+ hriBucket :: Bucket,
419+ hriObject :: Object,
420+ hriDetail :: Text,
421+ hriParityBlocks :: Maybe Int,
422+ hriDataBlocks :: Maybe Int,
423+ hriDiskCount :: Int,
424+ hriSetCount :: Int,
425+ hriObjectSize :: Int,
426+ hriBefore :: [DriveInfo],
427+ hriAfter :: [DriveInfo]
428+ }
429+ deriving stock (Show, Eq)
430+
431+instance FromJSON HealResultItem where
432+ parseJSON = withObject "HealResultItem" $ \v ->
433+ HealResultItem
434+ <$> v
435+ .: "resultId"
436+ <*> v
437+ .: "type"
438+ <*> v
439+ .: "bucket"
440+ <*> v
441+ .: "object"
442+ <*> v
443+ .: "detail"
444+ <*> v
445+ .:? "parityBlocks"
446+ <*> v
447+ .:? "dataBlocks"
448+ <*> v
449+ .: "diskCount"
450+ <*> v
451+ .: "setCount"
452+ <*> v
453+ .: "objectSize"
454+ <*> ( do
455+ before <- v .: "before"
456+ before .: "drives"
457+ )
458+ <*> ( do
459+ after <- v .: "after"
460+ after .: "drives"
461+ )
462+
463+data HealStatus = HealStatus
464+ { hsSummary :: Text,
465+ hsStartTime :: UTCTime,
466+ hsSettings :: HealOpts,
467+ hsNumDisks :: Int,
468+ hsFailureDetail :: Maybe Text,
469+ hsItems :: Maybe [HealResultItem]
470+ }
471+ deriving stock (Show, Eq)
472+
473+instance FromJSON HealStatus where
474+ parseJSON = withObject "HealStatus" $ \v ->
475+ HealStatus
476+ <$> v
477+ .: "Summary"
478+ <*> v
479+ .: "StartTime"
480+ <*> v
481+ .: "Settings"
482+ <*> v
483+ .: "NumDisks"
484+ <*> v
485+ .:? "Detail"
486+ <*> v
487+ .: "Items"
488+
489+healPath :: Maybe Bucket -> Maybe Text -> ByteString
490+healPath bucket prefix = do
491+ if isJust bucket
492+ then
493+ encodeUtf8 $
494+ "v1/heal/"
495+ <> fromMaybe "" bucket
496+ <> "/"
497+ <> fromMaybe "" prefix
498+ else encodeUtf8 ("v1/heal/" :: Text)
499+
500+-- | Get server version and uptime.
501+serviceStatus :: Minio ServiceStatus
502+serviceStatus = do
503+ rsp <-
504+ executeAdminRequest
505+ AdminReqInfo
506+ { ariMethod = HT.methodGet,
507+ ariPayload = PayloadBS B.empty,
508+ ariPayloadHash = Nothing,
509+ ariPath = "v1/service",
510+ ariHeaders = [],
511+ ariQueryParams = []
512+ }
513+
514+ let rspBS = NC.responseBody rsp
515+ case eitherDecode rspBS of
516+ Right ss -> return ss
517+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
518+
519+-- | Send service restart or stop action to MinIO server.
520+serviceSendAction :: ServiceAction -> Minio ()
521+serviceSendAction action = do
522+ let payload = PayloadBS $ LBS.toStrict $ A.encode action
523+ void $
524+ executeAdminRequest
525+ AdminReqInfo
526+ { ariMethod = HT.methodPost,
527+ ariPayload = payload,
528+ ariPayloadHash = Nothing,
529+ ariPath = "v1/service",
530+ ariHeaders = [],
531+ ariQueryParams = []
532+ }
533+
534+-- | Get the current config file from server.
535+getConfig :: Minio ByteString
536+getConfig = do
537+ rsp <-
538+ executeAdminRequest
539+ AdminReqInfo
540+ { ariMethod = HT.methodGet,
541+ ariPayload = PayloadBS B.empty,
542+ ariPayloadHash = Nothing,
543+ ariPath = "v1/config",
544+ ariHeaders = [],
545+ ariQueryParams = []
546+ }
547+ return $ LBS.toStrict $ NC.responseBody rsp
548+
549+-- | Set a new config to the server.
550+setConfig :: ByteString -> Minio SetConfigResult
551+setConfig config = do
552+ rsp <-
553+ executeAdminRequest
554+ AdminReqInfo
555+ { ariMethod = HT.methodPut,
556+ ariPayload = PayloadBS config,
557+ ariPayloadHash = Nothing,
558+ ariPath = "v1/config",
559+ ariHeaders = [],
560+ ariQueryParams = []
561+ }
562+
563+ let rspBS = NC.responseBody rsp
564+ case eitherDecode rspBS of
565+ Right scr -> return scr
566+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
567+
568+-- | Get the progress of currently running heal task, this API should be
569+-- invoked right after `startHeal`. `token` is obtained after `startHeal`
570+-- which should be used to get the heal status.
571+getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
572+getHealStatus bucket prefix token = do
573+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
574+ let qparams = HT.queryTextToQuery [("clientToken", Just token)]
575+ rsp <-
576+ executeAdminRequest
577+ AdminReqInfo
578+ { ariMethod = HT.methodPost,
579+ ariPayload = PayloadBS B.empty,
580+ ariPayloadHash = Nothing,
581+ ariPath = healPath bucket prefix,
582+ ariHeaders = [],
583+ ariQueryParams = qparams
584+ }
585+ let rspBS = NC.responseBody rsp
586+ case eitherDecode rspBS of
587+ Right hs -> return hs
588+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
589+
590+doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
591+doHeal bucket prefix opts forceStart = do
592+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
593+ let payload = PayloadBS $ LBS.toStrict $ A.encode opts
594+ let qparams =
595+ bool
596+ []
597+ (HT.queryTextToQuery [("forceStart", Just "true")])
598+ forceStart
599+
600+ rsp <-
601+ executeAdminRequest
602+ AdminReqInfo
603+ { ariMethod = HT.methodPost,
604+ ariPayload = payload,
605+ ariPayloadHash = Nothing,
606+ ariPath = healPath bucket prefix,
607+ ariHeaders = [],
608+ ariQueryParams = qparams
609+ }
610+
611+ let rspBS = NC.responseBody rsp
612+ case eitherDecode rspBS of
613+ Right hsr -> return hsr
614+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
615+
616+-- | Start a heal sequence that scans data under given (possible empty)
617+-- `bucket` and `prefix`. The `recursive` bool turns on recursive
618+-- traversal under the given path. `dryRun` does not mutate on-disk data,
619+-- but performs data validation. Two heal sequences on overlapping paths
620+-- may not be initiated. The progress of a heal should be followed using
621+-- the `HealStatus` API. The server accumulates results of the heal
622+-- traversal and waits for the client to receive and acknowledge
623+-- them using the status API
624+startHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
625+startHeal bucket prefix opts = doHeal bucket prefix opts False
626+
627+-- | Similar to start a heal sequence, but force start a new heal sequence
628+-- even if an active heal is under progress.
629+forceStartHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
630+forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
631+
632+-- | Fetches information for all cluster nodes, such as server
633+-- properties, storage information, network statistics, etc.
634+getServerInfo :: Minio [ServerInfo]
635+getServerInfo = do
636+ rsp <-
637+ executeAdminRequest
638+ AdminReqInfo
639+ { ariMethod = HT.methodGet,
640+ ariPayload = PayloadBS B.empty,
641+ ariPayloadHash = Nothing,
642+ ariPath = "v1/info",
643+ ariHeaders = [],
644+ ariQueryParams = []
645+ }
646+ let rspBS = NC.responseBody rsp
647+ case eitherDecode rspBS of
648+ Right si -> return si
649+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
650+
651+executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
652+executeAdminRequest ari = do
653+ req <- buildAdminRequest ari
654+ mgr <- asks mcConnManager
655+ httpLbs req mgr
656+
657+buildAdminRequest :: AdminReqInfo -> Minio NC.Request
658+buildAdminRequest areq = do
659+ ci <- asks mcConnInfo
660+ sha256Hash <-
661+ if connectIsSecure ci
662+ then -- if secure connection
663+ return "UNSIGNED-PAYLOAD"
664+ else -- otherwise compute sha256
665+ getPayloadSHA256Hash (ariPayload areq)
666+
667+ timeStamp <- liftIO getCurrentTime
668+
669+ mgr <- asks mcConnManager
670+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671+
672+ let hostHeader = (hHost, getHostAddr ci)
673+ newAreq =
674+ areq
675+ { ariPayloadHash = Just sha256Hash,
676+ ariHeaders =
677+ hostHeader
678+ : sha256Header sha256Hash
679+ : ariHeaders areq
680+ }
681+ signReq = toRequest ci newAreq
682+ sp =
683+ SignParams
684+ (coerce $ cvAccessKey cv)
685+ (coerce $ cvSecretKey cv)
686+ (coerce $ cvSessionToken cv)
687+ ServiceS3
688+ timeStamp
689+ Nothing
690+ Nothing
691+ (ariPayloadHash newAreq)
692+ signHeaders = signV4 sp signReq
693+
694+ -- Update signReq with Authorization header containing v4 signature
695+ return
696+ signReq
697+ { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
698+ }
699+ where
700+ toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
701+ toRequest ci aReq =
702+ NC.defaultRequest
703+ { NC.method = ariMethod aReq,
704+ NC.secure = connectIsSecure ci,
705+ NC.host = encodeUtf8 $ connectHost ci,
706+ NC.port = connectPort ci,
707+ NC.path = B.intercalate "/" [adminPath, ariPath aReq],
708+ NC.requestHeaders = ariHeaders aReq,
709+ NC.queryString = HT.renderQuery False $ ariQueryParams aReq,
710+ NC.requestBody = getRequestBody (ariPayload aReq)
711+ }
src/Network/Minio/CopyObject.hsnew file+106
@@ -0,0 +1,106 @@
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+module Network.Minio.CopyObject where
18+
19+import qualified Data.List as List
20+import Lib.Prelude
21+import Network.Minio.Data
22+import Network.Minio.Errors
23+import Network.Minio.S3API
24+import Network.Minio.Utils
25+
26+-- | Copy an object using single or multipart copy strategy.
27+copyObjectInternal ::
28+ Bucket ->
29+ Object ->
30+ SourceInfo ->
31+ Minio ETag
32+copyObjectInternal b' o srcInfo = do
33+ let sBucket = srcBucket srcInfo
34+ sObject = srcObject srcInfo
35+
36+ -- get source object size with a head request
37+ oi <- headObject sBucket sObject []
38+ let srcSize = oiSize oi
39+
40+ -- check that byte offsets are valid if specified in cps
41+ let rangeMay = srcRange srcInfo
42+ range = maybe (0, srcSize) identity rangeMay
43+ startOffset = fst range
44+ endOffset = snd range
45+
46+ when
47+ ( isJust rangeMay
48+ && ( (startOffset < 0)
49+ || (endOffset < startOffset)
50+ || (endOffset >= srcSize)
51+ )
52+ )
53+ $ throwIO
54+ $ MErrVInvalidSrcObjByteRange range
55+
56+ -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
57+ -- 2. If startOffset /= 0 use multipart copy
58+ let destSize =
59+ (\(a, b) -> b - a + 1) $
60+ maybe (0, srcSize - 1) identity rangeMay
61+
62+ if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
63+ then multiPartCopyObject b' o srcInfo srcSize
64+ else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} []
65+
66+-- | Given the input byte range of the source object, compute the
67+-- splits for a multipart copy object procedure. Minimum part size
68+-- used is minPartSize.
69+selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
70+selectCopyRanges (st, end) =
71+ zip pns $
72+ zipWith (\x y -> (st + x, st + x + y - 1)) startOffsets partSizes
73+ where
74+ size = end - st + 1
75+ (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
76+
77+-- | Perform a multipart copy object action. Since we cannot verify
78+-- existing parts based on the source object, there is no resuming
79+-- copy action support.
80+multiPartCopyObject ::
81+ Bucket ->
82+ Object ->
83+ SourceInfo ->
84+ Int64 ->
85+ Minio ETag
86+multiPartCopyObject b o cps srcSize = do
87+ uid <- newMultipartUpload b o []
88+
89+ let byteRange = maybe (0, srcSize - 1) identity $ srcRange cps
90+ partRanges = selectCopyRanges byteRange
91+ partSources =
92+ map
93+ (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end)}))
94+ partRanges
95+ dstInfo = defaultDestinationInfo {dstBucket = b, dstObject = o}
96+
97+ copiedParts <-
98+ limitedMapConcurrently
99+ 10
100+ ( \(pn, cps') -> do
101+ (etag, _) <- copyObjectPart dstInfo cps' uid pn []
102+ return (pn, etag)
103+ )
104+ partSources
105+
106+ completeMultipartUpload b o uid copiedParts
src/Network/Minio/Credentials.hsnew file+77
@@ -0,0 +1,77 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.Credentials
18+ ( CredentialValue (..),
19+ credentialValueText,
20+ STSCredentialProvider (..),
21+ AccessKey (..),
22+ SecretKey (..),
23+ SessionToken (..),
24+ ExpiryTime (..),
25+ STSCredentialStore,
26+ initSTSCredential,
27+ getSTSCredential,
28+ Creds (..),
29+ getCredential,
30+ Endpoint,
31+
32+ -- * STS Assume Role
33+ defaultSTSAssumeRoleOptions,
34+ STSAssumeRole (..),
35+ STSAssumeRoleOptions (..),
36+ )
37+where
38+
39+import Data.Time (diffUTCTime, getCurrentTime)
40+import qualified Network.HTTP.Client as NC
41+import Network.Minio.Credentials.AssumeRole
42+import Network.Minio.Credentials.Types
43+import qualified UnliftIO.MVar as M
44+
45+data STSCredentialStore = STSCredentialStore
46+ { cachedCredentials :: M.MVar (CredentialValue, ExpiryTime),
47+ refreshAction :: Endpoint -> NC.Manager -> IO (CredentialValue, ExpiryTime)
48+ }
49+
50+initSTSCredential :: (STSCredentialProvider p) => p -> IO STSCredentialStore
51+initSTSCredential p = do
52+ let action = retrieveSTSCredentials p
53+ -- start with dummy credential, so that refresh happens for first request.
54+ now <- getCurrentTime
55+ mvar <- M.newMVar (CredentialValue mempty mempty mempty, coerce now)
56+ return $
57+ STSCredentialStore
58+ { cachedCredentials = mvar,
59+ refreshAction = action
60+ }
61+
62+getSTSCredential :: STSCredentialStore -> Endpoint -> NC.Manager -> IO (CredentialValue, Bool)
63+getSTSCredential store ep mgr = M.modifyMVar (cachedCredentials store) $ \cc@(v, expiry) -> do
64+ now <- getCurrentTime
65+ if diffUTCTime now (coerce expiry) > 0
66+ then do
67+ res <- refreshAction store ep mgr
68+ return (res, (fst res, True))
69+ else return (cc, (v, False))
70+
71+data Creds
72+ = CredsStatic CredentialValue
73+ | CredsSTS STSCredentialStore
74+
75+getCredential :: Creds -> Endpoint -> NC.Manager -> IO CredentialValue
76+getCredential (CredsStatic v) _ _ = return v
77+getCredential (CredsSTS s) ep mgr = fst <$> getSTSCredential s ep mgr
src/Network/Minio/Credentials/AssumeRole.hsnew file+266
@@ -0,0 +1,266 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.Credentials.AssumeRole where
18+
19+import qualified Data.ByteArray as BA
20+import qualified Data.ByteString.Lazy as LB
21+import qualified Data.Text as T
22+import qualified Data.Time as Time
23+import Data.Time.Units (Second)
24+import Lib.Prelude (UTCTime, throwIO)
25+import Network.HTTP.Client (RequestBody (RequestBodyBS))
26+import qualified Network.HTTP.Client as NC
27+import Network.HTTP.Types (hContentType, methodPost, renderSimpleQuery)
28+import Network.HTTP.Types.Header (hHost)
29+import Network.Minio.Credentials.Types
30+import Network.Minio.Data.Crypto (hashSHA256)
31+import Network.Minio.Errors (MErrV (..))
32+import Network.Minio.Sign.V4
33+import Network.Minio.Utils (getHostHeader, httpLbs)
34+import Network.Minio.XmlCommon
35+import Text.XML.Cursor hiding (bool)
36+
37+stsVersion :: ByteString
38+stsVersion = "2011-06-15"
39+
40+defaultDurationSeconds :: Second
41+defaultDurationSeconds = 3600
42+
43+-- | Assume Role API argument.
44+--
45+-- @since 1.7.0
46+data STSAssumeRole = STSAssumeRole
47+ { -- | Credentials to use in the AssumeRole STS API.
48+ sarCredentials :: CredentialValue,
49+ -- | Optional settings.
50+ sarOptions :: STSAssumeRoleOptions
51+ }
52+
53+-- | Options for STS Assume Role API.
54+data STSAssumeRoleOptions = STSAssumeRoleOptions
55+ { -- | STS endpoint to which the request will be made. For MinIO, this is the
56+ -- same as the server endpoint. For AWS, this has to be the Security Token
57+ -- Service endpoint. If using with 'setSTSCredential', this option can be
58+ -- left as 'Nothing' and the endpoint in 'ConnectInfo' will be used.
59+ saroEndpoint :: Maybe Text,
60+ -- | Desired validity for the generated credentials.
61+ saroDurationSeconds :: Maybe Second,
62+ -- | IAM policy to apply for the generated credentials.
63+ saroPolicyJSON :: Maybe ByteString,
64+ -- | Location is usually required for AWS.
65+ saroLocation :: Maybe Text,
66+ saroRoleARN :: Maybe Text,
67+ saroRoleSessionName :: Maybe Text
68+ }
69+
70+-- | Default STS Assume Role options - all options are Nothing, except for
71+-- duration which is set to 1 hour.
72+defaultSTSAssumeRoleOptions :: STSAssumeRoleOptions
73+defaultSTSAssumeRoleOptions =
74+ STSAssumeRoleOptions
75+ { saroEndpoint = Nothing,
76+ saroDurationSeconds = Just 3600,
77+ saroPolicyJSON = Nothing,
78+ saroLocation = Nothing,
79+ saroRoleARN = Nothing,
80+ saroRoleSessionName = Nothing
81+ }
82+
83+data AssumeRoleCredentials = AssumeRoleCredentials
84+ { arcCredentials :: CredentialValue,
85+ arcExpiration :: UTCTime
86+ }
87+ deriving stock (Show, Eq)
88+
89+data AssumeRoleResult = AssumeRoleResult
90+ { arrSourceIdentity :: Text,
91+ arrAssumedRoleArn :: Text,
92+ arrAssumedRoleId :: Text,
93+ arrRoleCredentials :: AssumeRoleCredentials
94+ }
95+ deriving stock (Show, Eq)
96+
97+-- | parseSTSAssumeRoleResult parses an XML response of the following form:
98+--
99+-- <AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
100+-- <AssumeRoleResult>
101+-- <SourceIdentity>Alice</SourceIdentity>
102+-- <AssumedRoleUser>
103+-- <Arn>arn:aws:sts::123456789012:assumed-role/demo/TestAR</Arn>
104+-- <AssumedRoleId>ARO123EXAMPLE123:TestAR</AssumedRoleId>
105+-- </AssumedRoleUser>
106+-- <Credentials>
107+-- <AccessKeyId>ASIAIOSFODNN7EXAMPLE</AccessKeyId>
108+-- <SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
109+-- <SessionToken>
110+-- AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
111+-- LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
112+-- QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
113+-- 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
114+-- +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
115+-- </SessionToken>
116+-- <Expiration>2019-11-09T13:34:41Z</Expiration>
117+-- </Credentials>
118+-- <PackedPolicySize>6</PackedPolicySize>
119+-- </AssumeRoleResult>
120+-- <ResponseMetadata>
121+-- <RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
122+-- </ResponseMetadata>
123+-- </AssumeRoleResponse>
124+parseSTSAssumeRoleResult :: (MonadIO m) => ByteString -> Text -> m AssumeRoleResult
125+parseSTSAssumeRoleResult xmldata namespace = do
126+ r <- parseRoot $ LB.fromStrict xmldata
127+ let s3Elem' = s3Elem namespace
128+ sourceIdentity =
129+ T.concat $
130+ r
131+ $/ s3Elem' "AssumeRoleResult"
132+ &/ s3Elem' "SourceIdentity"
133+ &/ content
134+ roleArn =
135+ T.concat $
136+ r
137+ $/ s3Elem' "AssumeRoleResult"
138+ &/ s3Elem' "AssumedRoleUser"
139+ &/ s3Elem' "Arn"
140+ &/ content
141+ roleId =
142+ T.concat $
143+ r
144+ $/ s3Elem' "AssumeRoleResult"
145+ &/ s3Elem' "AssumedRoleUser"
146+ &/ s3Elem' "AssumedRoleId"
147+ &/ content
148+
149+ convSB :: Text -> BA.ScrubbedBytes
150+ convSB = BA.convert . (encodeUtf8 :: Text -> ByteString)
151+
152+ credsInfo = do
153+ cr <-
154+ maybe (Left $ MErrVXmlParse "No Credentials Element found") Right $
155+ listToMaybe $
156+ r $/ s3Elem' "AssumeRoleResult" &/ s3Elem' "Credentials"
157+ let cur = fromNode $ node cr
158+ return
159+ ( CredentialValue
160+ { cvAccessKey =
161+ coerce $
162+ T.concat $
163+ cur $/ s3Elem' "AccessKeyId" &/ content,
164+ cvSecretKey =
165+ coerce $
166+ convSB $
167+ T.concat $
168+ cur
169+ $/ s3Elem' "SecretAccessKey"
170+ &/ content,
171+ cvSessionToken =
172+ Just $
173+ coerce $
174+ convSB $
175+ T.concat $
176+ cur
177+ $/ s3Elem' "SessionToken"
178+ &/ content
179+ },
180+ T.concat $ cur $/ s3Elem' "Expiration" &/ content
181+ )
182+ creds <- either throwIO pure credsInfo
183+ expiry <- parseS3XMLTime $ snd creds
184+ let roleCredentials =
185+ AssumeRoleCredentials
186+ { arcCredentials = fst creds,
187+ arcExpiration = expiry
188+ }
189+ return
190+ AssumeRoleResult
191+ { arrSourceIdentity = sourceIdentity,
192+ arrAssumedRoleArn = roleArn,
193+ arrAssumedRoleId = roleId,
194+ arrRoleCredentials = roleCredentials
195+ }
196+
197+instance STSCredentialProvider STSAssumeRole where
198+ getSTSEndpoint = saroEndpoint . sarOptions
199+ retrieveSTSCredentials sar (host', port', isSecure') mgr = do
200+ -- Assemble STS request
201+ let requiredParams =
202+ [ ("Action", "AssumeRole"),
203+ ("Version", stsVersion)
204+ ]
205+ opts = sarOptions sar
206+
207+ durSecs :: Int =
208+ fromIntegral $
209+ fromMaybe defaultDurationSeconds $
210+ saroDurationSeconds opts
211+ otherParams =
212+ [ ("RoleArn",) . encodeUtf8 <$> saroRoleARN opts,
213+ ("RoleSessionName",) . encodeUtf8 <$> saroRoleSessionName opts,
214+ Just ("DurationSeconds", show durSecs),
215+ ("Policy",) <$> saroPolicyJSON opts
216+ ]
217+ parameters = requiredParams ++ catMaybes otherParams
218+ (host, port, isSecure) =
219+ case getSTSEndpoint sar of
220+ Just ep ->
221+ let endPt = NC.parseRequest_ $ toString ep
222+ in (NC.host endPt, NC.port endPt, NC.secure endPt)
223+ Nothing -> (host', port', isSecure')
224+ reqBody = renderSimpleQuery False parameters
225+ req =
226+ NC.defaultRequest
227+ { NC.host = host,
228+ NC.port = port,
229+ NC.secure = isSecure,
230+ NC.method = methodPost,
231+ NC.requestHeaders =
232+ [ (hHost, getHostHeader (host, port)),
233+ (hContentType, "application/x-www-form-urlencoded")
234+ ],
235+ NC.requestBody = RequestBodyBS reqBody
236+ }
237+
238+ -- Sign the STS request.
239+ timeStamp <- liftIO Time.getCurrentTime
240+ let sp =
241+ SignParams
242+ { spAccessKey = coerce $ cvAccessKey $ sarCredentials sar,
243+ spSecretKey = coerce $ cvSecretKey $ sarCredentials sar,
244+ spSessionToken = coerce $ cvSessionToken $ sarCredentials sar,
245+ spService = ServiceSTS,
246+ spTimeStamp = timeStamp,
247+ spRegion = saroLocation opts,
248+ spExpirySecs = Nothing,
249+ spPayloadHash = Just $ hashSHA256 reqBody
250+ }
251+ signHeaders = signV4 sp req
252+ signedReq =
253+ req
254+ { NC.requestHeaders = NC.requestHeaders req ++ signHeaders
255+ }
256+
257+ -- Make the STS request
258+ resp <- httpLbs signedReq mgr
259+ result <-
260+ parseSTSAssumeRoleResult
261+ (toStrict $ NC.responseBody resp)
262+ "https://sts.amazonaws.com/doc/2011-06-15/"
263+ return
264+ ( arcCredentials $ arrRoleCredentials result,
265+ coerce $ arcExpiration $ arrRoleCredentials result
266+ )
src/Network/Minio/Credentials/Types.hsnew file+90
@@ -0,0 +1,90 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17+{-# LANGUAGE StrictData #-}
18+
19+module Network.Minio.Credentials.Types where
20+
21+import qualified Data.ByteArray as BA
22+import Lib.Prelude (UTCTime)
23+import qualified Network.HTTP.Client as NC
24+
25+-- | Access Key type.
26+newtype AccessKey = AccessKey {unAccessKey :: Text}
27+ deriving stock (Show)
28+ deriving newtype (Eq, IsString, Semigroup, Monoid)
29+
30+-- | Secret Key type - has a show instance that does not print the value.
31+newtype SecretKey = SecretKey {unSecretKey :: BA.ScrubbedBytes}
32+ deriving stock (Show)
33+ deriving newtype (Eq, IsString, Semigroup, Monoid)
34+
35+-- | Session Token type - has a show instance that does not print the value.
36+newtype SessionToken = SessionToken {unSessionToken :: BA.ScrubbedBytes}
37+ deriving stock (Show)
38+ deriving newtype (Eq, IsString, Semigroup, Monoid)
39+
40+-- | Object storage credential data type. It has support for the optional
41+-- [SessionToken](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html)
42+-- for using temporary credentials requested via STS.
43+--
44+-- The show instance for this type does not print the value of secrets for
45+-- security.
46+--
47+-- @since 1.7.0
48+data CredentialValue = CredentialValue
49+ { cvAccessKey :: AccessKey,
50+ cvSecretKey :: SecretKey,
51+ cvSessionToken :: Maybe SessionToken
52+ }
53+ deriving stock (Eq, Show)
54+
55+scrubbedToText :: BA.ScrubbedBytes -> Text
56+scrubbedToText =
57+ let b2t :: ByteString -> Text
58+ b2t = decodeUtf8
59+ s2b :: BA.ScrubbedBytes -> ByteString
60+ s2b = BA.convert
61+ in b2t . s2b
62+
63+-- | Convert a 'CredentialValue' to a text tuple. Use this to output the
64+-- credential to files or other programs.
65+credentialValueText :: CredentialValue -> (Text, Text, Maybe Text)
66+credentialValueText cv =
67+ ( coerce $ cvAccessKey cv,
68+ (scrubbedToText . coerce) $ cvSecretKey cv,
69+ scrubbedToText . coerce <$> cvSessionToken cv
70+ )
71+
72+-- | Endpoint represented by host, port and TLS enabled flag.
73+type Endpoint = (ByteString, Int, Bool)
74+
75+-- | Typeclass for STS credential providers.
76+--
77+-- @since 1.7.0
78+class STSCredentialProvider p where
79+ retrieveSTSCredentials ::
80+ p ->
81+ -- | STS Endpoint (host, port, isSecure)
82+ Endpoint ->
83+ NC.Manager ->
84+ IO (CredentialValue, ExpiryTime)
85+ getSTSEndpoint :: p -> Maybe Text
86+
87+-- | 'ExpiryTime' represents a time at which a credential expires.
88+newtype ExpiryTime = ExpiryTime {unExpiryTime :: UTCTime}
89+ deriving stock (Show)
90+ deriving newtype (Eq)
src/Network/Minio/Data.hs+1038−330
@@ -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,141 +13,276 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16+{-# LANGUAGE CPP #-}
17+{-# LANGUAGE GADTs #-}
18+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
19+{-# LANGUAGE StrictData #-}
20+{-# LANGUAGE TypeFamilies #-}
1621
17−{-# LANGUAGE GeneralizedNewtypeDeriving, TypeFamilies #-}
1822module Network.Minio.Data where
1923
20−import Control.Monad.Base
21−import qualified Control.Monad.Catch as MC
22−import Control.Monad.Trans.Control
23−import Control.Monad.Trans.Resource
24−
24+import qualified Conduit as C
25+import qualified Control.Concurrent.MVar as M
26+import Control.Monad.Trans.Except (throwE)
27+import Control.Monad.Trans.Resource
28+ ( MonadResource,
29+ MonadThrow (..),
30+ MonadUnliftIO,
31+ ResourceT,
32+ runResourceT,
33+ )
34+import qualified Data.Aeson as A
35+import qualified Data.ByteArray as BA
2536import qualified Data.ByteString as B
26−import Data.Default (Default(..))
27−import qualified Data.Map as Map
37+import qualified Data.ByteString.Lazy as LB
38+import Data.Default.Class (def)
39+import qualified Data.HashMap.Strict as H
40+import qualified Data.Ini as Ini
41+import qualified Data.List as List
2842import qualified Data.Text as T
29−import Data.Time (formatTime, defaultTimeLocale)
30−import Network.HTTP.Client (defaultManagerSettings)
43+import qualified Data.Text.Encoding as TE
44+import Data.Time (defaultTimeLocale, formatTime)
45+import Lib.Prelude (UTCTime, throwIO)
46+import qualified Network.Connection as Conn
47+import Network.HTTP.Client (defaultManagerSettings)
48+import qualified Network.HTTP.Client.TLS as TLS
3149import qualified Network.HTTP.Conduit as NC
32−import Network.HTTP.Types (Method, Header, Query)
50+import Network.HTTP.Types
51+ ( ByteRange,
52+ Header,
53+ Method,
54+ Query,
55+ hRange,
56+ )
3357import qualified Network.HTTP.Types as HT
34−import Network.Minio.Errors
35−import Text.XML
58+import Network.Minio.Credentials
59+import Network.Minio.Data.Crypto
60+ ( encodeToBase64,
61+ hashMD5ToBase64,
62+ )
63+import Network.Minio.Data.Time (UrlExpiry)
64+import Network.Minio.Errors
65+ ( MErrV (MErrVInvalidEncryptionKeyLength, MErrVMissingCredentials),
66+ MinioErr (..),
67+ )
68+import Network.Minio.Utils
69+import System.Directory (doesFileExist, getHomeDirectory)
70+import qualified System.Environment as Env
71+import System.FilePath.Posix (combine)
72+import qualified UnliftIO as U
73+import qualified UnliftIO.MVar as UM
74+
75+-- | max obj size is 5TiB
76+maxObjectSize :: Int64
77+maxObjectSize = 5 * 1024 * 1024 * oneMiB
3678
37−import GHC.Show (Show(..))
79+-- | minimum size of parts used in multipart operations.
80+minPartSize :: Int64
81+minPartSize = 64 * oneMiB
3882
39−import Lib.Prelude
83+oneMiB :: Int64
84+oneMiB = 1024 * 1024
4085
86+-- | maximum number of parts that can be uploaded for a single object.
87+maxMultipartParts :: Int64
88+maxMultipartParts = 10000
4189
4290-- TODO: Add a type which provides typed constants for region. this
4391-- type should have a IsString instance to infer the appropriate
4492-- constant.
93+
4594-- | awsRegionMap - library constant
46−awsRegionMap :: Map.Map Text Text
47−awsRegionMap = Map.fromList [
48− ("us-east-1", "s3.amazonaws.com")
49− , ("us-east-2", "s3-us-east-2.amazonaws.com")
50− , ("us-west-1", "s3-us-west-1.amazonaws.com")
51− , ("us-east-2", "s3-us-west-2.amazonaws.com")
52− , ("ca-central-1", "s3-ca-central-1.amazonaws.com")
53− , ("ap-south-1", "s3-ap-south-1.amazonaws.com")
54− , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
55− , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
56− , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
57− , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
58− , ("eu-west-1", "s3-eu-west-1.amazonaws.com")
59− , ("eu-west-2", "s3-eu-west-2.amazonaws.com")
60− , ("eu-central-1", "s3-eu-central-1.amazonaws.com")
61− , ("sa-east-1", "s3-sa-east-1.amazonaws.com")
62− ]
63−
64−-- | Connection Info data type. To create a 'ConnectInfo' value, use one
65−-- of the provided smart constructors or override fields of the
66−-- Default instance.
67−data ConnectInfo = ConnectInfo {
68− connectHost :: Text
69− , connectPort :: Int
70− , connectAccessKey :: Text
71− , connectSecretKey :: Text
72− , connectIsSecure :: Bool
73− , connectRegion :: Region
74− , connectAutoDiscoverRegion :: Bool
75− } deriving (Eq, Show)
76−
77−-- | Connects to a Minio server located at @localhost:9000@ with access
78−-- key /minio/ and secret key /minio123/. It is over __HTTP__ by
79−-- default.
80−instance Default ConnectInfo where
81− def = ConnectInfo "localhost" 9000 "minio" "minio123" False "us-east-1" True
82−
83−-- | Default AWS ConnectInfo. Connects to "us-east-1". Credentials
84−-- should be supplied before use, for e.g.:
95+awsRegionMap :: H.HashMap Text Text
96+awsRegionMap =
97+ H.fromList
98+ [ ("us-east-1", "s3.us-east-1.amazonaws.com"),
99+ ("us-east-2", "s3.us-east-2.amazonaws.com"),
100+ ("us-west-1", "s3.us-west-1.amazonaws.com"),
101+ ("us-west-2", "s3.us-west-2.amazonaws.com"),
102+ ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
103+ ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
104+ ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
105+ ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
106+ ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
107+ ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
108+ ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
109+ ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
110+ ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
111+ ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
112+ ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
113+ ("eu-west-3", "s3.eu-west-3.amazonaws.com"),
114+ ("eu-central-1", "s3.eu-central-1.amazonaws.com"),
115+ ("eu-central-2", "s3.eu-central-2.amazonaws.com"),
116+ ("eu-south-1", "s3.eu-south-1.amazonaws.com"),
117+ ("eu-south-2", "s3.eu-south-2.amazonaws.com"),
118+ ("af-south-1", "s3.af-south-1.amazonaws.com"),
119+ ("ap-east-1", "s3.ap-east-1.amazonaws.com"),
120+ ("cn-north-1", "s3.cn-north-1.amazonaws.com.cn"),
121+ ("cn-northwest-1", "s3.cn-northwest-1.amazonaws.com.cn"),
122+ ("eu-north-1", "s3.eu-north-1.amazonaws.com"),
123+ ("me-south-1", "s3.me-south-1.amazonaws.com"),
124+ ("me-central-1", "s3.me-central-1.amazonaws.com"),
125+ ("us-gov-east-1", "s3.us-gov-east-1.amazonaws.com"),
126+ ("us-gov-west-1", "s3.us-gov-west-1.amazonaws.com"),
127+ ("sa-east-1", "s3.sa-east-1.amazonaws.com")
128+ ]
129+
130+-- | Connection Info data type. To create a 'ConnectInfo' value,
131+-- enable the @OverloadedStrings@ language extension and use the
132+-- `IsString` instance to provide a URL, for example:
85133--
86−-- > awsCI {
87−-- > connectAccessKey = "my-access-key"
88−-- > , connectSecretKey = "my-secret-key"
89−-- > }
90−awsCI :: ConnectInfo
91−awsCI = def {
92− connectHost = "s3.amazonaws.com"
93− , connectPort = 443
94− , connectAccessKey = ""
95− , connectSecretKey = ""
96− , connectIsSecure = True
134+-- > let c :: ConnectInfo = "https://play.min.io"
135+data ConnectInfo = ConnectInfo
136+ { connectHost :: Text,
137+ connectPort :: Int,
138+ connectCreds :: Creds,
139+ connectIsSecure :: Bool,
140+ connectRegion :: Region,
141+ connectAutoDiscoverRegion :: Bool,
142+ connectDisableTLSCertValidation :: Bool
97143 }
98144
99−-- | AWS ConnectInfo with a specified region. It can optionally
100−-- disable the automatic discovery of a bucket's region via the
101−-- Boolean argument.
102−--
103−-- > awsWithRegionCI "us-west-1" False {
104−-- > connectAccessKey = "my-access-key"
105−-- > , connectSecretKey = "my-secret-key"
106−-- > }
145+getEndpoint :: ConnectInfo -> Endpoint
146+getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
147+
148+instance IsString ConnectInfo where
149+ fromString str =
150+ let req = NC.parseRequest_ str
151+ in ConnectInfo
152+ { connectHost = TE.decodeUtf8 $ NC.host req,
153+ connectPort = NC.port req,
154+ connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
155+ connectIsSecure = NC.secure req,
156+ connectRegion = "",
157+ connectAutoDiscoverRegion = True,
158+ connectDisableTLSCertValidation = False
159+ }
160+
161+-- | A 'CredentialLoader' is an action that may return a 'CredentialValue'.
162+-- Loaders may be chained together using 'findFirst'.
107163--
108−-- This restricts all operations to the "us-west-1" region and does
109−-- not perform any bucket location requests.
110−awsWithRegionCI :: Region -> Bool -> ConnectInfo
111−awsWithRegionCI region autoDiscoverRegion =
112− let host = maybe "s3.amazonaws.com" identity $
113− Map.lookup region awsRegionMap
114− in awsCI {
115− connectHost = host
116− , connectRegion = region
117− , connectAutoDiscoverRegion = autoDiscoverRegion
164+-- @since 1.7.0
165+type CredentialLoader = IO (Maybe CredentialValue)
166+
167+-- | Combines the given list of loaders, by calling each one in
168+-- order until a 'CredentialValue' is returned.
169+findFirst :: [CredentialLoader] -> IO (Maybe CredentialValue)
170+findFirst [] = return Nothing
171+findFirst (f : fs) = do
172+ c <- f
173+ maybe (findFirst fs) (return . Just) c
174+
175+-- | This action returns a 'CredentialValue' populated from
176+-- @~\/.aws\/credentials@
177+fromAWSConfigFile :: CredentialLoader
178+fromAWSConfigFile = do
179+ credsE <- runExceptT $ do
180+ homeDir <- lift getHomeDirectory
181+ let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
182+ fileExists <- lift $ doesFileExist awsCredsFile
183+ bool (throwE "FileNotFound") (return ()) fileExists
184+ ini <- ExceptT $ Ini.readIniFile awsCredsFile
185+ akey <-
186+ ExceptT $
187+ return $
188+ Ini.lookupValue "default" "aws_access_key_id" ini
189+ skey <-
190+ ExceptT $
191+ return $
192+ Ini.lookupValue "default" "aws_secret_access_key" ini
193+ return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
194+ return $ either (const Nothing) Just credsE
195+
196+-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
197+-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
198+fromAWSEnv :: CredentialLoader
199+fromAWSEnv = runMaybeT $ do
200+ akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
201+ skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
202+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
203+
204+-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
205+-- and @MINIO_SECRET_KEY@ environment variables.
206+fromMinioEnv :: CredentialLoader
207+fromMinioEnv = runMaybeT $ do
208+ akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
209+ skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
210+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
211+
212+-- | setCredsFrom retrieves access credentials from the first action in the
213+-- given list that succeeds and sets it in the 'ConnectInfo'.
214+setCredsFrom :: [CredentialLoader] -> ConnectInfo -> IO ConnectInfo
215+setCredsFrom ps ci = do
216+ pMay <- findFirst ps
217+ maybe
218+ (throwIO MErrVMissingCredentials)
219+ (return . (`setCreds` ci))
220+ pMay
221+
222+-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
223+setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
224+setCreds cv connInfo =
225+ connInfo
226+ { connectCreds = CredsStatic cv
118227 }
119228
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}
120236
121−-- | <https://play.minio.io:9000 Minio Play Server>
122−-- ConnectInfo. Credentials are already filled in.
123−minioPlayCI :: ConnectInfo
124−minioPlayCI = def {
125− connectHost = "play.minio.io"
126− , connectPort = 9000
127− , connectAccessKey = "Q3AM3UQ867SPQQA43P2F"
128− , connectSecretKey = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
129− , connectIsSecure = True
130− , connectAutoDiscoverRegion = False
131− }
237+-- | Set the S3 region parameter in the `ConnectInfo`
238+setRegion :: Region -> ConnectInfo -> ConnectInfo
239+setRegion r connInfo =
240+ connInfo
241+ { connectRegion = r,
242+ connectAutoDiscoverRegion = False
243+ }
132244
133−-- | ConnectInfo for Minio server. Takes hostname, port and a Boolean
134−-- to enable TLS.
135−--
136−-- > minioCI "minio.example.com" 9000 True {
137−-- > connectAccessKey = "my-access-key"
138−-- > , connectSecretKey = "my-secret-key"
139−-- > }
245+-- | Check if the connection to object storage server is secure
246+-- (i.e. uses TLS)
247+isConnectInfoSecure :: ConnectInfo -> Bool
248+isConnectInfoSecure = connectIsSecure
249+
250+-- | Disable TLS certificate validation completely! This makes TLS
251+-- insecure! Use only for testing with self-signed or temporary
252+-- certificates. Note that this option has no effect, if you provide
253+-- your own Manager in `mkMinioConn`.
254+disableTLSCertValidation :: ConnectInfo -> ConnectInfo
255+disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
256+
257+getHostAddr :: ConnectInfo -> ByteString
258+getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
259+
260+-- | Default Google Compute Storage ConnectInfo. Works only for
261+-- "Simple Migration" use-case with interoperability mode enabled on
262+-- GCP console. For more information -
263+-- https://cloud.google.com/storage/docs/migrating
140264--
141−-- This connects to a Minio server at the given hostname and port over
142−-- HTTPS.
143−minioCI :: Text -> Int -> Bool -> ConnectInfo
144−minioCI host port isSecure = def {
145− connectHost = host
146− , connectPort = port
147− , connectRegion = "us-east-1"
148− , connectIsSecure = isSecure
149− , connectAutoDiscoverRegion = False
150− }
265+-- Credentials should be supplied before use.
266+gcsCI :: ConnectInfo
267+gcsCI =
268+ setRegion
269+ "us"
270+ "https://storage.googleapis.com"
271+
272+-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
273+-- should be supplied before use.
274+awsCI :: ConnectInfo
275+awsCI = "https://s3.amazonaws.com"
276+
277+-- | <https://play.min.io MinIO Play Server>
278+-- ConnectInfo. Credentials are already filled in.
279+minioPlayCI :: ConnectInfo
280+minioPlayCI =
281+ let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
282+ in setCreds playCreds $
283+ setRegion
284+ "us-east-1"
285+ "https://play.min.io"
151286
152287-- |
153288-- Represents a bucket in the object store
@@ -157,25 +292,156 @@ type Bucket = Text
157292-- Represents an object name
158293type Object = Text
159294
160−-- |
161−-- Represents a region
162−-- TODO: This could be a Sum Type with all defined regions for AWS.
295+-- | Represents a region
163296type Region = Text
164297
165−-- | A type alias to represent an Entity-Tag returned by S3-compatible
166−-- APIs.
298+-- | A type alias to represent an Entity-Tag returned by S3-compatible APIs.
167299type ETag = Text
168300
301+-- | Data type to represent an object encryption key. Create one using
302+-- the `mkSSECKey` function.
303+newtype SSECKey = SSECKey BA.ScrubbedBytes
304+ deriving stock (Eq, Show)
305+
306+-- | Validates that the given ByteString is 32 bytes long and creates
307+-- an encryption key.
308+mkSSECKey :: (MonadThrow m) => ByteString -> m SSECKey
309+mkSSECKey keyBytes
310+ | B.length keyBytes /= 32 =
311+ throwM MErrVInvalidEncryptionKeyLength
312+ | otherwise =
313+ return $ SSECKey $ BA.convert keyBytes
314+
315+-- | Data type to represent Server-Side-Encryption settings
316+data SSE where
317+ -- | Specifies SSE S3 encryption - server manages encryption keys
318+ SSE :: SSE
319+ -- | Specifies that KMS service should be used. The first argument
320+ -- to the constructor is the Key Id to be used by the server (if
321+ -- not specified, the default KMS key id is used). The second
322+ -- argument is the optional KMS context that must have a
323+ -- `A.ToJSON` instance - please refer to the AWS S3 documentation
324+ -- for detailed information.
325+ SSEKMS :: (A.ToJSON a) => Maybe ByteString -> Maybe a -> SSE
326+ -- | Specifies server-side encryption with customer provided
327+ -- key. The argument is the encryption key to be used.
328+ SSEC :: SSECKey -> SSE
329+
330+toPutObjectHeaders :: SSE -> [HT.Header]
331+toPutObjectHeaders sseArg =
332+ let sseHeader = "x-amz-server-side-encryption"
333+ sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
334+ sseKmsContextHeader = sseHeader <> "-context"
335+ ssecAlgo = sseHeader <> "-customer-algorithm"
336+ ssecKey = sseHeader <> "-customer-key"
337+ ssecKeyMD5 = ssecKey <> "-MD5"
338+ in case sseArg of
339+ SSE -> [(sseHeader, "AES256")]
340+ SSEKMS keyIdMay ctxMay ->
341+ [(sseHeader, "aws:kms")]
342+ ++ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay
343+ ++ maybe [] (\k -> [(sseKmsContextHeader, LB.toStrict $ A.encode k)]) ctxMay
344+ SSEC (SSECKey sb) ->
345+ [ (ssecAlgo, "AES256"),
346+ (ssecKey, encodeToBase64 sb),
347+ (ssecKeyMD5, hashMD5ToBase64 sb)
348+ ]
349+
350+-- | Data type for options in PutObject call. Start with the empty
351+-- `defaultPutObjectOptions` and use various the various poo*
352+-- accessors.
353+data PutObjectOptions = PutObjectOptions
354+ { -- | Set a standard MIME type describing the format of the object.
355+ pooContentType :: Maybe Text,
356+ -- | Set what content encodings have been applied to the object and thus
357+ -- what decoding mechanisms must be applied to obtain the media-type
358+ -- referenced by the Content-Type header field.
359+ pooContentEncoding :: Maybe Text,
360+ -- | Set presentational information for the object.
361+ pooContentDisposition :: Maybe Text,
362+ -- | Set to specify caching behavior for the object along the
363+ -- request/reply chain.
364+ pooCacheControl :: Maybe Text,
365+ -- | Set to describe the language(s) intended for the audience.
366+ pooContentLanguage :: Maybe Text,
367+ -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
368+ -- performance needs, storage class is @STANDARD@ by default (i.e
369+ -- when Nothing is passed).
370+ pooStorageClass :: Maybe Text,
371+ -- | Set user defined metadata to store with the object.
372+ pooUserMetadata :: [(Text, Text)],
373+ -- | Set number of worker threads used to upload an object.
374+ pooNumThreads :: Maybe Word,
375+ -- | Set object encryption parameters for the request.
376+ pooSSE :: Maybe SSE
377+ }
378+
379+-- | Provide default `PutObjectOptions`.
380+defaultPutObjectOptions :: PutObjectOptions
381+defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
382+
383+pooToHeaders :: PutObjectOptions -> [HT.Header]
384+pooToHeaders poo =
385+ userMetadata
386+ ++ mapMaybe tupToMaybe (zip names values)
387+ ++ maybe [] toPutObjectHeaders (pooSSE poo)
388+ where
389+ tupToMaybe (k, Just v) = Just (k, v)
390+ tupToMaybe (_, Nothing) = Nothing
391+ userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
392+ names =
393+ [ "content-type",
394+ "content-encoding",
395+ "content-disposition",
396+ "content-language",
397+ "cache-control",
398+ "x-amz-storage-class"
399+ ]
400+ values =
401+ map
402+ (fmap encodeUtf8 . (poo &))
403+ [ pooContentType,
404+ pooContentEncoding,
405+ pooContentDisposition,
406+ pooContentLanguage,
407+ pooCacheControl,
408+ pooStorageClass
409+ ]
410+
169411-- |
170412-- BucketInfo returned for list buckets call
171−data BucketInfo = BucketInfo {
172− biName :: Bucket
173− , biCreationDate :: UTCTime
174− } deriving (Show, Eq)
413+data BucketInfo = BucketInfo
414+ { biName :: Bucket,
415+ biCreationDate :: UTCTime
416+ }
417+ deriving stock (Show, Eq)
175418
176419-- | A type alias to represent a part-number for multipart upload
177420type PartNumber = Int16
178421
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+
179445-- | A type alias to represent an upload-id for multipart upload
180446type UploadId = Text
181447
@@ -184,163 +450,264 @@ type PartTuple = (PartNumber, ETag)
184450
185451-- | Represents result from a listing of object parts of an ongoing
186452-- multipart upload.
187−data ListPartsResult = ListPartsResult {
188− lprHasMore :: Bool
189− , lprNextPart :: Maybe Int
190− , lprParts :: [ObjectPartInfo]
191− } deriving (Show, Eq)
192−
453+data ListPartsResult = ListPartsResult
454+ { lprHasMore :: Bool,
455+ lprNextPart :: Maybe Int,
456+ lprParts :: [ObjectPartInfo]
457+ }
458+ deriving stock (Show, Eq)
193459
194460-- | Represents information about an object part in an ongoing
195461-- multipart upload.
196−data ObjectPartInfo = ObjectPartInfo {
197− opiNumber :: PartNumber
198− , opiETag :: ETag
199− , opiSize :: Int64
200− , opiModTime :: UTCTime
201− } deriving (Show, Eq)
462+data ObjectPartInfo = ObjectPartInfo
463+ { opiNumber :: PartNumber,
464+ opiETag :: ETag,
465+ opiSize :: Int64,
466+ opiModTime :: UTCTime
467+ }
468+ deriving stock (Show, Eq)
202469
203470-- | Represents result from a listing of incomplete uploads to a
204471-- bucket.
205−data ListUploadsResult = ListUploadsResult {
206− lurHasMore :: Bool
207− , lurNextKey :: Maybe Text
208− , lurNextUpload :: Maybe Text
209− , lurUploads :: [(Object, UploadId, UTCTime)]
210− , lurCPrefixes :: [Text]
211− } deriving (Show, Eq)
472+data ListUploadsResult = ListUploadsResult
473+ { lurHasMore :: Bool,
474+ lurNextKey :: Maybe Text,
475+ lurNextUpload :: Maybe Text,
476+ lurUploads :: [(Object, UploadId, UTCTime)],
477+ lurCPrefixes :: [Text]
478+ }
479+ deriving stock (Show, Eq)
212480
213481-- | Represents information about a multipart upload.
214−data UploadInfo = UploadInfo {
215− uiKey :: Object
216− , uiUploadId :: UploadId
217− , uiInitTime :: UTCTime
218− , uiSize :: Int64
219− } deriving (Show, Eq)
482+data UploadInfo = UploadInfo
483+ { uiKey :: Object,
484+ uiUploadId :: UploadId,
485+ uiInitTime :: UTCTime,
486+ uiSize :: Int64
487+ }
488+ deriving stock (Show, Eq)
220489
221490-- | Represents result from a listing of objects in a bucket.
222−data ListObjectsResult = ListObjectsResult {
223− lorHasMore :: Bool
224− , lorNextToken :: Maybe Text
225− , lorObjects :: [ObjectInfo]
226− , lorCPrefixes :: [Text]
227− } deriving (Show, Eq)
491+data ListObjectsResult = ListObjectsResult
492+ { lorHasMore :: Bool,
493+ lorNextToken :: Maybe Text,
494+ lorObjects :: [ObjectInfo],
495+ lorCPrefixes :: [Text]
496+ }
497+ deriving stock (Show, Eq)
228498
229499-- | Represents result from a listing of objects version 1 in a bucket.
230−data ListObjectsV1Result = ListObjectsV1Result {
231− lorHasMore' :: Bool
232− , lorNextMarker :: Maybe Text
233− , lorObjects' :: [ObjectInfo]
234− , lorCPrefixes' :: [Text]
235− } deriving (Show, Eq)
500+data ListObjectsV1Result = ListObjectsV1Result
501+ { lorHasMore' :: Bool,
502+ lorNextMarker :: Maybe Text,
503+ lorObjects' :: [ObjectInfo],
504+ lorCPrefixes' :: [Text]
505+ }
506+ deriving stock (Show, Eq)
236507
237508-- | Represents information about an object.
238−data ObjectInfo = ObjectInfo {
239− oiObject :: Object
240− , oiModTime :: UTCTime
241− , oiETag :: ETag
242− , oiSize :: Int64
243− } deriving (Show, Eq)
244−
245−data CopyPartSource = CopyPartSource {
246− -- | formatted like "\/sourceBucket\/sourceObject"
247− cpSource :: Text
248− -- | (0, 9) means first ten bytes of the source object
249− , cpSourceRange :: Maybe (Int64, Int64)
250− , cpSourceIfMatch :: Maybe Text
251− , cpSourceIfNoneMatch :: Maybe Text
252− , cpSourceIfUnmodifiedSince :: Maybe UTCTime
253− , cpSourceIfModifiedSince :: Maybe UTCTime
254− } deriving (Show, Eq)
255−
256−instance Default CopyPartSource where
257− def = CopyPartSource "" def def def def def
258−
259−cpsToHeaders :: CopyPartSource -> [HT.Header]
260−cpsToHeaders cps = ("x-amz-copy-source", encodeUtf8 $ cpSource cps) :
261− rangeHdr ++ zip names values
262− where
263− names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",
264− "x-amz-copy-source-if-unmodified-since",
265− "x-amz-copy-source-if-modified-since"]
266− values = mapMaybe (fmap encodeUtf8 . (cps &))
267− [cpSourceIfMatch, cpSourceIfNoneMatch,
268− fmap formatRFC1123 . cpSourceIfUnmodifiedSince,
269− fmap formatRFC1123 . cpSourceIfModifiedSince]
270− rangeHdr = ("x-amz-copy-source-range",)
271− . HT.renderByteRanges
272− . (:[])
273− . uncurry HT.ByteRangeFromTo
274− <$> map (both fromIntegral) (maybeToList $ cpSourceRange cps)
275−
276−-- | Extract the source bucket and source object name. TODO: validate
277−-- the bucket and object name extracted.
278−cpsToObject :: CopyPartSource -> Maybe (Bucket, Object)
279−cpsToObject cps = do
280− [_, bucket, object] <- Just splits
281− return (bucket, object)
509+data ObjectInfo = ObjectInfo
510+ { -- | Object key
511+ oiObject :: Object,
512+ -- | Modification time of the object
513+ oiModTime :: UTCTime,
514+ -- | ETag of the object
515+ oiETag :: ETag,
516+ -- | Size of the object in bytes
517+ oiSize :: Int64,
518+ -- | A map of user-metadata
519+ -- pairs stored with an
520+ -- object (keys will not
521+ -- have the @X-Amz-Meta-@
522+ -- prefix).
523+ oiUserMetadata :: H.HashMap Text Text,
524+ -- | A map of metadata
525+ -- key-value pairs (not
526+ -- including the
527+ -- user-metadata pairs)
528+ oiMetadata :: H.HashMap Text Text
529+ }
530+ deriving stock (Show, Eq)
531+
532+-- | Represents source object in server-side copy object
533+data SourceInfo = SourceInfo
534+ { -- | Bucket containing the source object
535+ srcBucket :: Text,
536+ -- | Source object key
537+ srcObject :: Text,
538+ -- | Source object
539+ -- byte-range
540+ -- (inclusive)
541+ srcRange :: Maybe (Int64, Int64),
542+ -- | ETag condition on source -
543+ -- object is copied only if the
544+ -- source object's ETag matches
545+ -- this value.
546+ srcIfMatch :: Maybe Text,
547+ -- | ETag not match condition
548+ -- on source - object is copied
549+ -- if ETag does not match this
550+ -- value.
551+ srcIfNoneMatch :: Maybe Text,
552+ -- | Copy source object only
553+ -- if the source has been
554+ -- modified since this time.
555+ srcIfModifiedSince :: Maybe UTCTime,
556+ -- | Copy source object only
557+ -- if the source has been
558+ -- un-modified since this
559+ -- given time.
560+ srcIfUnmodifiedSince :: Maybe UTCTime
561+ }
562+ deriving stock (Show, Eq)
563+
564+-- | Provide a default for `SourceInfo`
565+defaultSourceInfo :: SourceInfo
566+defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing
567+
568+-- | Represents destination object in server-side copy object
569+data DestinationInfo = DestinationInfo
570+ { -- | Destination bucket
571+ dstBucket :: Text,
572+ -- | Destination object key
573+ dstObject :: Text
574+ }
575+ deriving stock (Show, Eq)
576+
577+-- | Provide a default for `DestinationInfo`
578+defaultDestinationInfo :: DestinationInfo
579+defaultDestinationInfo = DestinationInfo "" ""
580+
581+-- | Data type for options when getting an object from the
582+-- service. Start with the empty `defaultGetObjectOptions` and modify
583+-- it using the goo* functions.
584+data GetObjectOptions = GetObjectOptions
585+ { -- | Set object's data of given offset begin and end,
586+ -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
587+ gooRange :: Maybe ByteRange,
588+ -- | Set matching ETag condition, GetObject which matches the following
589+ -- ETag.
590+ gooIfMatch :: Maybe ETag,
591+ -- | Set matching ETag none condition, GetObject which does not match
592+ -- the following ETag.
593+ gooIfNoneMatch :: Maybe ETag,
594+ -- | Set object unmodified condition, GetObject unmodified since given time.
595+ gooIfUnmodifiedSince :: Maybe UTCTime,
596+ -- | Set object modified condition, GetObject modified since given time.
597+ gooIfModifiedSince :: Maybe UTCTime,
598+ -- | Specify SSE-C key
599+ gooSSECKey :: Maybe SSECKey
600+ }
601+
602+-- | Provide default `GetObjectOptions`.
603+defaultGetObjectOptions :: GetObjectOptions
604+defaultGetObjectOptions =
605+ GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
606+
607+gooToHeaders :: GetObjectOptions -> [HT.Header]
608+gooToHeaders goo =
609+ rangeHdr
610+ ++ zip names values
611+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
282612 where
283− splits = T.splitOn "/" $ cpSource cps
613+ names =
614+ [ "If-Match",
615+ "If-None-Match",
616+ "If-Unmodified-Since",
617+ "If-Modified-Since"
618+ ]
619+ values =
620+ mapMaybe
621+ (fmap encodeUtf8 . (goo &))
622+ [ gooIfMatch,
623+ gooIfNoneMatch,
624+ fmap formatRFC1123 . gooIfUnmodifiedSince,
625+ fmap formatRFC1123 . gooIfModifiedSince
626+ ]
627+ rangeHdr =
628+ maybe [] (\a -> [(hRange, HT.renderByteRanges [a])]) $
629+ gooRange goo
630+
631+-- | Data type returned by 'getObject' representing the object being
632+-- retrieved. Use the @gor*@ functions to access its contents.
633+data GetObjectResponse = GetObjectResponse
634+ { -- | ObjectInfo of the object being retrieved.
635+ gorObjectInfo :: ObjectInfo,
636+ -- | A conduit of the bytes of the object.
637+ gorObjectStream :: C.ConduitM () ByteString Minio ()
638+ }
284639
285640-- | A data-type for events that can occur in the object storage
286641-- server. Reference:
287642-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
288−data Event = ObjectCreated
289− | ObjectCreatedPut
290− | ObjectCreatedPost
291− | ObjectCreatedCopy
292− | ObjectCreatedMultipartUpload
293− | ObjectRemoved
294− | ObjectRemovedDelete
295− | ObjectRemovedDeleteMarkerCreated
296− | ReducedRedundancyLostObject
297− deriving (Eq)
298−
299−instance Show Event where
300− show ObjectCreated = "s3:ObjectCreated:*"
301− show ObjectCreatedPut = "s3:ObjectCreated:Put"
302− show ObjectCreatedPost = "s3:ObjectCreated:Post"
303− show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
304− show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
305− show ObjectRemoved = "s3:ObjectRemoved:*"
306− show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
307− show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
308− show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
643+data Event
644+ = ObjectCreated
645+ | ObjectCreatedPut
646+ | ObjectCreatedPost
647+ | ObjectCreatedCopy
648+ | ObjectCreatedMultipartUpload
649+ | ObjectRemoved
650+ | ObjectRemovedDelete
651+ | ObjectRemovedDeleteMarkerCreated
652+ | ReducedRedundancyLostObject
653+ deriving stock (Eq, Show)
654+
655+instance ToText Event where
656+ toText ObjectCreated = "s3:ObjectCreated:*"
657+ toText ObjectCreatedPut = "s3:ObjectCreated:Put"
658+ toText ObjectCreatedPost = "s3:ObjectCreated:Post"
659+ toText ObjectCreatedCopy = "s3:ObjectCreated:Copy"
660+ toText ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
661+ toText ObjectRemoved = "s3:ObjectRemoved:*"
662+ toText ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
663+ toText ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
664+ toText ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
309665
310666textToEvent :: Text -> Maybe Event
311667textToEvent t = case t of
312− "s3:ObjectCreated:*" -> Just ObjectCreated
313− "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
314− "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
315− "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
316− "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
317− "s3:ObjectRemoved:*" -> Just ObjectRemoved
318− "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
668+ "s3:ObjectCreated:*" -> Just ObjectCreated
669+ "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
670+ "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
671+ "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
672+ "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
673+ "s3:ObjectRemoved:*" -> Just ObjectRemoved
674+ "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
319675 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
320− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
321− _ -> Nothing
322−
676+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
677+ _ -> Nothing
323678
324−data Filter = Filter
679+-- | Filter data type - part of notification configuration
680+newtype Filter = Filter
325681 { fFilter :: FilterKey
326− } deriving (Show, Eq)
682+ }
683+ deriving stock (Show, Eq)
327684
328−instance Default Filter where
329− def = Filter def
685+-- | defaultFilter is empty, used to create a notification
686+-- configuration.
687+defaultFilter :: Filter
688+defaultFilter = Filter defaultFilterKey
330689
331−data FilterKey = FilterKey
690+-- | FilterKey contains FilterRules, and is part of a Filter.
691+newtype FilterKey = FilterKey
332692 { fkKey :: FilterRules
333− } deriving (Show, Eq)
693+ }
694+ deriving stock (Show, Eq)
334695
335−instance Default FilterKey where
336− def = FilterKey def
696+-- | defaultFilterKey is empty, used to create notification
697+-- configuration.
698+defaultFilterKey :: FilterKey
699+defaultFilterKey = FilterKey defaultFilterRules
337700
338−data FilterRules = FilterRules
701+-- | FilterRules represents a collection of `FilterRule`s.
702+newtype FilterRules = FilterRules
339703 { frFilterRules :: [FilterRule]
340− } deriving (Show, Eq)
704+ }
705+ deriving stock (Show, Eq)
341706
342−instance Default FilterRules where
343− def = FilterRules []
707+-- | defaultFilterRules is empty, used to create notification
708+-- configuration.
709+defaultFilterRules :: FilterRules
710+defaultFilterRules = FilterRules []
344711
345712-- | A filter rule that can act based on the suffix or prefix of an
346713-- object. As an example, let's create two filter rules:
@@ -348,25 +715,28 @@ instance Default FilterRules where
348715-- > let suffixRule = FilterRule "suffix" ".jpg"
349716-- > let prefixRule = FilterRule "prefix" "images/"
350717--
351−-- The `suffixRule` restricts the notification to be triggered only
352−-- for objects having a suffix of ".jpg", and the `prefixRule`
718+-- The @suffixRule@ restricts the notification to be triggered only
719+-- for objects having a suffix of ".jpg", and the @prefixRule@
353720-- restricts it to objects having a prefix of "images/".
354721data FilterRule = FilterRule
355− { frName :: Text
356− , frValue :: Text
357− } deriving (Show, Eq)
722+ { frName :: Text,
723+ frValue :: Text
724+ }
725+ deriving stock (Show, Eq)
358726
727+-- | Arn is an alias of Text
359728type Arn = Text
360729
361730-- | A data-type representing the configuration for a particular
362731-- notification system. It could represent a Queue, Topic or Lambda
363732-- Function configuration.
364733data NotificationConfig = NotificationConfig
365− { ncId :: Text
366− , ncArn :: Arn
367− , ncEvents :: [Event]
368− , ncFilter :: Filter
369− } deriving (Show, Eq)
734+ { ncId :: Text,
735+ ncArn :: Arn,
736+ ncEvents :: [Event],
737+ ncFilter :: Filter
738+ }
739+ deriving stock (Show, Eq)
370740
371741-- | A data-type to represent bucket notification configuration. It is
372742-- a collection of queue, topic or lambda function configurations. The
@@ -374,110 +744,448 @@ data NotificationConfig = NotificationConfig
374744-- described at
375745-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
376746data Notification = Notification
377− { nQueueConfigurations :: [NotificationConfig]
378− , nTopicConfigurations :: [NotificationConfig]
379− , nCloudFunctionConfigurations :: [NotificationConfig]
380− } deriving (Eq, Show)
747+ { nQueueConfigurations :: [NotificationConfig],
748+ nTopicConfigurations :: [NotificationConfig],
749+ nCloudFunctionConfigurations :: [NotificationConfig]
750+ }
751+ deriving stock (Show, Eq)
752+
753+-- | The default notification configuration is empty.
754+defaultNotification :: Notification
755+defaultNotification = Notification [] [] []
756+
757+--------------------------------------------------------------------------
758+-- Select API Related Types
759+--------------------------------------------------------------------------
760+
761+-- | SelectRequest represents the Select API call. Use the
762+-- `selectRequest` function to create a value of this type.
763+data SelectRequest = SelectRequest
764+ { srExpression :: Text,
765+ srExpressionType :: ExpressionType,
766+ srInputSerialization :: InputSerialization,
767+ srOutputSerialization :: OutputSerialization,
768+ srRequestProgressEnabled :: Maybe Bool
769+ }
770+ deriving stock (Show, Eq)
771+
772+data ExpressionType = SQL
773+ deriving stock (Show, Eq)
774+
775+-- | InputSerialization represents format information of the input
776+-- object being queried. Use one of the smart constructors such as
777+-- `defaultCsvInput` as a starting value, and add compression info
778+-- using `setInputCompressionType`
779+data InputSerialization = InputSerialization
780+ { isCompressionType :: Maybe CompressionType,
781+ isFormatInfo :: InputFormatInfo
782+ }
783+ deriving stock (Show, Eq)
784+
785+-- | Data type representing the compression setting in a Select
786+-- request.
787+data CompressionType
788+ = CompressionTypeNone
789+ | CompressionTypeGzip
790+ | CompressionTypeBzip2
791+ deriving stock (Show, Eq)
792+
793+-- | Data type representing input object format information in a
794+-- Select request.
795+data InputFormatInfo
796+ = InputFormatCSV CSVInputProp
797+ | InputFormatJSON JSONInputProp
798+ | InputFormatParquet
799+ deriving stock (Show, Eq)
800+
801+-- | defaultCsvInput returns InputSerialization with default CSV
802+-- format, and without any compression setting.
803+defaultCsvInput :: InputSerialization
804+defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
805+
806+-- | linesJsonInput returns InputSerialization with JSON line based
807+-- format with no compression setting.
808+linesJsonInput :: InputSerialization
809+linesJsonInput =
810+ InputSerialization
811+ Nothing
812+ (InputFormatJSON $ JSONInputProp JSONTypeLines)
813+
814+-- | documentJsonInput returns InputSerialization with JSON document
815+-- based format with no compression setting.
816+documentJsonInput :: InputSerialization
817+documentJsonInput =
818+ InputSerialization
819+ Nothing
820+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)
821+
822+-- | defaultParquetInput returns InputSerialization with Parquet
823+-- format, and no compression setting.
824+defaultParquetInput :: InputSerialization
825+defaultParquetInput = InputSerialization Nothing InputFormatParquet
826+
827+-- | setInputCompressionType sets the compression type for the input
828+-- of the SelectRequest
829+setInputCompressionType ::
830+ CompressionType ->
831+ SelectRequest ->
832+ SelectRequest
833+setInputCompressionType c i =
834+ let is = srInputSerialization i
835+ is' = is {isCompressionType = Just c}
836+ in i {srInputSerialization = is'}
837+
838+-- | defaultCsvOutput returns OutputSerialization with default CSV
839+-- format.
840+defaultCsvOutput :: OutputSerialization
841+defaultCsvOutput = OutputSerializationCSV defaultCSVProp
842+
843+-- | defaultJsonInput returns OutputSerialization with default JSON
844+-- format.
845+defaultJsonOutput :: OutputSerialization
846+defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
847+
848+-- | selectRequest is used to build a `SelectRequest`
849+-- value. @selectRequest query inputSer outputSer@ represents a
850+-- SelectRequest with the SQL query text given by @query@, the input
851+-- serialization settings (compression format and format information)
852+-- @inputSer@ and the output serialization settings @outputSer@.
853+selectRequest ::
854+ Text ->
855+ InputSerialization ->
856+ OutputSerialization ->
857+ SelectRequest
858+selectRequest sqlQuery inputSer outputSer =
859+ SelectRequest
860+ { srExpression = sqlQuery,
861+ srExpressionType = SQL,
862+ srInputSerialization = inputSer,
863+ srOutputSerialization = outputSer,
864+ srRequestProgressEnabled = Nothing
865+ }
866+
867+-- | setRequestProgressEnabled sets the flag for turning on progress
868+-- messages when the Select response is being streamed back to the
869+-- client.
870+setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
871+setRequestProgressEnabled enabled sr =
872+ sr {srRequestProgressEnabled = Just enabled}
873+
874+type CSVInputProp = CSVProp
875+
876+-- | CSVProp represents CSV format properties. It is built up using
877+-- the Monoid instance.
878+newtype CSVProp = CSVProp (H.HashMap Text Text)
879+ deriving stock (Show, Eq)
880+
881+instance Semigroup CSVProp where
882+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
883+
884+instance Monoid CSVProp where
885+ mempty = CSVProp mempty
886+
887+csvPropsList :: CSVProp -> [(Text, Text)]
888+csvPropsList (CSVProp h) = sort $ H.toList h
889+
890+defaultCSVProp :: CSVProp
891+defaultCSVProp = mempty
892+
893+-- | Specify the CSV record delimiter property.
894+recordDelimiter :: Text -> CSVProp
895+recordDelimiter = CSVProp . H.singleton "RecordDelimiter"
896+
897+-- | Specify the CSV field delimiter property.
898+fieldDelimiter :: Text -> CSVProp
899+fieldDelimiter = CSVProp . H.singleton "FieldDelimiter"
900+
901+-- | Specify the CSV quote character property.
902+quoteCharacter :: Text -> CSVProp
903+quoteCharacter = CSVProp . H.singleton "QuoteCharacter"
904+
905+-- | Specify the CSV quote-escape character property.
906+quoteEscapeCharacter :: Text -> CSVProp
907+quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
908+
909+-- | FileHeaderInfo specifies information about column headers for CSV
910+-- format.
911+data FileHeaderInfo
912+ = -- | No column headers are present
913+ FileHeaderNone
914+ | -- | Headers are present and they should be used
915+ FileHeaderUse
916+ | -- | Header are present, but should be ignored
917+ FileHeaderIgnore
918+ deriving stock (Show, Eq)
919+
920+-- | Specify the CSV file header info property.
921+fileHeaderInfo :: FileHeaderInfo -> CSVProp
922+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toStr
923+ where
924+ toStr FileHeaderNone = "NONE"
925+ toStr FileHeaderUse = "USE"
926+ toStr FileHeaderIgnore = "IGNORE"
927+
928+-- | Specify the CSV comment character property. Lines starting with
929+-- this character are ignored by the server.
930+commentCharacter :: Text -> CSVProp
931+commentCharacter = CSVProp . H.singleton "Comments"
381932
382−instance Default Notification where
383− def = Notification [] [] []
933+-- | Allow quoted record delimiters inside a row using this property.
934+allowQuotedRecordDelimiter :: CSVProp
935+allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter" "TRUE"
936+
937+-- | Set the CSV format properties in the InputSerialization.
938+setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
939+setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
940+
941+-- | Set the CSV format properties in the OutputSerialization.
942+outputCSVFromProps :: CSVProp -> OutputSerialization
943+outputCSVFromProps = OutputSerializationCSV
944+
945+newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
946+ deriving stock (Show, Eq)
947+
948+data JSONType = JSONTypeDocument | JSONTypeLines
949+ deriving stock (Show, Eq)
950+
951+-- | OutputSerialization represents output serialization settings for
952+-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
953+-- a starting point.
954+data OutputSerialization
955+ = OutputSerializationJSON JSONOutputProp
956+ | OutputSerializationCSV CSVOutputProp
957+ deriving stock (Show, Eq)
958+
959+type CSVOutputProp = CSVProp
960+
961+-- | quoteFields is an output serialization parameter
962+quoteFields :: QuoteFields -> CSVProp
963+quoteFields q = CSVProp $
964+ H.singleton "QuoteFields" $
965+ case q of
966+ QuoteFieldsAsNeeded -> "ASNEEDED"
967+ QuoteFieldsAlways -> "ALWAYS"
968+
969+-- | Represent the QuoteField setting.
970+data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
971+ deriving stock (Show, Eq)
972+
973+newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
974+ deriving stock (Show, Eq)
975+
976+-- | Set the output record delimiter for JSON format
977+outputJSONFromRecordDelimiter :: Text -> OutputSerialization
978+outputJSONFromRecordDelimiter t =
979+ OutputSerializationJSON (JSONOutputProp $ Just t)
980+
981+-- Response related types
982+
983+-- | An EventMessage represents each kind of message received from the server.
984+data EventMessage
985+ = ProgressEventMessage Progress
986+ | StatsEventMessage Stats
987+ | RequestLevelErrorMessage
988+ Text
989+ -- ^ Error code
990+ Text
991+ -- ^ Error message
992+ | RecordPayloadEventMessage ByteString
993+ deriving stock (Show, Eq)
994+
995+data MsgHeaderName
996+ = MessageType
997+ | EventType
998+ | ContentType
999+ | ErrorCode
1000+ | ErrorMessage
1001+ deriving stock (Show, Eq)
1002+
1003+msgHeaderValueType :: Word8
1004+msgHeaderValueType = 7
1005+
1006+type MessageHeader = (MsgHeaderName, Text)
1007+
1008+-- | Represent the progress event returned in the Select response.
1009+data Progress = Progress
1010+ { pBytesScanned :: Int64,
1011+ pBytesProcessed :: Int64,
1012+ pBytesReturned :: Int64
1013+ }
1014+ deriving stock (Show, Eq)
1015+
1016+-- | Represent the stats event returned at the end of the Select
1017+-- response.
1018+type Stats = Progress
1019+
1020+--------------------------------------------------------------------------
1021+-- Select API Related Types End
1022+--------------------------------------------------------------------------
3841023
3851024-- | Represents different kinds of payload that are used with S3 API
3861025-- requests.
387−data Payload = PayloadBS ByteString
388− | PayloadH Handle
389− Int64 -- offset
390− Int64 -- size
391−
392−instance Default Payload where
393− def = PayloadBS ""
394−
395−data RequestInfo = RequestInfo {
396− riMethod :: Method
397− , riBucket :: Maybe Bucket
398− , riObject :: Maybe Object
399− , riQueryParams :: Query
400− , riHeaders :: [Header]
401− , riPayload :: Payload
402− , riPayloadHash :: Maybe ByteString
403− , riRegion :: Maybe Region
404− , riNeedsLocation :: Bool
1026+data Payload
1027+ = PayloadBS ByteString
1028+ | PayloadH Handle Int64 Int64 -- file handle, offset and length
1029+ | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
1030+
1031+defaultPayload :: Payload
1032+defaultPayload = PayloadBS ""
1033+
1034+data AdminReqInfo = AdminReqInfo
1035+ { ariMethod :: Method,
1036+ ariPayloadHash :: Maybe ByteString,
1037+ ariPayload :: Payload,
1038+ ariPath :: ByteString,
1039+ ariHeaders :: [Header],
1040+ ariQueryParams :: Query
4051041 }
4061042
407−instance Default RequestInfo where
408− def = RequestInfo HT.methodGet def def def def def Nothing def True
1043+data S3ReqInfo = S3ReqInfo
1044+ { riMethod :: Method,
1045+ riBucket :: Maybe Bucket,
1046+ riObject :: Maybe Object,
1047+ riQueryParams :: Query,
1048+ riHeaders :: [Header],
1049+ riPayload :: Payload,
1050+ riPayloadHash :: Maybe ByteString,
1051+ riRegion :: Maybe Region,
1052+ riNeedsLocation :: Bool,
1053+ riPresignExpirySecs :: Maybe UrlExpiry
1054+ }
4091055
410−getPathFromRI :: RequestInfo -> ByteString
411−getPathFromRI ri =
412− let
413− b = riBucket ri
414− o = riObject ri
415− segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
416− in
417− B.concat ["/", B.intercalate "/" segments]
1056+defaultS3ReqInfo :: S3ReqInfo
1057+defaultS3ReqInfo =
1058+ S3ReqInfo
1059+ HT.methodGet
1060+ Nothing
1061+ Nothing
1062+ []
1063+ []
1064+ defaultPayload
1065+ Nothing
1066+ Nothing
1067+ True
1068+ Nothing
4181069
419−-- | Time to expire for a presigned URL. It interpreted as a number of
420−-- seconds. The maximum duration that can be specified is 7 days.
421−type UrlExpiry = Int
1070+getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
1071+getS3Path b o =
1072+ let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
1073+ in B.concat ["/", B.intercalate "/" segments]
4221074
423−type RegionMap = Map.Map Bucket Region
1075+type RegionMap = H.HashMap Bucket Region
4241076
425−newtype Minio a = Minio {
426− unMinio :: ReaderT MinioConn (StateT RegionMap (ResourceT IO)) a
1077+-- | The Minio Monad - all computations accessing object storage
1078+-- happens in it.
1079+newtype Minio a = Minio
1080+ { unMinio :: ReaderT MinioConn (ResourceT IO) a
4271081 }
428− deriving (
429− Functor
430− , Applicative
431− , Monad
432− , MonadIO
433− , MonadReader MinioConn
434− , MonadState RegionMap
435− , MonadThrow
436− , MonadCatch
437− , MonadBase IO
438− , MonadResource
1082+ deriving newtype
1083+ ( Functor,
1084+ Applicative,
1085+ Monad,
1086+ MonadIO,
1087+ MonadReader MinioConn,
1088+ MonadResource,
1089+ MonadUnliftIO
4391090 )
4401091
441−instance MonadBaseControl IO Minio where
442− type StM Minio a = (a, RegionMap)
443− liftBaseWith f = Minio $ liftBaseWith $ \q -> f (q . unMinio)
444− restoreM = Minio . restoreM
445−
446−-- | MinioConn holds connection info and a connection pool
447−data MinioConn = MinioConn {
448− mcConnInfo :: ConnectInfo
449− , mcConnManager :: NC.Manager
1092+-- | MinioConn holds connection info and a connection pool to allow
1093+-- for efficient resource re-use.
1094+data MinioConn = MinioConn
1095+ { mcConnInfo :: ConnectInfo,
1096+ mcConnManager :: NC.Manager,
1097+ mcRegionMap :: MVar RegionMap
4501098 }
4511099
1100+class HasSvcNamespace env where
1101+ getSvcNamespace :: env -> Text
1102+
1103+instance HasSvcNamespace MinioConn where
1104+ getSvcNamespace env =
1105+ let host = connectHost $ mcConnInfo env
1106+ in ( if host == "storage.googleapis.com"
1107+ then "http://doc.s3.amazonaws.com/2006-03-01"
1108+ else "http://s3.amazonaws.com/doc/2006-03-01/"
1109+ )
1110+
4521111-- | Takes connection information and returns a connection object to
453−-- be passed to 'runMinio'
1112+-- be passed to 'runMinio'. The returned value can be kept in the
1113+-- application environment and passed to `runMinioWith` whenever
1114+-- object storage is accessed.
4541115connect :: ConnectInfo -> IO MinioConn
4551116connect ci = do
456− let settings = bool defaultManagerSettings NC.tlsManagerSettings $
457− connectIsSecure ci
1117+ let settings
1118+ | connectIsSecure ci && connectDisableTLSCertValidation ci =
1119+ let badTlsSettings =
1120+ case def of
1121+ tlsSettings@Conn.TLSSettingsSimple {} ->
1122+ tlsSettings {Conn.settingDisableCertificateValidation = True}
1123+ _ -> def
1124+ in TLS.mkManagerSettings badTlsSettings Nothing
1125+ | connectIsSecure ci = NC.tlsManagerSettings
1126+ | otherwise = defaultManagerSettings
4581127 mgr <- NC.newManager settings
459− return $ MinioConn ci mgr
1128+ mkMinioConn ci mgr
1129+
1130+-- | Run the computation accessing object storage using the given
1131+-- `MinioConn`. This reuses connections, but otherwise it is similar
1132+-- to `runMinio`.
1133+runMinioWith :: MinioConn -> Minio a -> IO (Either MinioErr a)
1134+runMinioWith conn m = runResourceT $ runMinioResWith conn m
1135+
1136+-- | Given `ConnectInfo` and a HTTP connection manager, create a
1137+-- `MinioConn`.
1138+mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
1139+mkMinioConn ci mgr = do
1140+ rMapMVar <- M.newMVar H.empty
1141+ return $ MinioConn ci mgr rMapMVar
4601142
4611143-- | Run the Minio action and return the result or an error.
4621144runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
4631145runMinio ci m = do
464− conn <- liftIO $ connect ci
465− runResourceT . flip evalStateT Map.empty . flip runReaderT conn . unMinio $
466− fmap Right m `MC.catches`
467− [ MC.Handler handlerServiceErr
468− , MC.Handler handlerHE
469− , MC.Handler handlerFE
470− , MC.Handler handlerValidation
471− ]
1146+ conn <- connect ci
1147+ runResourceT $ runMinioResWith conn m
1148+
1149+-- | Similar to 'runMinioWith'. Allows applications to allocate/release
1150+-- its resources along side MinIO's internal resources.
1151+runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)
1152+runMinioResWith conn m =
1153+ flip runReaderT conn . unMinio $
1154+ fmap Right m
1155+ `U.catches` [ U.Handler handlerServiceErr,
1156+ U.Handler handlerHE,
1157+ U.Handler handlerFE,
1158+ U.Handler handlerValidation
1159+ ]
4721160 where
4731161 handlerServiceErr = return . Left . MErrService
4741162 handlerHE = return . Left . MErrHTTP
4751163 handlerFE = return . Left . MErrIO
4761164 handlerValidation = return . Left . MErrValidation
4771165
478−s3Name :: Text -> Name
479−s3Name s = Name s (Just "http://s3.amazonaws.com/doc/2006-03-01/") Nothing
1166+-- | Similar to 'runMinio'. Allows applications to allocate/release
1167+-- its resources along side MinIO's internal resources.
1168+runMinioRes :: ConnectInfo -> Minio a -> ResourceT IO (Either MinioErr a)
1169+runMinioRes ci m = do
1170+ conn <- liftIO $ connect ci
1171+ runMinioResWith conn m
4801172
4811173-- | Format as per RFC 1123.
4821174formatRFC1123 :: UTCTime -> T.Text
4831175formatRFC1123 = 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+20−19
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,23 +13,21 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−
1716{-# LANGUAGE FlexibleInstances #-}
17+
1818module Network.Minio.Data.ByteString
19− (
20− stripBS
21− , UriEncodable(..)
22− ) where
19+ ( stripBS,
20+ UriEncodable (..),
21+ )
22+where
2323
2424import qualified Data.ByteString as B
2525import qualified Data.ByteString.Builder as BB
2626import qualified Data.ByteString.Char8 as BC8
2727import qualified Data.ByteString.Lazy as LB
28−import Data.Char (isSpace, toUpper, isAsciiUpper, isAsciiLower, isDigit)
28+import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace, toUpper)
2929import qualified Data.Text as T
30−import Numeric (showHex)
31−
32−import Lib.Prelude
30+import Numeric (showHex)
3331
3432stripBS :: ByteString -> ByteString
3533stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
@@ -39,8 +37,10 @@ class UriEncodable s where
3937
4038instance UriEncodable [Char] where
4139 uriEncode encodeSlash payload =
42− LB.toStrict $ BB.toLazyByteString $ mconcat $
43− map (`uriEncodeChar` encodeSlash) payload
40+ LB.toStrict $
41+ BB.toLazyByteString $
42+ mconcat $
43+ map (`uriEncodeChar` encodeSlash) payload
4444
4545instance UriEncodable ByteString where
4646 -- assumes that uriEncode is passed ASCII encoded strings.
@@ -59,16 +59,17 @@ uriEncodeChar '/' True = BB.byteString "%2F"
5959uriEncodeChar '/' False = BB.char7 '/'
6060uriEncodeChar ch _
6161 | isAsciiUpper ch
62− || isAsciiLower ch
63− || isDigit ch
64− || (ch == '_')
65− || (ch == '-')
66− || (ch == '.')
67− || (ch == '~') = BB.char7 ch
62+ || isAsciiLower ch
63+ || isDigit ch
64+ || (ch == '_')
65+ || (ch == '-')
66+ || (ch == '.')
67+ || (ch == '~') =
68+ BB.char7 ch
6869 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
6970 where
7071 f :: Word8 -> BB.Builder
7172 f n = BB.char7 '%' <> BB.string7 hexStr
7273 where
7374 hexStr = map toUpper $ showHex q $ showHex r ""
74− (q, r) = divMod (fromIntegral n) (16::Word8)
75+ (q, r) = divMod n (16 :: Word8)
src/Network/Minio/Data/Crypto.hs+39−28
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,50 +15,54 @@
1515--
1616
1717module Network.Minio.Data.Crypto
18− (
19− hashSHA256
20− , hashSHA256FromSource
18+ ( hashSHA256,
19+ hashSHA256FromSource,
20+ hashMD5,
21+ hashMD5ToBase64,
22+ hashMD5FromSource,
23+ hmacSHA256,
24+ hmacSHA256RawBS,
25+ digestToBS,
26+ digestToBase16,
27+ encodeToBase64,
28+ )
29+where
2130
22− , hashMD5
23− , hashMD5FromSource
24−
25− , hmacSHA256
26− , hmacSHA256RawBS
27− , digestToBS
28− , digestToBase16
29− ) where
30−
31−import Crypto.Hash (SHA256(..), MD5(..), hashWith, Digest)
32−import Crypto.Hash.Conduit (sinkHash)
33−import Crypto.MAC.HMAC (hmac, HMAC)
34−import Data.ByteArray (ByteArrayAccess, convert)
35−import Data.ByteArray.Encoding (convertToBase, Base(Base16))
31+import Crypto.Hash
32+ ( Digest,
33+ MD5 (..),
34+ SHA256 (..),
35+ hashWith,
36+ )
37+import Crypto.Hash.Conduit (sinkHash)
38+import Crypto.MAC.HMAC (HMAC, hmac)
39+import Data.ByteArray (ByteArrayAccess, convert)
40+import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
3641import qualified Data.Conduit as C
3742
38−import Lib.Prelude
39−
4043hashSHA256 :: ByteString -> ByteString
4144hashSHA256 = digestToBase16 . hashWith SHA256
4245
43−hashSHA256FromSource :: Monad m => C.Producer m ByteString -> m ByteString
46+hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
4447hashSHA256FromSource src = do
45− digest <- src C.$$ sinkSHA256Hash
48+ digest <- C.connect src sinkSHA256Hash
4649 return $ digestToBase16 digest
4750 where
4851 -- To help with type inference
49− sinkSHA256Hash :: Monad m => C.Consumer ByteString m (Digest SHA256)
52+ sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
5053 sinkSHA256Hash = sinkHash
5154
55+-- Returns MD5 hash hex encoded.
5256hashMD5 :: ByteString -> ByteString
5357hashMD5 = digestToBase16 . hashWith MD5
5458
55−hashMD5FromSource :: Monad m => C.Producer m ByteString -> m ByteString
59+hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
5660hashMD5FromSource src = do
57− digest <- src C.$$ sinkMD5Hash
61+ digest <- C.connect src sinkMD5Hash
5862 return $ digestToBase16 digest
5963 where
6064 -- To help with type inference
61− sinkMD5Hash :: Monad m => C.Consumer ByteString m (Digest MD5)
65+ sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
6266 sinkMD5Hash = sinkHash
6367
6468hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -67,8 +71,15 @@ hmacSHA256 message key = hmac key message
6771hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
6872hmacSHA256RawBS message key = convert $ hmacSHA256 message key
6973
70−digestToBS :: ByteArrayAccess a => a -> ByteString
74+digestToBS :: (ByteArrayAccess a) => a -> ByteString
7175digestToBS = convert
7276
73−digestToBase16 :: ByteArrayAccess a => a -> ByteString
77+digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
7478digestToBase16 = convertToBase Base16
79+
80+-- Returns MD5 hash base 64 encoded.
81+hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
82+hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
83+
84+encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
85+encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+18−14
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,20 +15,24 @@
1515--
1616
1717module Network.Minio.Data.Time
18− (
19− awsTimeFormat
20− , awsTimeFormatBS
21− , awsDateFormat
22− , awsDateFormatBS
23− , awsParseTime
24− , iso8601TimeFormat
25− ) where
26−
27−
28−import Data.ByteString.Char8 (pack)
18+ ( awsTimeFormat,
19+ awsTimeFormatBS,
20+ awsDateFormat,
21+ awsDateFormatBS,
22+ awsParseTime,
23+ iso8601TimeFormat,
24+ UrlExpiry,
25+ )
26+where
27+
28+import Data.ByteString.Char8 (pack)
2929import qualified Data.Time as Time
30+import Data.Time.Format.ISO8601 (iso8601Show)
31+import Lib.Prelude
3032
31−import Lib.Prelude
33+-- | Time to expire for a presigned URL. It interpreted as a number of
34+-- seconds. The maximum duration that can be specified is 7 days.
35+type UrlExpiry = Int
3236
3337awsTimeFormat :: UTCTime -> [Char]
3438awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
@@ -46,4 +50,4 @@ awsParseTime :: [Char] -> Maybe UTCTime
4650awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
4751
4852iso8601TimeFormat :: UTCTime -> [Char]
49−iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
53+iso8601TimeFormat = iso8601Show
src/Network/Minio/Errors.hs+59−45
@@ -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.
@@ -14,69 +14,83 @@
1414-- limitations under the License.
1515--
1616
17−module Network.Minio.Errors where
17+module Network.Minio.Errors
18+ ( MErrV (..),
19+ ServiceErr (..),
20+ MinioErr (..),
21+ toServiceErr,
22+ )
23+where
1824
19−import Control.Exception
25+import Control.Exception (IOException)
2026import qualified Network.HTTP.Conduit as NC
2127
22−import Lib.Prelude
23−
24−
2528---------------------------------
2629-- Errors
2730---------------------------------
31+
2832-- | Various validation errors
29−data MErrV = MErrVSinglePUTSizeExceeded Int64
30− | MErrVPutSizeExceeded Int64
31− | MErrVETagHeaderNotFound
32− | MErrVInvalidObjectInfoResponse
33− | MErrVInvalidSrcObjSpec Text
34− | MErrVInvalidSrcObjByteRange (Int64, Int64)
35− | MErrVCopyObjSingleNoRangeAccepted
36− | MErrVRegionNotSupported Text
37− | MErrVXmlParse Text
38− | MErrVInvalidBucketName Text
39− | MErrVInvalidObjectName Text
40− | MErrVInvalidUrlExpiry Int
41− deriving (Show, Eq)
33+data MErrV
34+ = MErrVSinglePUTSizeExceeded Int64
35+ | MErrVPutSizeExceeded Int64
36+ | MErrVETagHeaderNotFound
37+ | MErrVInvalidObjectInfoResponse
38+ | MErrVInvalidSrcObjSpec Text
39+ | MErrVInvalidSrcObjByteRange (Int64, Int64)
40+ | MErrVCopyObjSingleNoRangeAccepted
41+ | MErrVRegionNotSupported Text
42+ | MErrVXmlParse Text
43+ | MErrVInvalidBucketName Text
44+ | MErrVInvalidObjectName Text
45+ | MErrVInvalidUrlExpiry Int
46+ | MErrVJsonParse Text
47+ | MErrVInvalidHealPath
48+ | MErrVMissingCredentials
49+ | MErrVInvalidEncryptionKeyLength
50+ | MErrVStreamingBodyUnexpectedEOF
51+ | MErrVUnexpectedPayload
52+ | MErrVSTSEndpointNotFound
53+ deriving stock (Show, Eq)
4254
4355instance Exception MErrV
4456
4557-- | Errors returned by S3 compatible service
46−data ServiceErr = BucketAlreadyExists
47− | BucketAlreadyOwnedByYou
48− | NoSuchBucket
49− | InvalidBucketName
50− | NoSuchKey
51− | ServiceErr Text Text
52− deriving (Show, Eq)
58+data ServiceErr
59+ = BucketAlreadyExists
60+ | BucketAlreadyOwnedByYou
61+ | NoSuchBucket
62+ | InvalidBucketName
63+ | NoSuchKey
64+ | SelectErr Text Text
65+ | ServiceErr Text Text
66+ deriving stock (Show, Eq)
5367
5468instance Exception ServiceErr
5569
5670toServiceErr :: Text -> Text -> ServiceErr
57−toServiceErr "NoSuchKey" _ = NoSuchKey
58−toServiceErr "NoSuchBucket" _ = NoSuchBucket
59−toServiceErr "InvalidBucketName" _ = InvalidBucketName
71+toServiceErr "NoSuchKey" _ = NoSuchKey
72+toServiceErr "NoSuchBucket" _ = NoSuchBucket
73+toServiceErr "InvalidBucketName" _ = InvalidBucketName
6074toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
61−toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
62−toServiceErr code message = ServiceErr code message
63−
75+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
76+toServiceErr code message = ServiceErr code message
6477
6578-- | Errors thrown by the library
66−data MinioErr = MErrHTTP NC.HttpException
67− | MErrIO IOException
68− | MErrService ServiceErr
69− | MErrValidation MErrV
70− deriving (Show)
79+data MinioErr
80+ = MErrHTTP NC.HttpException
81+ | MErrIO IOException
82+ | MErrService ServiceErr
83+ | MErrValidation MErrV
84+ deriving stock (Show)
7185
7286instance Eq MinioErr where
73− MErrHTTP _ == MErrHTTP _ = True
74− MErrHTTP _ == _ = False
75− MErrIO _ == MErrIO _ = True
76− MErrIO _ == _ = False
77− MErrService a == MErrService b = a == b
78− MErrService _ == _ = False
79− MErrValidation a == MErrValidation b = a == b
80− MErrValidation _ == _ = False
87+ MErrHTTP _ == MErrHTTP _ = True
88+ MErrHTTP _ == _ = False
89+ MErrIO _ == MErrIO _ = True
90+ MErrIO _ == _ = False
91+ MErrService a == MErrService b = a == b
92+ MErrService _ == _ = False
93+ MErrValidation a == MErrValidation b = a == b
94+ MErrValidation _ == _ = False
8195
8296instance Exception MinioErr
src/Network/Minio/JsonParser.hsnew file+49
@@ -0,0 +1,49 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018 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.JsonParser
18+ ( parseErrResponseJSON,
19+ )
20+where
21+
22+import Data.Aeson
23+ ( FromJSON,
24+ eitherDecode,
25+ parseJSON,
26+ withObject,
27+ (.:),
28+ )
29+import qualified Data.Text as T
30+import Lib.Prelude
31+import Network.Minio.Errors
32+
33+data AdminErrJSON = AdminErrJSON
34+ { aeCode :: Text,
35+ aeMessage :: Text
36+ }
37+ deriving stock (Eq, Show)
38+
39+instance FromJSON AdminErrJSON where
40+ parseJSON = withObject "AdminErrJSON" $ \v ->
41+ AdminErrJSON
42+ <$> v .: "Code"
43+ <*> v .: "Message"
44+
45+parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
46+parseErrResponseJSON jsondata =
47+ case eitherDecode jsondata of
48+ Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
49+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs+120−38
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -19,80 +19,162 @@ 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 Network.Minio.Data
23+ ( Bucket,
24+ ListObjectsResult
25+ ( lorCPrefixes,
26+ lorHasMore,
27+ lorNextToken,
28+ lorObjects
29+ ),
30+ ListObjectsV1Result
31+ ( lorCPrefixes',
32+ lorHasMore',
33+ lorNextMarker,
34+ lorObjects'
35+ ),
36+ ListPartsResult (lprHasMore, lprNextPart, lprParts),
37+ ListUploadsResult
38+ ( lurHasMore,
39+ lurNextKey,
40+ lurNextUpload,
41+ lurUploads
42+ ),
43+ Minio,
44+ Object,
45+ ObjectInfo,
46+ ObjectPartInfo (opiSize),
47+ UploadId,
48+ UploadInfo (UploadInfo),
49+ )
50+import Network.Minio.S3API
51+ ( listIncompleteParts',
52+ listIncompleteUploads',
53+ listObjects',
54+ listObjectsV1',
55+ )
2256
23−import Lib.Prelude
57+-- | Represents a list output item - either an object or an object
58+-- prefix (i.e. a directory).
59+data ListItem
60+ = ListItemObject ObjectInfo
61+ | ListItemPrefix Text
62+ deriving stock (Show, Eq)
2463
25−import Network.Minio.Data
26−import Network.Minio.S3API
27−
28−-- | List objects in a bucket matching the given prefix. If recurse is
29−-- set to True objects matching prefix are recursively listed.
30−listObjects :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
64+-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
65+-- similar to a file system tree traversal.
66+--
67+-- If @prefix@ is not 'Nothing', only items with the given prefix are
68+-- listed, otherwise items under the bucket are returned.
69+--
70+-- If @recurse@ is set to @True@ all directories under the prefix are
71+-- recursively traversed and only objects are returned.
72+--
73+-- If @recurse@ is set to @False@, objects and directories immediately
74+-- under the given prefix are returned (no recursive traversal is
75+-- performed).
76+listObjects :: Bucket -> Maybe Text -> Bool -> C.ConduitM () ListItem Minio ()
3177listObjects bucket prefix recurse = loop Nothing
3278 where
33− loop :: Maybe Text -> C.Producer Minio ObjectInfo
79+ loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
3480 loop nextToken = do
35− let
36− delimiter = bool (Just "/") Nothing recurse
81+ let delimiter = bool (Just "/") Nothing recurse
3782
3883 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
39− CL.sourceList $ lorObjects res
84+ CL.sourceList $ map ListItemObject $ lorObjects res
85+ unless recurse $
86+ CL.sourceList $
87+ map ListItemPrefix $
88+ lorCPrefixes res
4089 when (lorHasMore res) $
4190 loop (lorNextToken res)
4291
43−-- | List objects in a bucket matching the given prefix. If recurse is
44−-- set to True objects matching prefix are recursively listed.
45−listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
92+-- | Lists objects - similar to @listObjects@, however uses the older
93+-- V1 AWS S3 API. Prefer @listObjects@ to this.
94+listObjectsV1 ::
95+ Bucket ->
96+ Maybe Text ->
97+ Bool ->
98+ C.ConduitM () ListItem Minio ()
4699listObjectsV1 bucket prefix recurse = loop Nothing
47100 where
48− loop :: Maybe Text -> C.Producer Minio ObjectInfo
101+ loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
49102 loop nextMarker = do
50− let
51− delimiter = bool (Just "/") Nothing recurse
103+ let delimiter = bool (Just "/") Nothing recurse
52104
53105 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
54− CL.sourceList $ lorObjects' res
106+ CL.sourceList $ map ListItemObject $ lorObjects' res
107+ unless recurse $
108+ CL.sourceList $
109+ map ListItemPrefix $
110+ lorCPrefixes' res
55111 when (lorHasMore' res) $
56112 loop (lorNextMarker res)
57113
58114-- | List incomplete uploads in a bucket matching the given prefix. If
59115-- recurse is set to True incomplete uploads for the given prefix are
60116-- recursively listed.
61−listIncompleteUploads :: Bucket -> Maybe Text -> Bool
62− -> C.Producer Minio UploadInfo
117+listIncompleteUploads ::
118+ Bucket ->
119+ Maybe Text ->
120+ Bool ->
121+ C.ConduitM () UploadInfo Minio ()
63122listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
64123 where
65− loop :: Maybe Text -> Maybe Text -> C.Producer Minio UploadInfo
124+ loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
66125 loop nextKeyMarker nextUploadIdMarker = do
67− let
68− delimiter = bool (Just "/") Nothing recurse
126+ let delimiter = bool (Just "/") Nothing recurse
69127
70− res <- lift $ listIncompleteUploads' bucket prefix delimiter
71− nextKeyMarker nextUploadIdMarker Nothing
128+ res <-
129+ lift $
130+ listIncompleteUploads'
131+ bucket
132+ prefix
133+ delimiter
134+ nextKeyMarker
135+ nextUploadIdMarker
136+ Nothing
72137
73− aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do
74− partInfos <- listIncompleteParts bucket uKey uId C.$$ CC.sinkList
75− 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
76145
77146 CL.sourceList $
78− map (\((uKey, uId, uInitTime), size) ->
79− UploadInfo uKey uId uInitTime size
80− ) $ zip (lurUploads res) aggrSizes
147+ zipWith
148+ ( curry
149+ ( \((uKey, uId, uInitTime), size) ->
150+ UploadInfo uKey uId uInitTime size
151+ )
152+ )
153+ (lurUploads res)
154+ aggrSizes
81155
82156 when (lurHasMore res) $
83157 loop (lurNextKey res) (lurNextUpload res)
84158
85−
86159-- | List object parts of an ongoing multipart upload for given
87160-- bucket, object and uploadId.
88−listIncompleteParts :: Bucket -> Object -> UploadId
89− -> C.Producer Minio ObjectPartInfo
161+listIncompleteParts ::
162+ Bucket ->
163+ Object ->
164+ UploadId ->
165+ C.ConduitM () ObjectPartInfo Minio ()
90166listIncompleteParts bucket object uploadId = loop Nothing
91167 where
92− loop :: Maybe Text -> C.Producer Minio ObjectPartInfo
168+ loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
93169 loop nextPartMarker = do
94− res <- lift $ listIncompleteParts' bucket object uploadId Nothing
95− nextPartMarker
170+ res <-
171+ lift $
172+ listIncompleteParts'
173+ bucket
174+ object
175+ uploadId
176+ Nothing
177+ nextPartMarker
96178 CL.sourceList $ lprParts res
97179 when (lprHasMore res) $
98180 loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs+221−147
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,45 +13,51 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16+{-# LANGUAGE CPP #-}
1617
1718module Network.Minio.PresignedOperations
18− ( UrlExpiry
19− , makePresignedUrl
20− , presignedPutObjectUrl
21− , presignedGetObjectUrl
22− , presignedHeadObjectUrl
23−
24− , PostPolicyCondition(..)
25− , ppCondBucket
26− , ppCondContentLengthRange
27− , ppCondContentType
28− , ppCondKey
29− , ppCondKeyStartsWith
30− , ppCondSuccessActionStatus
31−
32− , PostPolicy(..)
33− , PostPolicyError(..)
34− , newPostPolicy
35− , showPostPolicy
36− , presignedPostPolicy
37− ) where
38−
39−import Data.Aeson ((.=))
40−import qualified Data.Aeson as Json
41−import Data.ByteString.Builder (byteString, toLazyByteString)
42−import Data.Default (def)
43−import qualified Data.Map.Strict as Map
44−import qualified Data.Text as T
45−import qualified Data.Time as Time
46−import qualified Network.HTTP.Types as HT
47−import Network.HTTP.Types.Header (hHost)
48−
49−import Lib.Prelude
50−
51−import Network.Minio.Data
52−import Network.Minio.Data.Time
53−import Network.Minio.Errors
54−import Network.Minio.Sign.V4
19+ ( UrlExpiry,
20+ makePresignedUrl,
21+ presignedPutObjectUrl,
22+ presignedGetObjectUrl,
23+ presignedHeadObjectUrl,
24+ PostPolicyCondition (..),
25+ ppCondBucket,
26+ ppCondContentLengthRange,
27+ ppCondContentType,
28+ ppCondKey,
29+ ppCondKeyStartsWith,
30+ ppCondSuccessActionStatus,
31+ PostPolicy (..),
32+ PostPolicyError (..),
33+ newPostPolicy,
34+ showPostPolicy,
35+ presignedPostPolicy,
36+ )
37+where
38+
39+import Data.Aeson ((.=))
40+import qualified Data.Aeson as Json
41+import Data.ByteString.Builder (byteString, toLazyByteString)
42+import qualified Data.HashMap.Strict as H
43+import qualified Data.Text as T
44+import qualified Data.Time as Time
45+import Lib.Prelude
46+import qualified Network.HTTP.Client as NClient
47+import qualified Network.HTTP.Types as HT
48+import Network.Minio.API (buildRequest)
49+import Network.Minio.Credentials
50+import Network.Minio.Data
51+import Network.Minio.Data.Time
52+import Network.Minio.Errors
53+import Network.Minio.Sign.V4
54+import Network.URI (uriToString)
55+
56+{- ORMOLU_DISABLE -}
57+#if MIN_VERSION_aeson(2,0,0)
58+import qualified Data.Aeson.Key as A
59+#endif
60+{- ORMOLU_ENABLE -}
5561
5662-- | Generate a presigned URL. This function allows for advanced usage
5763-- - for simple cases prefer the `presigned*Url` functions.
@@ -61,35 +67,36 @@ import Network.Minio.Sign.V4
6167--
6268-- All extra query parameters or headers are signed, and therefore are
6369-- required to be sent when the generated URL is actually used.
64−makePresignedUrl :: UrlExpiry -> HT.Method -> Maybe Bucket -> Maybe Object
65− -> Maybe Region -> HT.Query -> HT.RequestHeaders
66− -> Minio ByteString
70+makePresignedUrl ::
71+ UrlExpiry ->
72+ HT.Method ->
73+ Maybe Bucket ->
74+ Maybe Object ->
75+ Maybe Region ->
76+ HT.Query ->
77+ HT.RequestHeaders ->
78+ Minio ByteString
6779makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
68− when (expiry > 7*24*3600 || expiry < 0) $
69− throwM $ MErrVInvalidUrlExpiry expiry
70−
71− ci <- asks mcConnInfo
72−
73− let
74− host = formatBS "{}:{}" (connectHost ci, connectPort ci)
75− hostHeader = (hHost, host)
76− ri = def { riMethod = method
77− , riBucket = bucket
78− , riObject = object
79− , riQueryParams = extraQuery
80− , riHeaders = hostHeader : extraHeaders
81− , riRegion = Just $ maybe (connectRegion ci) identity region
82− }
83−
84− signPairs <- liftIO $ signV4 ci ri (Just expiry)
85−
86− let
87− qpToAdd = (fmap . fmap) Just signPairs
88− queryStr = HT.renderQueryBuilder True (riQueryParams ri ++ qpToAdd)
89− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
90−
91− return $ toS $ toLazyByteString $
92− scheme <> byteString host <> byteString (getPathFromRI ri) <> queryStr
80+ when (expiry > 7 * 24 * 3600 || expiry < 0) $
81+ throwIO $
82+ MErrVInvalidUrlExpiry expiry
83+
84+ let s3ri =
85+ defaultS3ReqInfo
86+ { riPresignExpirySecs = Just expiry,
87+ riMethod = method,
88+ riBucket = bucket,
89+ riObject = object,
90+ riRegion = region,
91+ riQueryParams = extraQuery,
92+ riHeaders = extraHeaders
93+ }
94+
95+ req <- buildRequest s3ri
96+ let uri = NClient.getUri req
97+ uriString = uriToString identity uri ""
98+
99+ return $ encodeUtf8 uriString
93100
94101-- | Generate a URL with authentication signature to PUT (upload) an
95102-- object. Any extra headers if passed, are signed, and so they are
@@ -98,11 +105,22 @@ makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
98105--
99106-- For a list of possible headers to pass, please refer to the PUT
100107-- object REST API AWS S3 documentation.
101−presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders
102− -> Minio ByteString
103−presignedPutObjectUrl bucket object expirySeconds extraHeaders =
104− makePresignedUrl expirySeconds HT.methodPut
105− (Just bucket) (Just object) Nothing [] extraHeaders
108+presignedPutObjectUrl ::
109+ Bucket ->
110+ Object ->
111+ UrlExpiry ->
112+ HT.RequestHeaders ->
113+ Minio ByteString
114+presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
115+ region <- asks (Just . connectRegion . mcConnInfo)
116+ makePresignedUrl
117+ expirySeconds
118+ HT.methodPut
119+ (Just bucket)
120+ (Just object)
121+ region
122+ []
123+ extraHeaders
106124
107125-- | Generate a URL with authentication signature to GET (download) an
108126-- object. All extra query parameters and headers passed here will be
@@ -113,11 +131,23 @@ presignedPutObjectUrl bucket object expirySeconds extraHeaders =
113131--
114132-- For a list of possible request parameters and headers, please refer
115133-- to the GET object REST API AWS S3 documentation.
116−presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query
117− -> HT.RequestHeaders -> Minio ByteString
118−presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders =
119− makePresignedUrl expirySeconds HT.methodGet
120− (Just bucket) (Just object) Nothing extraQuery extraHeaders
134+presignedGetObjectUrl ::
135+ Bucket ->
136+ Object ->
137+ UrlExpiry ->
138+ HT.Query ->
139+ HT.RequestHeaders ->
140+ Minio ByteString
141+presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
142+ region <- asks (Just . connectRegion . mcConnInfo)
143+ makePresignedUrl
144+ expirySeconds
145+ HT.methodGet
146+ (Just bucket)
147+ (Just object)
148+ region
149+ extraQuery
150+ extraHeaders
121151
122152-- | Generate a URL with authentication signature to make a HEAD
123153-- request on an object. This is used to fetch metadata about an
@@ -126,49 +156,74 @@ presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders =
126156--
127157-- For a list of possible headers to pass, please refer to the HEAD
128158-- object REST API AWS S3 documentation.
129−presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry
130− -> HT.RequestHeaders -> Minio ByteString
131−presignedHeadObjectUrl bucket object expirySeconds extraHeaders =
132− makePresignedUrl expirySeconds HT.methodHead
133− (Just bucket) (Just object) Nothing [] extraHeaders
159+presignedHeadObjectUrl ::
160+ Bucket ->
161+ Object ->
162+ UrlExpiry ->
163+ HT.RequestHeaders ->
164+ Minio ByteString
165+presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
166+ region <- asks (Just . connectRegion . mcConnInfo)
167+ makePresignedUrl
168+ expirySeconds
169+ HT.methodHead
170+ (Just bucket)
171+ (Just object)
172+ region
173+ []
174+ extraHeaders
134175
135176-- | Represents individual conditions in a Post Policy document.
136−data PostPolicyCondition = PPCStartsWith Text Text
137− | PPCEquals Text Text
138− | PPCRange Text Int64 Int64
139− deriving (Show, Eq)
177+data PostPolicyCondition
178+ = PPCStartsWith Text Text
179+ | PPCEquals Text Text
180+ | PPCRange Text Int64 Int64
181+ deriving stock (Show, Eq)
140182
183+{- ORMOLU_DISABLE -}
141184instance Json.ToJSON PostPolicyCondition where
142185 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
143189 toJSON (PPCEquals k v) = Json.object [k .= v]
190+#endif
144191 toJSON (PPCRange k minVal maxVal) =
145192 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
146193
147194 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
148198 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
199+#endif
149200 toEncoding (PPCRange k minVal maxVal) =
150201 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202+{- ORMOLU_ENABLE -}
151203
152204-- | A PostPolicy is required to perform uploads via browser forms.
153−data PostPolicy = PostPolicy {
154− expiration :: UTCTime
155− , conditions :: [PostPolicyCondition]
156− } deriving (Show, Eq)
205+data PostPolicy = PostPolicy
206+ { expiration :: UTCTime,
207+ conditions :: [PostPolicyCondition]
208+ }
209+ deriving stock (Show, Eq)
157210
158211instance Json.ToJSON PostPolicy where
159212 toJSON (PostPolicy e c) =
160− Json.object $ [ "expiration" .= iso8601TimeFormat e
161− , "conditions" .= c
162− ]
213+ Json.object
214+ [ "expiration" .= iso8601TimeFormat e,
215+ "conditions" .= c
216+ ]
163217 toEncoding (PostPolicy e c) =
164218 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
165219
166220-- | Possible validation errors when creating a PostPolicy.
167−data PostPolicyError = PPEKeyNotSpecified
168− | PPEBucketNotSpecified
169− | PPEConditionKeyEmpty
170− | PPERangeInvalid
171− deriving (Eq, Show)
221+data PostPolicyError
222+ = PPEKeyNotSpecified
223+ | PPEBucketNotSpecified
224+ | PPEConditionKeyEmpty
225+ | PPERangeInvalid
226+ deriving stock (Show, Eq)
172227
173228-- | Set the bucket name that the upload should use.
174229ppCondBucket :: Bucket -> PostPolicyCondition
@@ -176,8 +231,10 @@ ppCondBucket = PPCEquals "bucket"
176231
177232-- | Set the content length range constraint with minimum and maximum
178233-- byte count values.
179−ppCondContentLengthRange :: Int64 -> Int64
180− -> PostPolicyCondition
234+ppCondContentLengthRange ::
235+ Int64 ->
236+ Int64 ->
237+ PostPolicyCondition
181238ppCondContentLengthRange = PPCRange "content-length-range"
182239
183240-- | Set the content-type header for the upload.
@@ -200,82 +257,99 @@ ppCondSuccessActionStatus n =
200257
201258-- | This function creates a PostPolicy after validating its
202259-- arguments.
203−newPostPolicy :: UTCTime -> [PostPolicyCondition]
204− -> Either PostPolicyError PostPolicy
260+newPostPolicy ::
261+ UTCTime ->
262+ [PostPolicyCondition] ->
263+ Either PostPolicyError PostPolicy
205264newPostPolicy expirationTime conds
206265 -- object name condition must be present
207266 | not $ any (keyEquals "key") conds =
208267 Left PPEKeyNotSpecified
209−
210268 -- bucket name condition must be present
211269 | not $ any (keyEquals "bucket") conds =
212270 Left PPEBucketNotSpecified
213−
214271 -- a condition with an empty key is invalid
215272 | any (keyEquals "") conds || any isEmptyRangeKey conds =
216273 Left PPEConditionKeyEmpty
217−
218274 -- invalid range check
219275 | any isInvalidRange conds =
220276 Left PPERangeInvalid
221−
222277 -- all good!
223278 | otherwise =
224279 return $ PostPolicy expirationTime conds
225−
226280 where
227281 keyEquals k' (PPCStartsWith k _) = k == k'
228− keyEquals k' (PPCEquals k _) = k == k'
229− keyEquals _ _ = False
230−
282+ keyEquals k' (PPCEquals k _) = k == k'
283+ keyEquals _ _ = False
231284 isEmptyRangeKey (PPCRange k _ _) = k == ""
232− isEmptyRangeKey _ = False
233−
285+ isEmptyRangeKey _ = False
234286 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
235− isInvalidRange _ = False
287+ isInvalidRange _ = False
236288
237289-- | Convert Post Policy to a string (e.g. for printing).
238290showPostPolicy :: PostPolicy -> ByteString
239−showPostPolicy = toS . Json.encode
291+showPostPolicy = toStrictBS . Json.encode
240292
241293-- | Generate a presigned URL and POST policy to upload files via a
242294-- browser. On success, this function returns a URL and POST
243295-- form-data.
244−presignedPostPolicy :: PostPolicy
245− -> Minio (ByteString, Map.Map Text ByteString)
296+presignedPostPolicy ::
297+ PostPolicy ->
298+ Minio (ByteString, H.HashMap Text ByteString)
246299presignedPostPolicy p = do
247300 ci <- asks mcConnInfo
248− signTime <- liftIO $ Time.getCurrentTime
249−
250− let
251− extraConditions =
252− [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime)
253− , PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256"
254− , PPCEquals "x-amz-credential"
255− (T.intercalate "/" [connectAccessKey ci,
256− decodeUtf8 $ mkScope signTime region])
257− ]
258− ppWithCreds = p {
259− conditions = conditions p ++ extraConditions
260− }
261− signData = signV4PostPolicy (showPostPolicy ppWithCreds)
262− signTime ci
263−
264− -- compute form-data
265− mkPair (PPCStartsWith k v) = Just (k, v)
266− mkPair (PPCEquals k v) = Just (k, v)
267− mkPair _ = Nothing
268− formFromPolicy = Map.map toS $ Map.fromList $ catMaybes $
269− mkPair <$> conditions ppWithCreds
270− formData = formFromPolicy `Map.union` signData
271−
272− -- compute POST upload URL
273− bucket = Map.findWithDefault "" "bucket" formData
274− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
275− host = formatBS "{}:{}" (connectHost ci, connectPort ci)
276− region = connectRegion ci
277−
278− url = toS $ toLazyByteString $ scheme <> byteString host <>
279− byteString "/" <> byteString (toS bucket) <> byteString "/"
301+ signTime <- liftIO Time.getCurrentTime
302+ mgr <- asks mcConnManager
303+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
304+
305+ let extraConditions signParams =
306+ [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
307+ PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
308+ PPCEquals
309+ "x-amz-credential"
310+ ( T.intercalate
311+ "/"
312+ [ coerce $ cvAccessKey cv,
313+ decodeUtf8 $ credentialScope signParams
314+ ]
315+ )
316+ ]
317+ ppWithCreds signParams =
318+ p
319+ { conditions = conditions p ++ extraConditions signParams
320+ }
321+ sp =
322+ SignParams
323+ (coerce $ cvAccessKey cv)
324+ (coerce $ cvSecretKey cv)
325+ (coerce $ cvSessionToken cv)
326+ ServiceS3
327+ signTime
328+ (Just $ connectRegion ci)
329+ Nothing
330+ Nothing
331+ signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
332+ -- compute form-data
333+ mkPair (PPCStartsWith k v) = Just (k, v)
334+ mkPair (PPCEquals k v) = Just (k, v)
335+ mkPair _ = Nothing
336+ formFromPolicy =
337+ H.map encodeUtf8 $
338+ H.fromList $
339+ mapMaybe
340+ mkPair
341+ (conditions $ ppWithCreds sp)
342+ formData = formFromPolicy `H.union` signData
343+ -- compute POST upload URL
344+ bucket = H.lookupDefault "" "bucket" formData
345+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
346+ url =
347+ toStrictBS $
348+ toLazyByteString $
349+ scheme
350+ <> byteString (getHostAddr ci)
351+ <> byteString "/"
352+ <> byteString bucket
353+ <> byteString "/"
280354
281355 return (url, formData)
src/Network/Minio/PutObject.hs+102−160
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,43 +15,24 @@
1515--
1616
1717module Network.Minio.PutObject
18− (
19− putObjectInternal
20− , ObjectData(..)
21− , selectPartSizes
22− , copyObjectInternal
23− , selectCopyRanges
24− , minPartSize
25− ) where
26−
27−
28−import qualified Data.Conduit as C
18+ ( putObjectInternal,
19+ ObjectData (..),
20+ selectPartSizes,
21+ )
22+where
23+
24+import Conduit (takeC)
25+import qualified Conduit as C
26+import qualified Data.ByteString.Lazy as LBS
2927import qualified Data.Conduit.Binary as CB
3028import qualified Data.Conduit.Combinators as CC
3129import qualified Data.Conduit.List as CL
3230import qualified Data.List as List
33−
34−import Lib.Prelude
35−
36−import Network.Minio.Data
37−import Network.Minio.Errors
38−import Network.Minio.S3API
39−import Network.Minio.Utils
40−
41−
42−-- | max obj size is 5TiB
43−maxObjectSize :: Int64
44−maxObjectSize = 5 * 1024 * 1024 * oneMiB
45−
46−-- | minimum size of parts used in multipart operations.
47−minPartSize :: Int64
48−minPartSize = 64 * oneMiB
49−
50−oneMiB :: Int64
51−oneMiB = 1024 * 1024
52−
53−maxMultipartParts :: Int64
54−maxMultipartParts = 10000
31+import Lib.Prelude
32+import Network.Minio.Data
33+import Network.Minio.Errors
34+import Network.Minio.S3API
35+import Network.Minio.Utils
5536
5637-- | A data-type to represent the source data for an object. A
5738-- file-path or a producer-conduit may be provided.
@@ -62,70 +43,90 @@ maxMultipartParts = 10000
6243--
6344-- For streams also, a size may be provided. This is useful to limit
6445-- the input - if it is not provided, upload will continue until the
65−-- stream ends or the object reaches `maxObjectsize` size.
66−data ObjectData m =
67− ODFile FilePath (Maybe Int64) -- ^ Takes filepath and optional size.
68− | ODStream (C.Producer m ByteString) (Maybe Int64) -- ^ Pass size in bytes as maybe if known.
46+-- stream ends or the object reaches `maxObjectSize` size.
47+data ObjectData m
48+ = -- | Takes filepath and optional
49+ -- size.
50+ ODFile FilePath (Maybe Int64)
51+ | -- | Pass
52+ -- size
53+ -- (bytes)
54+ -- if
55+ -- known.
56+ ODStream (C.ConduitM () ByteString m ()) (Maybe Int64)
6957
7058-- | Put an object from ObjectData. This high-level API handles
7159-- objects of all sizes, and even if the object size is unknown.
72−putObjectInternal :: Bucket -> Object -> ObjectData Minio -> Minio ETag
73−putObjectInternal b o (ODStream src sizeMay) = sequentialMultipartUpload b o sizeMay src
74−putObjectInternal b o (ODFile fp sizeMay) = do
60+putObjectInternal ::
61+ Bucket ->
62+ Object ->
63+ PutObjectOptions ->
64+ ObjectData Minio ->
65+ Minio ETag
66+putObjectInternal b o opts (ODStream src sizeMay) = do
67+ case sizeMay of
68+ -- unable to get size, so assume non-seekable file
69+ Nothing -> sequentialMultipartUpload b o opts Nothing src
70+ -- got file size, so check for single/multipart upload
71+ Just size ->
72+ if
73+ | size <= 64 * oneMiB -> do
74+ bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
75+ putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
76+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
77+ | otherwise -> sequentialMultipartUpload b o opts (Just size) src
78+putObjectInternal b o opts (ODFile fp sizeMay) = do
7579 hResE <- withNewHandle fp $ \h ->
76− liftM2 (,) (isHandleSeekable h) (getFileSize h)
80+ liftA2 (,) (isHandleSeekable h) (getFileSize h)
7781
78− (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return
79− hResE
82+ (isSeekable, handleSizeMay) <-
83+ either
84+ (const $ return (False, Nothing))
85+ return
86+ hResE
8087
8188 -- prefer given size to queried size.
8289 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
8390
8491 case finalSizeMay of
85− -- unable to get size, so assume non-seekable file and max-object size
86− Nothing -> sequentialMultipartUpload b o (Just maxObjectSize) $
87− CB.sourceFile fp
88−
92+ -- unable to get size, so assume non-seekable file
93+ Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
8994 -- got file size, so check for single/multipart upload
9095 Just size ->
91− if | size <= 64 * oneMiB -> either throwM return =<<
92− withNewHandle fp (\h -> putObjectSingle b o [] h 0 size)
93− | size > maxObjectSize -> throwM $ MErrVPutSizeExceeded size
94− | isSeekable -> parallelMultipartUpload b o fp size
95− | otherwise -> sequentialMultipartUpload b o (Just size) $
96− CB.sourceFile fp
97−
98−-- | Select part sizes - the logic is that the minimum part-size will
99−-- be 64MiB.
100−selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
101−selectPartSizes size = uncurry (List.zip3 [1..]) $
102− List.unzip $ loop 0 size
103− where
104− ceil :: Double -> Int64
105− ceil = ceiling
106− partSize = max minPartSize (ceil $ fromIntegral size /
107− fromIntegral maxMultipartParts)
108−
109− m = fromIntegral partSize
110− loop st sz
111− | st > sz = []
112− | st + m >= sz = [(st, sz - st)]
113− | otherwise = (st, m) : loop (st + m) sz
114−
115−parallelMultipartUpload :: Bucket -> Object -> FilePath -> Int64
116− -> Minio ETag
117−parallelMultipartUpload b o filePath size = do
96+ if
97+ | size <= 64 * oneMiB ->
98+ either throwIO return
99+ =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
100+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
101+ | isSeekable -> parallelMultipartUpload b o opts fp size
102+ | otherwise ->
103+ sequentialMultipartUpload b o opts (Just size) $
104+ CB.sourceFile fp
105+
106+parallelMultipartUpload ::
107+ Bucket ->
108+ Object ->
109+ PutObjectOptions ->
110+ FilePath ->
111+ Int64 ->
112+ Minio ETag
113+parallelMultipartUpload b o opts filePath size = do
118114 -- get a new upload id.
119− uploadId <- newMultipartUpload b o []
115+ uploadId <- newMultipartUpload b o (pooToHeaders opts)
120116
121117 let partSizeInfo = selectPartSizes size
122118
123− -- perform upload with 10 threads
124− uploadedPartsE <- limitedMapConcurrently 10
125− (uploadPart uploadId) partSizeInfo
119+ let threads = fromMaybe 10 $ pooNumThreads opts
120+
121+ -- perform upload with 'threads' threads
122+ uploadedPartsE <-
123+ limitedMapConcurrently
124+ (fromIntegral threads)
125+ (uploadPart uploadId)
126+ partSizeInfo
126127
127128 -- if there were any errors, rethrow exception.
128− mapM_ throwM $ lefts uploadedPartsE
129+ mapM_ throwIO $ lefts uploadedPartsE
129130
130131 -- if we get here, all parts were successfully uploaded.
131132 completeMultipartUpload b o uploadId $ rights uploadedPartsE
@@ -136,96 +137,37 @@ parallelMultipartUpload b o filePath size = do
136137 putObjectPart b o uploadId partNum [] payload
137138
138139-- | Upload multipart object from conduit source sequentially
139−sequentialMultipartUpload :: Bucket -> Object -> Maybe Int64
140− -> C.Producer Minio ByteString -> Minio ETag
141−sequentialMultipartUpload b o sizeMay src = do
140+sequentialMultipartUpload ::
141+ Bucket ->
142+ Object ->
143+ PutObjectOptions ->
144+ Maybe Int64 ->
145+ C.ConduitM () ByteString Minio () ->
146+ Minio ETag
147+sequentialMultipartUpload b o opts sizeMay src = do
142148 -- get a new upload id.
143− uploadId <- newMultipartUpload b o []
149+ uploadId <- newMultipartUpload b o (pooToHeaders opts)
144150
145151 -- upload parts in loop
146152 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
147153 (pnums, _, sizes) = List.unzip3 partSizes
148− uploadedParts <- src
149− C..| chunkBSConduit sizes
150− C..| CL.map PayloadBS
151− C..| uploadPart' uploadId pnums
152− C.$$ CC.sinkList
154+ uploadedParts <-
155+ C.runConduit $
156+ src
157+ C..| chunkBSConduit (map fromIntegral sizes)
158+ C..| CL.map PayloadBS
159+ C..| uploadPart' uploadId pnums
160+ C..| CC.sinkList
153161
154162 -- complete multipart upload
155163 completeMultipartUpload b o uploadId uploadedParts
156−
157164 where
158165 uploadPart' _ [] = return ()
159− uploadPart' uid (pn:pns) = do
166+ uploadPart' uid (pn : pns) = do
160167 payloadMay <- C.await
161168 case payloadMay of
162169 Nothing -> return ()
163− Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload
164− C.yield pinfo
165− uploadPart' uid pns
166−
167−-- | Copy an object using single or multipart copy strategy.
168−copyObjectInternal :: Bucket -> Object -> CopyPartSource
169− -> Minio ETag
170−copyObjectInternal b' o cps = do
171− -- validate and extract the src bucket and object
172− (srcBucket, srcObject) <- maybe
173− (throwM $ MErrVInvalidSrcObjSpec $ cpSource cps)
174− return $ cpsToObject cps
175−
176− -- get source object size with a head request
177− (ObjectInfo _ _ _ srcSize) <- headObject srcBucket srcObject
178−
179− -- check that byte offsets are valid if specified in cps
180− when (isJust (cpSourceRange cps) &&
181− or [fst range < 0, snd range < fst range,
182− snd range >= fromIntegral srcSize]) $
183− throwM $ MErrVInvalidSrcObjByteRange range
184−
185− -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
186− -- 2. If startOffset /= 0 use multipart copy
187− let destSize = (\(a, b) -> b - a + 1 ) $
188− maybe (0, srcSize - 1) identity $ cpSourceRange cps
189− startOffset = maybe 0 fst $ cpSourceRange cps
190− endOffset = maybe (srcSize - 1) snd $ cpSourceRange cps
191−
192− if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
193− then multiPartCopyObject b' o cps srcSize
194−
195− else fst <$> copyObjectSingle b' o cps{cpSourceRange = Nothing} []
196−
197− where
198− range = maybe (0, 0) identity $ cpSourceRange cps
199−
200−-- | Given the input byte range of the source object, compute the
201−-- splits for a multipart copy object procedure. Minimum part size
202−-- used is minPartSize.
203−selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
204−selectCopyRanges (st, end) = zip pns $
205− map (\(x, y) -> (st + x, st + x + y - 1)) $ zip startOffsets partSizes
206− where
207− size = end - st + 1
208− (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
209−
210−-- | Perform a multipart copy object action. Since we cannot verify
211−-- existing parts based on the source object, there is no resuming
212−-- copy action support.
213−multiPartCopyObject :: Bucket -> Object -> CopyPartSource -> Int64
214− -> Minio ETag
215−multiPartCopyObject b o cps srcSize = do
216− uid <- newMultipartUpload b o []
217−
218− let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $
219− cpSourceRange cps
220− partRanges = selectCopyRanges byteRange
221− partSources = map (\(x, y) -> (x, cps {cpSourceRange = Just y}))
222− partRanges
223−
224− copiedParts <- limitedMapConcurrently 10
225− (\(pn, cps') -> do
226− (etag, _) <- copyObjectPart b o cps' uid pn []
227− return (pn, etag)
228− )
229− partSources
230−
231− completeMultipartUpload b o uid copiedParts
170+ Just payload -> do
171+ pinfo <- lift $ putObjectPart b o uid pn [] payload
172+ C.yield pinfo
173+ uploadPart' uid pns
src/Network/Minio/S3API.hs+544−281
@@ -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.
@@ -14,409 +14,672 @@
1414-- limitations under the License.
1515--
1616
17+-- |
18+-- Module: Network.Minio.S3API
19+-- Copyright: (c) 2017-2023 MinIO Dev Team
20+-- License: Apache 2.0
21+-- Maintainer: MinIO Dev Team <dev@min.io>
22+--
23+-- Lower-level API for S3 compatible object stores. Start with @Network.Minio@
24+-- and use this only if needed.
1725module Network.Minio.S3API
18− (
19− Region
20− , getLocation
21−
22− -- * Listing buckets
23− --------------------
24− , getService
25−
26− -- * Listing objects
27− --------------------
28− , ListObjectsResult(..)
29− , ListObjectsV1Result(..)
30− , listObjects'
31− , listObjectsV1'
32−
33− -- * Retrieving buckets
34− , headBucket
35−
36− -- * Retrieving objects
37− -----------------------
38− , getObject'
39− , headObject
40−
41− -- * Creating buckets and objects
42− ---------------------------------
43− , putBucket
44− , ETag
45− , putObjectSingle
46− , copyObjectSingle
47−
48− -- * Multipart Upload APIs
49− --------------------------
50− , UploadId
51− , PartTuple
52− , Payload(..)
53− , PartNumber
54− , CopyPartSource(..)
55− , newMultipartUpload
56− , putObjectPart
57− , copyObjectPart
58− , completeMultipartUpload
59− , abortMultipartUpload
60− , ListUploadsResult(..)
61− , listIncompleteUploads'
62− , ListPartsResult(..)
63− , listIncompleteParts'
64−
65− -- * Deletion APIs
66− --------------------------
67− , deleteBucket
68− , deleteObject
69−
70− -- * Presigned Operations
71− -----------------------------
72− , module Network.Minio.PresignedOperations
73−
74− -- * Bucket Notifications
75− -------------------------
76− , Notification(..)
77− , NotificationConfig(..)
78− , Arn
79− , Event(..)
80− , Filter(..)
81− , FilterKey(..)
82− , FilterRules(..)
83− , FilterRule(..)
84− , getBucketNotification
85− , putBucketNotification
86− , removeAllBucketNotification
87− ) where
88−
89−import Control.Monad.Catch (catches, Handler(..))
90−import qualified Data.Conduit as C
91−import Data.Default (def)
26+ ( Region,
27+ getLocation,
28+
29+ -- * Listing buckets
30+
31+ --------------------
32+ getService,
33+
34+ -- * Listing objects
35+
36+ --------------------
37+ ListObjectsResult (..),
38+ ListObjectsV1Result (..),
39+ listObjects',
40+ listObjectsV1',
41+
42+ -- * Retrieving buckets
43+ headBucket,
44+
45+ -- * Retrieving objects
46+
47+ -----------------------
48+ getObject',
49+ headObject,
50+
51+ -- * Creating buckets and objects
52+
53+ ---------------------------------
54+ putBucket,
55+ ETag,
56+ maxSinglePutObjectSizeBytes,
57+ putObjectSingle',
58+ putObjectSingle,
59+ copyObjectSingle,
60+
61+ -- * Multipart Upload APIs
62+
63+ --------------------------
64+ UploadId,
65+ PartTuple,
66+ Payload (..),
67+ PartNumber,
68+ newMultipartUpload,
69+ putObjectPart,
70+ copyObjectPart,
71+ completeMultipartUpload,
72+ abortMultipartUpload,
73+ ListUploadsResult (..),
74+ listIncompleteUploads',
75+ ListPartsResult (..),
76+ listIncompleteParts',
77+
78+ -- * Deletion APIs
79+
80+ --------------------------
81+ deleteBucket,
82+ deleteObject,
83+
84+ -- * Presigned Operations
85+
86+ -----------------------------
87+ module Network.Minio.PresignedOperations,
88+
89+ -- ** Bucket Policies
90+ getBucketPolicy,
91+ setBucketPolicy,
92+
93+ -- * Bucket Notifications
94+
95+ -------------------------
96+ Notification (..),
97+ NotificationConfig (..),
98+ Arn,
99+ Event (..),
100+ Filter (..),
101+ FilterKey (..),
102+ FilterRules (..),
103+ FilterRule (..),
104+ getBucketNotification,
105+ putBucketNotification,
106+ removeAllBucketNotification,
107+ )
108+where
109+
110+import qualified Data.ByteString as BS
111+import qualified Data.Text as T
112+import Lib.Prelude
92113import qualified Network.HTTP.Conduit as NC
93114import qualified Network.HTTP.Types as HT
94−import Network.HTTP.Types.Status (status404)
95−
96−import Lib.Prelude hiding (catches)
97−
98−import Network.Minio.API
99−import Network.Minio.Data
100−import Network.Minio.Errors
101−import Network.Minio.Utils
102−import Network.Minio.XmlGenerator
103−import Network.Minio.XmlParser
104−import Network.Minio.PresignedOperations
105−
115+import Network.HTTP.Types.Status (status404)
116+import Network.Minio.API
117+import Network.Minio.APICommon
118+import Network.Minio.Data
119+import Network.Minio.Errors
120+import Network.Minio.PresignedOperations
121+import Network.Minio.Utils
122+import Network.Minio.XmlGenerator
123+import Network.Minio.XmlParser
124+import UnliftIO (Handler (Handler))
106125
107126-- | Fetch all buckets from the service.
108127getService :: Minio [BucketInfo]
109128getService = do
110− resp <- executeRequest $ def {
111− riNeedsLocation = False
112− }
129+ resp <-
130+ executeRequest $
131+ defaultS3ReqInfo
132+ { riNeedsLocation = False
133+ }
113134 parseListBuckets $ NC.responseBody resp
114135
115−-- | GET an object from the service and return the response headers
116−-- and a conduit source for the object content
117−getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
118− -> Minio ([HT.Header], C.ResumableSource Minio ByteString)
136+-- Parse headers from getObject and headObject calls.
137+parseGetObjectHeaders :: Object -> [HT.Header] -> Maybe ObjectInfo
138+parseGetObjectHeaders object headers =
139+ let metadataPairs = getMetadata headers
140+ userMetadata = getUserMetadataMap metadataPairs
141+ metadata = getNonUserMetadataMap metadataPairs
142+ in ObjectInfo
143+ <$> Just object
144+ <*> getLastModifiedHeader headers
145+ <*> getETagHeader headers
146+ <*> getContentLength headers
147+ <*> Just userMetadata
148+ <*> Just metadata
149+
150+-- | GET an object from the service and return parsed ObjectInfo and a
151+-- conduit source for the object content
152+getObject' ::
153+ Bucket ->
154+ Object ->
155+ HT.Query ->
156+ [HT.Header] ->
157+ Minio GetObjectResponse
119158getObject' bucket object queryParams headers = do
120159 resp <- mkStreamRequest reqInfo
121− return (NC.responseHeaders resp, NC.responseBody resp)
160+ let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
161+ objInfo <-
162+ maybe
163+ (throwIO MErrVInvalidObjectInfoResponse)
164+ return
165+ objInfoMaybe
166+ return $
167+ GetObjectResponse
168+ { gorObjectInfo = objInfo,
169+ gorObjectStream = NC.responseBody resp
170+ }
122171 where
123− reqInfo = def { riBucket = Just bucket
124− , riObject = Just object
125− , riQueryParams = queryParams
126− , riHeaders = headers
127− }
172+ reqInfo =
173+ defaultS3ReqInfo
174+ { riBucket = Just bucket,
175+ riObject = Just object,
176+ riQueryParams = queryParams,
177+ riHeaders =
178+ headers
179+ -- This header is required for safety as otherwise http-client,
180+ -- sends Accept-Encoding: gzip, and the server may actually gzip
181+ -- body. In that case Content-Length header will be missing.
182+ <> [("Accept-Encoding", "identity")]
183+ }
128184
129185-- | Creates a bucket via a PUT bucket call.
130186putBucket :: Bucket -> Region -> Minio ()
131−putBucket bucket location = void $
132− executeRequest $
133− def { riMethod = HT.methodPut
134− , riBucket = Just bucket
135− , riPayload = PayloadBS $ mkCreateBucketConfig location
136− , riNeedsLocation = False
187+putBucket bucket location = do
188+ ns <- asks getSvcNamespace
189+ void $
190+ executeRequest $
191+ defaultS3ReqInfo
192+ { riMethod = HT.methodPut,
193+ riBucket = Just bucket,
194+ riPayload = PayloadBS $ mkCreateBucketConfig ns location,
195+ riNeedsLocation = False
137196 }
138197
139198-- | Single PUT object size.
140199maxSinglePutObjectSizeBytes :: Int64
141200maxSinglePutObjectSizeBytes = 5 * 1024 * 1024 * 1024
142201
202+-- | PUT an object into the service. This function performs a single
203+-- PUT object call and uses a strict ByteString as the object
204+-- data. `putObjectSingle` is preferable as the object data will not
205+-- be resident in memory.
206+putObjectSingle' :: Bucket -> Object -> [HT.Header] -> ByteString -> Minio ETag
207+putObjectSingle' bucket object headers bs = do
208+ let size = fromIntegral (BS.length bs)
209+ -- check length is within single PUT object size.
210+ when (size > maxSinglePutObjectSizeBytes) $
211+ throwIO $
212+ MErrVSinglePUTSizeExceeded size
213+
214+ let payload = mkStreamingPayload $ PayloadBS bs
215+ resp <-
216+ executeRequest $
217+ defaultS3ReqInfo
218+ { riMethod = HT.methodPut,
219+ riBucket = Just bucket,
220+ riObject = Just object,
221+ riHeaders = headers,
222+ riPayload = payload
223+ }
224+
225+ let rheaders = NC.responseHeaders resp
226+ etag = getETagHeader rheaders
227+ maybe
228+ (throwIO MErrVETagHeaderNotFound)
229+ return
230+ etag
231+
143232-- | PUT an object into the service. This function performs a single
144233-- PUT object call, and so can only transfer objects upto 5GiB.
145−putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
146− -> Int64 -> Minio ETag
234+putObjectSingle ::
235+ Bucket ->
236+ Object ->
237+ [HT.Header] ->
238+ Handle ->
239+ Int64 ->
240+ Int64 ->
241+ Minio ETag
147242putObjectSingle bucket object headers h offset size = do
148243 -- check length is within single PUT object size.
149244 when (size > maxSinglePutObjectSizeBytes) $
150− throwM $ MErrVSinglePUTSizeExceeded size
245+ throwIO $
246+ MErrVSinglePUTSizeExceeded size
151247
152248 -- content-length header is automatically set by library.
153− resp <- executeRequest $
154− def { riMethod = HT.methodPut
155− , riBucket = Just bucket
156− , riObject = Just object
157− , riHeaders = headers
158− , riPayload = PayloadH h offset size
159− }
249+ let payload = mkStreamingPayload $ PayloadH h offset size
250+ resp <-
251+ executeRequest $
252+ defaultS3ReqInfo
253+ { riMethod = HT.methodPut,
254+ riBucket = Just bucket,
255+ riObject = Just object,
256+ riHeaders = headers,
257+ riPayload = payload
258+ }
160259
161260 let rheaders = NC.responseHeaders resp
162261 etag = getETagHeader rheaders
163262 maybe
164− (throwM MErrVETagHeaderNotFound)
165− return etag
263+ (throwIO MErrVETagHeaderNotFound)
264+ return
265+ etag
166266
167267-- | List objects in a bucket matching prefix up to delimiter,
168268-- starting from nextMarker.
169−listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
170− -> Minio ListObjectsV1Result
269+listObjectsV1' ::
270+ Bucket ->
271+ Maybe Text ->
272+ Maybe Text ->
273+ Maybe Text ->
274+ Maybe Int ->
275+ Minio ListObjectsV1Result
171276listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
172− resp <- executeRequest $ def { riMethod = HT.methodGet
173− , riBucket = Just bucket
174− , riQueryParams = mkOptionalParams params
175− }
277+ resp <-
278+ executeRequest $
279+ defaultS3ReqInfo
280+ { riMethod = HT.methodGet,
281+ riBucket = Just bucket,
282+ riQueryParams = mkOptionalParams params
283+ }
176284 parseListObjectsV1Response $ NC.responseBody resp
177285 where
178− params = [
179− ("marker", nextMarker)
180− , ("prefix", prefix)
181− , ("delimiter", delimiter)
182− , ("max-keys", show <$> maxKeys)
286+ params =
287+ [ ("marker", nextMarker),
288+ ("prefix", prefix),
289+ ("delimiter", delimiter),
290+ ("max-keys", show <$> maxKeys)
183291 ]
184292
185293-- | List objects in a bucket matching prefix up to delimiter,
186294-- starting from nextToken.
187−listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
188− -> Minio ListObjectsResult
295+listObjects' ::
296+ Bucket ->
297+ Maybe Text ->
298+ Maybe Text ->
299+ Maybe Text ->
300+ Maybe Int ->
301+ Minio ListObjectsResult
189302listObjects' bucket prefix nextToken delimiter maxKeys = do
190− resp <- executeRequest $ def { riMethod = HT.methodGet
191− , riBucket = Just bucket
192− , riQueryParams = mkOptionalParams params
193− }
303+ resp <-
304+ executeRequest $
305+ defaultS3ReqInfo
306+ { riMethod = HT.methodGet,
307+ riBucket = Just bucket,
308+ riQueryParams = mkOptionalParams params
309+ }
194310 parseListObjectsResponse $ NC.responseBody resp
195311 where
196− params = [
197− ("list-type", Just "2")
198− , ("continuation_token", nextToken)
199− , ("prefix", prefix)
200− , ("delimiter", delimiter)
201− , ("max-keys", show <$> maxKeys)
312+ params =
313+ [ ("list-type", Just "2"),
314+ ("continuation_token", nextToken),
315+ ("prefix", prefix),
316+ ("delimiter", delimiter),
317+ ("max-keys", show <$> maxKeys)
202318 ]
203319
204320-- | DELETE a bucket from the service.
205321deleteBucket :: Bucket -> Minio ()
206−deleteBucket bucket = void $
207− executeRequest $
208− def { riMethod = HT.methodDelete
209− , riBucket = Just bucket
322+deleteBucket bucket =
323+ void $
324+ executeRequest $
325+ defaultS3ReqInfo
326+ { riMethod = HT.methodDelete,
327+ riBucket = Just bucket
210328 }
211329
212330-- | DELETE an object from the service.
213331deleteObject :: Bucket -> Object -> Minio ()
214−deleteObject bucket object = void $
215− executeRequest $
216− def { riMethod = HT.methodDelete
217− , riBucket = Just bucket
218− , riObject = Just object
332+deleteObject bucket object =
333+ void $
334+ executeRequest $
335+ defaultS3ReqInfo
336+ { riMethod = HT.methodDelete,
337+ riBucket = Just bucket,
338+ riObject = Just object
219339 }
220340
221341-- | Create a new multipart upload.
222342newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
223343newMultipartUpload bucket object headers = do
224− resp <- executeRequest $ def { riMethod = HT.methodPost
225− , riBucket = Just bucket
226− , riObject = Just object
227− , riQueryParams = [("uploads", Nothing)]
228− , riHeaders = headers
229− }
344+ resp <-
345+ executeRequest $
346+ defaultS3ReqInfo
347+ { riMethod = HT.methodPost,
348+ riBucket = Just bucket,
349+ riObject = Just object,
350+ riQueryParams = [("uploads", Nothing)],
351+ riHeaders = headers
352+ }
230353 parseNewMultipartUpload $ NC.responseBody resp
231354
232355-- | PUT a part of an object as part of a multipart upload.
233−putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
234− -> Payload -> Minio PartTuple
356+putObjectPart ::
357+ Bucket ->
358+ Object ->
359+ UploadId ->
360+ PartNumber ->
361+ [HT.Header] ->
362+ Payload ->
363+ Minio PartTuple
235364putObjectPart bucket object uploadId partNumber headers payload = do
236− resp <- executeRequest $
237− def { riMethod = HT.methodPut
238− , riBucket = Just bucket
239− , riObject = Just object
240− , riQueryParams = mkOptionalParams params
241− , riHeaders = headers
242− , riPayload = payload
243− }
365+ -- transform payload to conduit to enable streaming signature
366+ let payload' = mkStreamingPayload payload
367+ resp <-
368+ executeRequest $
369+ defaultS3ReqInfo
370+ { riMethod = HT.methodPut,
371+ riBucket = Just bucket,
372+ riObject = Just object,
373+ riQueryParams = mkOptionalParams params,
374+ riHeaders = headers,
375+ riPayload = payload'
376+ }
244377 let rheaders = NC.responseHeaders resp
245378 etag = getETagHeader rheaders
246379 maybe
247− (throwM MErrVETagHeaderNotFound)
248− (return . (partNumber, )) etag
380+ (throwIO MErrVETagHeaderNotFound)
381+ (return . (partNumber,))
382+ etag
383+ where
384+ params =
385+ [ ("uploadId", Just uploadId),
386+ ("partNumber", Just $ show partNumber)
387+ ]
388+
389+srcInfoToHeaders :: SourceInfo -> [HT.Header]
390+srcInfoToHeaders srcInfo =
391+ ( "x-amz-copy-source",
392+ encodeUtf8 $
393+ T.concat
394+ [ "/",
395+ srcBucket srcInfo,
396+ "/",
397+ srcObject srcInfo
398+ ]
399+ )
400+ : rangeHdr
401+ ++ zip names values
249402 where
250− params = [
251− ("uploadId", Just uploadId)
252− , ("partNumber", Just $ show partNumber)
403+ names =
404+ [ "x-amz-copy-source-if-match",
405+ "x-amz-copy-source-if-none-match",
406+ "x-amz-copy-source-if-unmodified-since",
407+ "x-amz-copy-source-if-modified-since"
253408 ]
409+ values =
410+ mapMaybe
411+ (fmap encodeUtf8 . (srcInfo &))
412+ [ srcIfMatch,
413+ srcIfNoneMatch,
414+ fmap formatRFC1123 . srcIfUnmodifiedSince,
415+ fmap formatRFC1123 . srcIfModifiedSince
416+ ]
417+ rangeHdr =
418+ maybe [] ((\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) . toByteRange) (srcRange srcInfo)
419+ toByteRange :: (Int64, Int64) -> HT.ByteRange
420+ toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
254421
255422-- | Performs server-side copy of an object or part of an object as an
256423-- upload part of an ongoing multi-part upload.
257−copyObjectPart :: Bucket -> Object -> CopyPartSource -> UploadId
258− -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
259−copyObjectPart bucket object cps uploadId partNumber headers = do
260− resp <- executeRequest $
261− def { riMethod = HT.methodPut
262− , riBucket = Just bucket
263− , riObject = Just object
264− , riQueryParams = mkOptionalParams params
265− , riHeaders = headers ++ cpsToHeaders cps
266− }
424+copyObjectPart ::
425+ DestinationInfo ->
426+ SourceInfo ->
427+ UploadId ->
428+ PartNumber ->
429+ [HT.Header] ->
430+ Minio (ETag, UTCTime)
431+copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
432+ resp <-
433+ executeRequest $
434+ defaultS3ReqInfo
435+ { riMethod = HT.methodPut,
436+ riBucket = Just $ dstBucket dstInfo,
437+ riObject = Just $ dstObject dstInfo,
438+ riQueryParams = mkOptionalParams params,
439+ riHeaders = headers ++ srcInfoToHeaders srcInfo
440+ }
267441
268442 parseCopyObjectResponse $ NC.responseBody resp
269443 where
270− params = [
271− ("uploadId", Just uploadId)
272− , ("partNumber", Just $ show partNumber)
444+ params =
445+ [ ("uploadId", Just uploadId),
446+ ("partNumber", Just $ show partNumber)
273447 ]
274448
275449-- | Performs server-side copy of an object that is upto 5GiB in
276450-- size. If the object is greater than 5GiB, this function throws the
277451-- error returned by the server.
278−copyObjectSingle :: Bucket -> Object -> CopyPartSource -> [HT.Header]
279− -> Minio (ETag, UTCTime)
280−copyObjectSingle bucket object cps headers = do
281− -- validate that cpSourceRange is Nothing for this API.
282− when (isJust $ cpSourceRange cps) $
283− throwM MErrVCopyObjSingleNoRangeAccepted
284− resp <- executeRequest $
285− def { riMethod = HT.methodPut
286− , riBucket = Just bucket
287− , riObject = Just object
288− , riHeaders = headers ++ cpsToHeaders cps
289− }
452+copyObjectSingle ::
453+ Bucket ->
454+ Object ->
455+ SourceInfo ->
456+ [HT.Header] ->
457+ Minio (ETag, UTCTime)
458+copyObjectSingle bucket object srcInfo headers = do
459+ -- validate that srcRange is Nothing for this API.
460+ when (isJust $ srcRange srcInfo) $
461+ throwIO MErrVCopyObjSingleNoRangeAccepted
462+ resp <-
463+ executeRequest $
464+ defaultS3ReqInfo
465+ { riMethod = HT.methodPut,
466+ riBucket = Just bucket,
467+ riObject = Just object,
468+ riHeaders = headers ++ srcInfoToHeaders srcInfo
469+ }
290470 parseCopyObjectResponse $ NC.responseBody resp
291471
292472-- | Complete a multipart upload.
293−completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
294− -> Minio ETag
473+completeMultipartUpload ::
474+ Bucket ->
475+ Object ->
476+ UploadId ->
477+ [PartTuple] ->
478+ Minio ETag
295479completeMultipartUpload bucket object uploadId partTuple = do
296− resp <- executeRequest $
297− def { riMethod = HT.methodPost
298− , riBucket = Just bucket
299− , riObject = Just object
300− , riQueryParams = mkOptionalParams params
301− , riPayload = PayloadBS $
302− mkCompleteMultipartUploadRequest partTuple
303− }
480+ resp <-
481+ executeRequest $
482+ defaultS3ReqInfo
483+ { riMethod = HT.methodPost,
484+ riBucket = Just bucket,
485+ riObject = Just object,
486+ riQueryParams = mkOptionalParams params,
487+ riPayload =
488+ PayloadBS $
489+ mkCompleteMultipartUploadRequest partTuple
490+ }
304491 parseCompleteMultipartUploadResponse $ NC.responseBody resp
305492 where
306493 params = [("uploadId", Just uploadId)]
307494
308495-- | Abort a multipart upload.
309496abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
310−abortMultipartUpload bucket object uploadId = void $
311− executeRequest $ def { riMethod = HT.methodDelete
312− , riBucket = Just bucket
313− , riObject = Just object
314− , riQueryParams = mkOptionalParams params
315− }
497+abortMultipartUpload bucket object uploadId =
498+ void $
499+ executeRequest $
500+ defaultS3ReqInfo
501+ { riMethod = HT.methodDelete,
502+ riBucket = Just bucket,
503+ riObject = Just object,
504+ riQueryParams = mkOptionalParams params
505+ }
316506 where
317507 params = [("uploadId", Just uploadId)]
318508
319509-- | List incomplete multipart uploads.
320−listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
321− -> Maybe Text -> Maybe Int -> Minio ListUploadsResult
510+listIncompleteUploads' ::
511+ Bucket ->
512+ Maybe Text ->
513+ Maybe Text ->
514+ Maybe Text ->
515+ Maybe Text ->
516+ Maybe Int ->
517+ Minio ListUploadsResult
322518listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
323− resp <- executeRequest $ def { riMethod = HT.methodGet
324− , riBucket = Just bucket
325− , riQueryParams = params
326− }
519+ resp <-
520+ executeRequest $
521+ defaultS3ReqInfo
522+ { riMethod = HT.methodGet,
523+ riBucket = Just bucket,
524+ riQueryParams = params
525+ }
327526 parseListUploadsResponse $ NC.responseBody resp
328527 where
329528 -- build query params
330− params = ("uploads", Nothing) : mkOptionalParams
331− [ ("prefix", prefix)
332− , ("delimiter", delimiter)
333− , ("key-marker", keyMarker)
334− , ("upload-id-marker", uploadIdMarker)
335− , ("max-uploads", show <$> maxKeys)
336− ]
337−
529+ params =
530+ ("uploads", Nothing)
531+ : mkOptionalParams
532+ [ ("prefix", prefix),
533+ ("delimiter", delimiter),
534+ ("key-marker", keyMarker),
535+ ("upload-id-marker", uploadIdMarker),
536+ ("max-uploads", show <$> maxKeys)
537+ ]
338538
339539-- | List parts of an ongoing multipart upload.
340−listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
341− -> Maybe Text -> Minio ListPartsResult
540+listIncompleteParts' ::
541+ Bucket ->
542+ Object ->
543+ UploadId ->
544+ Maybe Text ->
545+ Maybe Text ->
546+ Minio ListPartsResult
342547listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
343− resp <- executeRequest $ def { riMethod = HT.methodGet
344− , riBucket = Just bucket
345− , riObject = Just object
346− , riQueryParams = mkOptionalParams params
347− }
548+ resp <-
549+ executeRequest $
550+ defaultS3ReqInfo
551+ { riMethod = HT.methodGet,
552+ riBucket = Just bucket,
553+ riObject = Just object,
554+ riQueryParams = mkOptionalParams params
555+ }
348556 parseListPartsResponse $ NC.responseBody resp
349557 where
350558 -- build optional query params
351− params = [
352− ("uploadId", Just uploadId)
353− , ("part-number-marker", partNumMarker)
354− , ("max-parts", maxParts)
559+ params =
560+ [ ("uploadId", Just uploadId),
561+ ("part-number-marker", partNumMarker),
562+ ("max-parts", maxParts)
355563 ]
356564
357565-- | Get metadata of an object.
358−headObject :: Bucket -> Object -> Minio ObjectInfo
359−headObject bucket object = do
360− resp <- executeRequest $ def { riMethod = HT.methodHead
361− , riBucket = Just bucket
362− , riObject = Just object
363− }
364−
365− let
366− headers = NC.responseHeaders resp
367− modTime = getLastModifiedHeader headers
368− etag = getETagHeader headers
369− size = getContentLength headers
370−
371− maybe (throwM MErrVInvalidObjectInfoResponse) return $
372− ObjectInfo <$> Just object <*> modTime <*> etag <*> size
373−
374−
566+headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
567+headObject bucket object reqHeaders = do
568+ resp <-
569+ executeRequest $
570+ defaultS3ReqInfo
571+ { riMethod = HT.methodHead,
572+ riBucket = Just bucket,
573+ riObject = Just object,
574+ riHeaders =
575+ reqHeaders
576+ -- This header is required for safety as otherwise http-client,
577+ -- sends Accept-Encoding: gzip, and the server may actually gzip
578+ -- body. In that case Content-Length header will be missing.
579+ <> [("Accept-Encoding", "identity")]
580+ }
581+ maybe (throwIO MErrVInvalidObjectInfoResponse) return $
582+ parseGetObjectHeaders object $
583+ NC.responseHeaders resp
375584
376585-- | Query the object store if a given bucket exists.
377586headBucket :: Bucket -> Minio Bool
378−headBucket bucket = headBucketEx `catches`
379− [ Handler handleNoSuchBucket
380− , Handler handleStatus404
381− ]
382−
587+headBucket bucket =
588+ headBucketEx
589+ `catches` [ Handler handleNoSuchBucket,
590+ Handler handleStatus404
591+ ]
383592 where
384593 handleNoSuchBucket :: ServiceErr -> Minio Bool
385− handleNoSuchBucket e | e == NoSuchBucket = return False
386− | otherwise = throwM e
387−
594+ handleNoSuchBucket e
595+ | e == NoSuchBucket = return False
596+ | otherwise = throwIO e
388597 handleStatus404 :: NC.HttpException -> Minio Bool
389598 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
390599 if NC.responseStatus res == status404
391− then return False
392− else throwM e
393− handleStatus404 e = throwM e
394−
600+ then return False
601+ else throwIO e
602+ handleStatus404 e = throwIO e
395603 headBucketEx = do
396− resp <- executeRequest $ def { riMethod = HT.methodHead
397− , riBucket = Just bucket
398− }
604+ resp <-
605+ executeRequest $
606+ defaultS3ReqInfo
607+ { riMethod = HT.methodHead,
608+ riBucket = Just bucket
609+ }
399610 return $ NC.responseStatus resp == HT.ok200
400611
401612-- | Set the notification configuration on a bucket.
402613putBucketNotification :: Bucket -> Notification -> Minio ()
403−putBucketNotification bucket ncfg =
404− void $ executeRequest $ def { riMethod = HT.methodPut
405− , riBucket = Just bucket
406− , riQueryParams = [("notification", Nothing)]
407− , riPayload = PayloadBS $
408− mkPutNotificationRequest ncfg
409− }
614+putBucketNotification bucket ncfg = do
615+ ns <- asks getSvcNamespace
616+ void $
617+ executeRequest $
618+ defaultS3ReqInfo
619+ { riMethod = HT.methodPut,
620+ riBucket = Just bucket,
621+ riQueryParams = [("notification", Nothing)],
622+ riPayload =
623+ PayloadBS $
624+ mkPutNotificationRequest ns ncfg
625+ }
410626
411627-- | Retrieve the notification configuration on a bucket.
412628getBucketNotification :: Bucket -> Minio Notification
413629getBucketNotification bucket = do
414− resp <- executeRequest $ def { riMethod = HT.methodGet
415− , riBucket = Just bucket
416− , riQueryParams = [("notification", Nothing)]
417− }
630+ resp <-
631+ executeRequest $
632+ defaultS3ReqInfo
633+ { riMethod = HT.methodGet,
634+ riBucket = Just bucket,
635+ riQueryParams = [("notification", Nothing)]
636+ }
418637 parseNotification $ NC.responseBody resp
419638
420639-- | Remove all notifications configured on a bucket.
421640removeAllBucketNotification :: Bucket -> Minio ()
422−removeAllBucketNotification = flip putBucketNotification def
641+removeAllBucketNotification = flip putBucketNotification defaultNotification
642+
643+-- | Fetch the policy if any on a bucket.
644+getBucketPolicy :: Bucket -> Minio Text
645+getBucketPolicy bucket = do
646+ resp <-
647+ executeRequest $
648+ defaultS3ReqInfo
649+ { riMethod = HT.methodGet,
650+ riBucket = Just bucket,
651+ riQueryParams = [("policy", Nothing)]
652+ }
653+ return $ decodeUtf8Lenient $ toStrictBS $ NC.responseBody resp
654+
655+-- | Set a new policy on a bucket.
656+-- As a special condition if the policy is empty
657+-- then we treat it as policy DELETE operation.
658+setBucketPolicy :: Bucket -> Text -> Minio ()
659+setBucketPolicy bucket policy = do
660+ if T.null policy
661+ then deleteBucketPolicy bucket
662+ else putBucketPolicy bucket policy
663+
664+-- | Save a new policy on a bucket.
665+putBucketPolicy :: Bucket -> Text -> Minio ()
666+putBucketPolicy bucket policy = do
667+ void $
668+ executeRequest $
669+ defaultS3ReqInfo
670+ { riMethod = HT.methodPut,
671+ riBucket = Just bucket,
672+ riQueryParams = [("policy", Nothing)],
673+ riPayload = PayloadBS $ encodeUtf8 policy
674+ }
675+
676+-- | Delete any policy set on a bucket.
677+deleteBucketPolicy :: Bucket -> Minio ()
678+deleteBucketPolicy bucket = do
679+ void $
680+ executeRequest $
681+ defaultS3ReqInfo
682+ { riMethod = HT.methodDelete,
683+ riBucket = Just bucket,
684+ riQueryParams = [("policy", Nothing)]
685+ }
src/Network/Minio/SelectAPI.hsnew file+294
@@ -0,0 +1,294 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2019 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.SelectAPI
18+ ( -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
19+ -- format objects in AWS S3 and in MinIO using SQL Select
20+ -- statements. This allows significant reduction of data transfer
21+ -- from object storage for computation-intensive tasks, as relevant
22+ -- data is filtered close to the storage.
23+ selectObjectContent,
24+ SelectRequest,
25+ selectRequest,
26+
27+ -- *** Input Serialization
28+ InputSerialization,
29+ defaultCsvInput,
30+ linesJsonInput,
31+ documentJsonInput,
32+ defaultParquetInput,
33+ setInputCSVProps,
34+ CompressionType (..),
35+ setInputCompressionType,
36+
37+ -- *** CSV Format details
38+
39+ -- | CSV format options such as delimiters and quote characters are
40+ -- specified using using the functions below. Options are combined
41+ -- monoidally.
42+ CSVProp,
43+ recordDelimiter,
44+ fieldDelimiter,
45+ quoteCharacter,
46+ quoteEscapeCharacter,
47+ commentCharacter,
48+ allowQuotedRecordDelimiter,
49+ FileHeaderInfo (..),
50+ fileHeaderInfo,
51+ QuoteFields (..),
52+ quoteFields,
53+
54+ -- *** Output Serialization
55+ OutputSerialization,
56+ defaultCsvOutput,
57+ defaultJsonOutput,
58+ outputCSVFromProps,
59+ outputJSONFromRecordDelimiter,
60+
61+ -- *** Progress messages
62+ setRequestProgressEnabled,
63+
64+ -- *** Interpreting Select output
65+
66+ -- | The conduit returned by `selectObjectContent` returns values of
67+ -- the `EventMessage` data type. This returns the query output
68+ -- messages formatted according to the chosen output serialization,
69+ -- interleaved with progress messages (if enabled by
70+ -- `setRequestProgressEnabled`), and at the end a statistics
71+ -- message.
72+ --
73+ -- If the application is interested in only the payload, then
74+ -- `getPayloadBytes` can be used. For example to simply print the
75+ -- payload to stdout:
76+ --
77+ -- > resultConduit <- selectObjectContent bucket object mySelectRequest
78+ -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
79+ --
80+ -- Note that runConduit, the connect operator (.|) and stdoutC are
81+ -- all from the "conduit" package.
82+ getPayloadBytes,
83+ EventMessage (..),
84+ Progress (..),
85+ Stats,
86+ )
87+where
88+
89+import Conduit ((.|))
90+import qualified Conduit as C
91+import qualified Data.Binary as Bin
92+import qualified Data.ByteString as B
93+import qualified Data.ByteString.Lazy as LB
94+import Data.Digest.CRC32 (crc32, crc32Update)
95+import Lib.Prelude
96+import qualified Network.HTTP.Conduit as NC
97+import qualified Network.HTTP.Types as HT
98+import Network.Minio.API
99+import Network.Minio.Data
100+import Network.Minio.Errors
101+import Network.Minio.Utils
102+import Network.Minio.XmlGenerator
103+import Network.Minio.XmlParser
104+import UnliftIO (MonadUnliftIO)
105+
106+data EventStreamException
107+ = ESEPreludeCRCFailed
108+ | ESEMessageCRCFailed
109+ | ESEUnexpectedEndOfStream
110+ | ESEDecodeFail [Char]
111+ | ESEInvalidHeaderType
112+ | ESEInvalidHeaderValueType
113+ | ESEInvalidMessageType
114+ deriving stock (Eq, Show)
115+
116+instance Exception EventStreamException
117+
118+-- chunkSize in bytes is 32KiB
119+chunkSize :: Int
120+chunkSize = 32 * 1024
121+
122+parseBinary :: (Bin.Binary a) => ByteString -> IO a
123+parseBinary b = do
124+ case Bin.decodeOrFail $ LB.fromStrict b of
125+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
126+ Right (_, _, r) -> return r
127+
128+bytesToHeaderName :: Text -> IO MsgHeaderName
129+bytesToHeaderName t = case t of
130+ ":message-type" -> return MessageType
131+ ":event-type" -> return EventType
132+ ":content-type" -> return ContentType
133+ ":error-code" -> return ErrorCode
134+ ":error-message" -> return ErrorMessage
135+ _ -> throwIO ESEInvalidHeaderType
136+
137+parseHeaders ::
138+ (MonadUnliftIO m) =>
139+ Word32 ->
140+ C.ConduitM ByteString a m [MessageHeader]
141+parseHeaders 0 = return []
142+parseHeaders hdrLen = do
143+ bs1 <- readNBytes 1
144+ n :: Word8 <- liftIO $ parseBinary bs1
145+
146+ headerKeyBytes <- readNBytes $ fromIntegral n
147+ let headerKey = decodeUtf8Lenient headerKeyBytes
148+ headerName <- liftIO $ bytesToHeaderName headerKey
149+
150+ bs2 <- readNBytes 1
151+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
152+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
153+
154+ bs3 <- readNBytes 2
155+ vLen :: Word16 <- liftIO $ parseBinary bs3
156+ headerValueBytes <- readNBytes $ fromIntegral vLen
157+ let headerValue = decodeUtf8Lenient headerValueBytes
158+ m = (headerName, headerValue)
159+ k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
160+
161+ ms <- parseHeaders (hdrLen - k)
162+ return (m : ms)
163+
164+-- readNBytes returns N bytes read from the string and throws an
165+-- exception if N bytes are not present on the stream.
166+readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
167+readNBytes n = do
168+ b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
169+ if B.length b /= n
170+ then throwIO ESEUnexpectedEndOfStream
171+ else return b
172+
173+crcCheck ::
174+ (MonadUnliftIO m) =>
175+ C.ConduitM ByteString ByteString m ()
176+crcCheck = do
177+ b <- readNBytes 12
178+ n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
179+ preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
180+ when (crc32 (B.take 8 b) /= preludeCRC) $
181+ throwIO ESEPreludeCRCFailed
182+
183+ -- we do not yield the checksum
184+ C.yield $ B.take 8 b
185+
186+ -- 12 bytes have been read off the current message. Now read the
187+ -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
188+ let startCrc = crc32 b
189+ finalCrc <- accumulateYield (fromIntegral n - 16) startCrc
190+
191+ bs <- readNBytes 4
192+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
193+
194+ when (finalCrc /= expectedCrc) $
195+ throwIO ESEMessageCRCFailed
196+
197+ -- we unconditionally recurse - downstream figures out when to
198+ -- quit reading the stream
199+ crcCheck
200+ where
201+ accumulateYield n checkSum = do
202+ let toRead = min n chunkSize
203+ b <- readNBytes toRead
204+ let c' = crc32Update checkSum b
205+ n' = n - B.length b
206+ C.yield b
207+ if n' > 0
208+ then accumulateYield n' c'
209+ else return c'
210+
211+handleMessage :: (MonadUnliftIO m) => C.ConduitT ByteString EventMessage m ()
212+handleMessage = do
213+ b1 <- readNBytes 4
214+ msgLen :: Word32 <- liftIO $ parseBinary b1
215+
216+ b2 <- readNBytes 4
217+ hdrLen :: Word32 <- liftIO $ parseBinary b2
218+
219+ hs <- parseHeaders hdrLen
220+
221+ let payloadLen = msgLen - hdrLen - 16
222+ getHdrVal h = fmap snd . find ((h ==) . fst)
223+ eventHdrValue = getHdrVal EventType hs
224+ msgHdrValue = getHdrVal MessageType hs
225+ errCode = getHdrVal ErrorCode hs
226+ errMsg = getHdrVal ErrorMessage hs
227+
228+ case msgHdrValue of
229+ Just "event" -> do
230+ case eventHdrValue of
231+ Just "Records" -> passThrough $ fromIntegral payloadLen
232+ Just "Cont" -> return ()
233+ Just "Progress" -> do
234+ bs <- readNBytes $ fromIntegral payloadLen
235+ progress <- parseSelectProgress bs
236+ C.yield $ ProgressEventMessage progress
237+ Just "Stats" -> do
238+ bs <- readNBytes $ fromIntegral payloadLen
239+ stats <- parseSelectProgress bs
240+ C.yield $ StatsEventMessage stats
241+ Just "End" -> return ()
242+ _ -> throwIO ESEInvalidMessageType
243+ when (eventHdrValue /= Just "End") handleMessage
244+ Just "error" -> do
245+ let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
246+ maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
247+ _ -> throwIO ESEInvalidMessageType
248+ where
249+ passThrough 0 = return ()
250+ passThrough n = do
251+ let c = min n chunkSize
252+ b <- readNBytes c
253+ C.yield $ RecordPayloadEventMessage b
254+ passThrough $ n - B.length b
255+
256+selectProtoConduit ::
257+ (MonadUnliftIO m) =>
258+ C.ConduitT ByteString EventMessage m ()
259+selectProtoConduit = crcCheck .| handleMessage
260+
261+-- | selectObjectContent calls the SelectRequest on the given
262+-- object. It returns a Conduit of event messages that can be consumed
263+-- by the client.
264+selectObjectContent ::
265+ Bucket ->
266+ Object ->
267+ SelectRequest ->
268+ Minio (C.ConduitT () EventMessage Minio ())
269+selectObjectContent b o r = do
270+ let reqInfo =
271+ defaultS3ReqInfo
272+ { riMethod = HT.methodPost,
273+ riBucket = Just b,
274+ riObject = Just o,
275+ riPayload = PayloadBS $ mkSelectRequest r,
276+ riNeedsLocation = False,
277+ riQueryParams = [("select", Nothing), ("select-type", Just "2")]
278+ }
279+ -- print $ mkSelectRequest r
280+ resp <- mkStreamRequest reqInfo
281+ return $ NC.responseBody resp .| selectProtoConduit
282+
283+-- | A helper conduit that returns only the record payload bytes.
284+getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
285+getPayloadBytes = do
286+ evM <- C.await
287+ case evM of
288+ Just v -> do
289+ case v of
290+ RecordPayloadEventMessage b -> C.yield b
291+ RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
292+ _ -> return ()
293+ getPayloadBytes
294+ Nothing -> return ()
src/Network/Minio/Sign/V4.hs+446−199
@@ -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,219 +13,313 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16+{-# LANGUAGE BangPatterns #-}
1617
1718module Network.Minio.Sign.V4
18− (
19− signV4
20− , signV4AtTime
21− , signV4PostPolicy
22− , mkScope
23− , getHeadersToSign
24− , mkCanonicalRequest
25− , mkStringToSign
26− , mkSigningKey
27− , computeSignature
28− , SignV4Data(..)
29− , debugPrintSignV4Data
30− ) where
19+ ( SignParams (..),
20+ signV4QueryParams,
21+ signV4,
22+ signV4PostPolicy,
23+ signV4Stream,
24+ Service (..),
25+ credentialScope,
26+ )
27+where
3128
29+import qualified Conduit as C
30+import qualified Data.ByteArray as BA
3231import qualified Data.ByteString as B
32+import qualified Data.ByteString.Base64 as Base64
3333import qualified Data.ByteString.Char8 as B8
34−import Data.CaseInsensitive (mk)
34+import qualified Data.ByteString.Lazy as LB
35+import Data.CaseInsensitive (mk)
3536import qualified Data.CaseInsensitive as CI
36−import qualified Data.Set as Set
37+import qualified Data.HashMap.Strict as Map
38+import qualified Data.HashSet as Set
39+import Data.List (partition)
40+import qualified Data.List.NonEmpty as NE
3741import qualified Data.Time as Time
38−import qualified Data.ByteString.Base64 as Base64
39−import qualified Data.Map.Strict as Map
40−import Network.HTTP.Types (Header)
41−import qualified Network.HTTP.Types.Header as H
42−
43−import Lib.Prelude
44−import Network.Minio.Data
45−import Network.Minio.Data.ByteString
46−import Network.Minio.Data.Crypto
47−import Network.Minio.Data.Time
42+import Lib.Prelude
43+import qualified Network.HTTP.Conduit as NC
44+import Network.HTTP.Types (Header, SimpleQuery, hContentEncoding, parseQuery)
45+import qualified Network.HTTP.Types as H
46+import Network.HTTP.Types.Header (RequestHeaders)
47+import Network.Minio.Data.ByteString
48+import Network.Minio.Data.Crypto
49+import Network.Minio.Data.Time
50+import Network.Minio.Errors
51+import Text.Printf (printf)
4852
4953-- these headers are not included in the string to sign when signing a
5054-- request
51−ignoredHeaders :: Set ByteString
52−ignoredHeaders = Set.fromList $ map CI.foldedCase
53− [ H.hAuthorization
54− , H.hContentType
55− , H.hContentLength
56− , H.hUserAgent
57− ]
58−
59−data SignV4Data = SignV4Data {
60− sv4SignTime :: UTCTime
61− , sv4Scope :: ByteString
62− , sv4CanonicalRequest :: ByteString
63− , sv4HeadersToSign :: [(ByteString, ByteString)]
64− , sv4Output :: [(ByteString, ByteString)]
65− , sv4StringToSign :: ByteString
66− , sv4SigningKey :: ByteString
67− } deriving (Show)
68−
69−debugPrintSignV4Data :: SignV4Data -> IO ()
70−debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
71− B8.putStrLn "SignV4Data:"
72− B8.putStr "Timestamp: " >> print t
73− B8.putStr "Scope: " >> B8.putStrLn s
74− B8.putStrLn "Canonical Request:"
75− B8.putStrLn cr
76− B8.putStr "Headers to Sign: " >> print h2s
77− B8.putStr "Output: " >> print o
78− B8.putStr "StringToSign: " >> B8.putStrLn sts
79− B8.putStr "SigningKey: " >> printBytes sk
80− B8.putStrLn "END of SignV4Data ========="
81− where
82− printBytes b = do
83− mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b
84− B8.putStrLn ""
85−
86−-- | Given MinioClient and request details, including request method,
55+ignoredHeaders :: Set.HashSet ByteString
56+ignoredHeaders =
57+ Set.fromList $
58+ map
59+ CI.foldedCase
60+ [ H.hAuthorization,
61+ H.hContentType,
62+ H.hUserAgent
63+ ]
64+
65+data Service = ServiceS3 | ServiceSTS
66+ deriving stock (Eq, Show)
67+
68+toByteString :: Service -> ByteString
69+toByteString ServiceS3 = "s3"
70+toByteString ServiceSTS = "sts"
71+
72+data SignParams = SignParams
73+ { spAccessKey :: Text,
74+ spSecretKey :: BA.ScrubbedBytes,
75+ spSessionToken :: Maybe BA.ScrubbedBytes,
76+ spService :: Service,
77+ spTimeStamp :: UTCTime,
78+ spRegion :: Maybe Text,
79+ spExpirySecs :: Maybe UrlExpiry,
80+ spPayloadHash :: Maybe ByteString
81+ }
82+ deriving stock (Show)
83+
84+mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
85+mkAuthHeader accessKey scope signedHeaderKeys sign =
86+ let authValue =
87+ B.concat
88+ [ "AWS4-HMAC-SHA256 Credential=",
89+ encodeUtf8 accessKey,
90+ "/",
91+ scope,
92+ ", SignedHeaders=",
93+ signedHeaderKeys,
94+ ", Signature=",
95+ sign
96+ ]
97+ in (H.hAuthorization, authValue)
98+
99+data IsStreaming = IsStreamingLength Int64 | NotStreaming
100+ deriving stock (Eq, Show)
101+
102+amzSecurityToken :: ByteString
103+amzSecurityToken = "X-Amz-Security-Token"
104+
105+-- | Given SignParams and request details, including request method,
87106-- request path, headers, query params and payload hash, generates an
88107-- updated set of headers, including the x-amz-date header and the
89108-- Authorization header, which includes the signature.
90−signV4 :: ConnectInfo -> RequestInfo -> Maybe Int
91− -> IO [(ByteString, ByteString)]
92−signV4 !ci !ri !expiry = do
93− timestamp <- Time.getCurrentTime
94− let signData = signV4AtTime timestamp ci ri expiry
95− -- debugPrintSignV4Data signData
96− return $ sv4Output signData
97−
98−-- | Takes a timestamp, server params and request params and generates
99−-- AWS Sign V4 data. For normal requests (i.e. without an expiry
100−-- time), the output is the list of headers to add to authenticate the
101−-- request.
109+--
110+-- For normal requests (i.e. without an expiry time), the output is
111+-- the list of headers to add to authenticate the request.
102112--
103113-- If `expiry` is not Nothing, it is assumed that a presigned request
104114-- is being created. The expiry is interpreted as an integer number of
105115-- seconds. The output will be the list of query-parameters to add to
106116-- the request.
107−signV4AtTime :: UTCTime -> ConnectInfo -> RequestInfo -> Maybe Int
108− -> SignV4Data
109−signV4AtTime ts ci ri expiry =
110− let
111− region = maybe (connectRegion ci) identity $ riRegion ri
112− scope = mkScope ts region
113− accessKey = toS $ connectAccessKey ci
114− secretKey = toS $ connectSecretKey ci
115−
116− -- headers to be added to the request
117− datePair = ("X-Amz-Date", awsTimeFormatBS ts)
118− computedHeaders = riHeaders ri ++
119− if isJust expiry
120− then []
121− else [(\(x, y) -> (mk x, y)) datePair]
122− headersToSign = getHeadersToSign computedHeaders
123− signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
124−
125− -- query-parameters to be added before signing for presigned URLs
126− -- (i.e. when `isJust expiry`)
127− authQP = [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
128− , ("X-Amz-Credential", B.concat [accessKey, "/", scope])
129− , datePair
130− , ("X-Amz-Expires", maybe "" show expiry)
131− , ("X-Amz-SignedHeaders", signedHeaderKeys)
132− ]
133− finalQP = riQueryParams ri ++
134− if isJust expiry
135− then (fmap . fmap) Just authQP
136− else []
137−
138− -- 1. compute canonical request
139− canonicalRequest = mkCanonicalRequest (ri {riQueryParams = finalQP})
140− headersToSign
141−
142− -- 2. compute string to sign
143− stringToSign = mkStringToSign ts scope canonicalRequest
144−
145− -- 3.1 compute signing key
146− signingKey = mkSigningKey ts region secretKey
147−
148− -- 3.2 compute signature
149− signature = computeSignature stringToSign signingKey
150−
151− -- 4. compute auth header
152− authValue = B.concat
153− [ "AWS4-HMAC-SHA256 Credential="
154− , accessKey
155− , "/"
156− , scope
157− , ", SignedHeaders="
158− , signedHeaderKeys
159− , ", Signature="
160− , signature
161− ]
162− authHeader = (H.hAuthorization, authValue)
117+signV4QueryParams :: SignParams -> NC.Request -> SimpleQuery
118+signV4QueryParams !sp !req =
119+ let scope = credentialScope sp
120+ expiry = spExpirySecs sp
121+
122+ headersToSign = getHeadersToSign $ NC.requestHeaders req
123+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
124+ -- query-parameters to be added before signing for presigned URLs
125+ -- (i.e. when `isJust expiry`)
126+ authQP =
127+ [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
128+ ("X-Amz-Credential", B.concat [encodeUtf8 $ spAccessKey sp, "/", scope]),
129+ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
130+ ("X-Amz-Expires", maybe "" showBS expiry),
131+ ("X-Amz-SignedHeaders", signedHeaderKeys)
132+ ]
133+ ++ maybeToList ((amzSecurityToken,) . BA.convert <$> spSessionToken sp)
134+ finalQP =
135+ parseQuery (NC.queryString req)
136+ ++ if isJust expiry
137+ then (fmap . fmap) Just authQP
138+ else []
139+ -- 1. compute canonical request
140+ canonicalRequest =
141+ mkCanonicalRequest
142+ False
143+ sp
144+ (NC.setQueryString finalQP req)
145+ headersToSign
146+
147+ -- 2. compute string to sign
148+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
149+ -- 3.1 compute signing key
150+ signingKey = getSigningKey sp
151+ -- 3.2 compute signature
152+ signature = computeSignature stringToSign signingKey
153+ in ("X-Amz-Signature", signature) : authQP
163154
164− -- finally compute output pairs
165− output = if isJust expiry
166− then ("X-Amz-Signature", signature) : authQP
167− else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
168− datePair]
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)
169176
170− in
171− SignV4Data ts scope canonicalRequest headersToSign output
172− stringToSign signingKey
177+ -- 1. compute canonical request
178+ reqHeaders = NC.requestHeaders req ++ extraHeaders
179+ (canonicalRequest, signedHeaderKeys) =
180+ getCanonicalRequestAndSignedHeaders
181+ NotStreaming
182+ sp
183+ req
184+ reqHeaders
173185
186+ -- 2. compute string to sign
187+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
188+ -- 3.1 compute signing key
189+ signingKey = getSigningKey sp
190+ -- 3.2 compute signature
191+ signature = computeSignature stringToSign signingKey
192+ -- 4. compute auth header
193+ authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
194+ in authHeader : extraHeaders
174195
175−mkScope :: UTCTime -> Region -> ByteString
176−mkScope ts region = B.intercalate "/"
177− [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts
178− , toS region
179− , "s3"
180− , "aws4_request"
181− ]
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+ ]
182206
207+-- Folds header name, trims whitespace in header values, skips ignored headers
208+-- and sorts headers.
183209getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
184210getHeadersToSign !h =
185− filter (flip Set.notMember ignoredHeaders . fst) $
186− map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
187−
188−mkCanonicalRequest :: RequestInfo -> [(ByteString, ByteString)]
189− -> ByteString
190−mkCanonicalRequest !ri !headersForSign =
191− let
192− canonicalQueryString = B.intercalate "&" $
193− map (\(x, y) -> B.concat [x, "=", y]) $
194− sort $ map (\(x, y) ->
195− (uriEncode True x, maybe "" (uriEncode True) y)) $
196− riQueryParams ri
197−
198− sortedHeaders = sort headersForSign
199−
200− canonicalHeaders = B.concat $
201− map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
202−
203− signedHeaders = B.intercalate ";" $ map fst sortedHeaders
204−
205− in
206− B.intercalate "\n"
207− [ riMethod ri
208− , uriEncode False $ getPathFromRI ri
209− , canonicalQueryString
210− , canonicalHeaders
211− , signedHeaders
212− , maybe "UNSIGNED-PAYLOAD" identity $ riPayloadHash ri
213− ]
211+ filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
212+ map (bimap CI.foldedCase stripBS) h
213+
214+-- | Given the list of headers in the request, computes the canonical headers
215+-- and the signed headers strings.
216+getCanonicalHeaders :: NonEmpty Header -> (ByteString, ByteString)
217+getCanonicalHeaders h =
218+ let -- Folds header name, trims spaces in header values, skips ignored
219+ -- headers and sorts headers by name (we must not re-order multi-valued
220+ -- headers).
221+ headersToSign =
222+ NE.toList $
223+ NE.sortBy (\a b -> compare (fst a) (fst b)) $
224+ NE.fromList $
225+ NE.filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
226+ NE.map (bimap CI.foldedCase stripBS) h
227+
228+ canonicalHeaders = mconcat $ map (\(a, b) -> a <> ":" <> b <> "\n") headersToSign
229+ signedHeaderKeys = B.intercalate ";" $ map fst headersToSign
230+ in (canonicalHeaders, signedHeaderKeys)
231+
232+getCanonicalRequestAndSignedHeaders ::
233+ IsStreaming ->
234+ SignParams ->
235+ NC.Request ->
236+ [Header] ->
237+ (ByteString, ByteString)
238+getCanonicalRequestAndSignedHeaders isStreaming sp req requestHeaders =
239+ let httpMethod = NC.method req
240+
241+ canonicalUri = uriEncode False $ NC.path req
242+
243+ canonicalQueryString =
244+ B.intercalate "&" $
245+ map (\(x, y) -> B.concat [x, "=", y]) $
246+ sort $
247+ map
248+ ( bimap (uriEncode True) (maybe "" (uriEncode True))
249+ )
250+ (parseQuery $ NC.queryString req)
251+
252+ (canonicalHeaders, signedHeaderKeys) = getCanonicalHeaders $ NE.fromList requestHeaders
253+ payloadHashStr =
254+ case isStreaming of
255+ IsStreamingLength _ -> "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
256+ NotStreaming -> fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
257+
258+ canonicalRequest =
259+ B.intercalate
260+ "\n"
261+ [ httpMethod,
262+ canonicalUri,
263+ canonicalQueryString,
264+ canonicalHeaders,
265+ signedHeaderKeys,
266+ payloadHashStr
267+ ]
268+ in (canonicalRequest, signedHeaderKeys)
269+
270+mkCanonicalRequest ::
271+ Bool ->
272+ SignParams ->
273+ NC.Request ->
274+ [(ByteString, ByteString)] ->
275+ ByteString
276+mkCanonicalRequest !isStreaming !sp !req !headersForSign =
277+ let httpMethod = NC.method req
278+ canonicalUri = uriEncode False $ NC.path req
279+ canonicalQueryString =
280+ B.intercalate "&" $
281+ map (\(x, y) -> B.concat [x, "=", y]) $
282+ sortBy (\a b -> compare (fst a) (fst b)) $
283+ map
284+ ( bimap (uriEncode True) (maybe "" (uriEncode True))
285+ )
286+ (parseQuery $ NC.queryString req)
287+ sortedHeaders = sort headersForSign
288+ canonicalHeaders =
289+ B.concat $
290+ map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
291+ signedHeaders = B.intercalate ";" $ map fst sortedHeaders
292+ payloadHashStr =
293+ if isStreaming
294+ then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
295+ else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
296+ in B.intercalate
297+ "\n"
298+ [ httpMethod,
299+ canonicalUri,
300+ canonicalQueryString,
301+ canonicalHeaders,
302+ signedHeaders,
303+ payloadHashStr
304+ ]
214305
215306mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
216−mkStringToSign ts !scope !canonicalRequest = B.intercalate "\n"
217− [ "AWS4-HMAC-SHA256"
218− , awsTimeFormatBS ts
219− , scope
220− , hashSHA256 canonicalRequest
221− ]
222−
223−mkSigningKey :: UTCTime -> Region -> ByteString -> ByteString
224−mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"
225− . hmacSHA256RawBS "s3"
226− . hmacSHA256RawBS (toS region)
227− . hmacSHA256RawBS (awsDateFormatBS ts)
228− $ B.concat ["AWS4", secretKey]
307+mkStringToSign ts !scope !canonicalRequest =
308+ B.intercalate
309+ "\n"
310+ [ "AWS4-HMAC-SHA256",
311+ awsTimeFormatBS ts,
312+ scope,
313+ hashSHA256 canonicalRequest
314+ ]
315+
316+getSigningKey :: SignParams -> ByteString
317+getSigningKey sp =
318+ hmacSHA256RawBS "aws4_request"
319+ . hmacSHA256RawBS (toByteString $ spService sp)
320+ . hmacSHA256RawBS (encodeUtf8 $ fromMaybe "" $ spRegion sp)
321+ . hmacSHA256RawBS (awsDateFormatBS $ spTimeStamp sp)
322+ $ B.concat ["AWS4", BA.convert $ spSecretKey sp]
229323
230324computeSignature :: ByteString -> ByteString -> ByteString
231325computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -233,15 +327,168 @@ computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
233327-- | Takes a validated Post Policy JSON bytestring, the signing time,
234328-- and ConnInfo and returns form-data for the POST upload containing
235329-- just the signature and the encoded post-policy.
236−signV4PostPolicy :: ByteString -> UTCTime -> ConnectInfo
237− -> Map.Map Text ByteString
238−signV4PostPolicy !postPolicyJSON !signTime !ci =
239− let
240− stringToSign = Base64.encode postPolicyJSON
241− region = connectRegion ci
242− signingKey = mkSigningKey signTime region $ toS $ connectSecretKey ci
243− signature = computeSignature stringToSign signingKey
244− in
245− Map.fromList [ ("x-amz-signature", signature)
246− , ("policy", stringToSign)
247− ]
330+signV4PostPolicy ::
331+ ByteString ->
332+ SignParams ->
333+ Map.HashMap Text ByteString
334+signV4PostPolicy !postPolicyJSON !sp =
335+ let stringToSign = Base64.encode postPolicyJSON
336+ signingKey = getSigningKey sp
337+ signature = computeSignature stringToSign signingKey
338+ in Map.fromList $
339+ [ ("x-amz-signature", signature),
340+ ("policy", stringToSign)
341+ ]
342+ ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
343+
344+chunkSizeConstant :: Int
345+chunkSizeConstant = 64 * 1024
346+
347+-- base16Len computes the number of bytes required to represent @n (> 0)@ in
348+-- hexadecimal.
349+base16Len :: (Integral a) => a -> Int
350+base16Len n
351+ | n == 0 = 0
352+ | otherwise = 1 + base16Len (n `div` 16)
353+
354+signedStreamLength :: Int64 -> Int64
355+signedStreamLength dataLen =
356+ let chunkSzInt = fromIntegral chunkSizeConstant
357+ (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
358+ -- Structure of a chunk:
359+ -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
360+ encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
361+ fullChunkSize = encodedChunkLen chunkSzInt
362+ lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
363+ finalChunkSize = 1 + 17 + 64 + 2 + 2
364+ in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
365+
366+-- For streaming S3, we need to update the content-encoding header.
367+addContentEncoding :: [Header] -> [Header]
368+addContentEncoding hs =
369+ -- assume there is at most one content-encoding header.
370+ let (ceHdrs, others) = partition ((== hContentEncoding) . fst) hs
371+ in maybe
372+ (hContentEncoding, "aws-chunked")
373+ (\(k, v) -> (k, v <> ",aws-chunked"))
374+ (listToMaybe ceHdrs)
375+ : others
376+
377+signV4Stream ::
378+ Int64 ->
379+ SignParams ->
380+ NC.Request ->
381+ (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
382+signV4Stream !payloadLength !sp !req =
383+ let ts = spTimeStamp sp
384+
385+ -- compute the updated list of headers to be added for signing purposes.
386+ signedContentLength = signedStreamLength payloadLength
387+ extraHeaders =
388+ [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
389+ ("x-amz-decoded-content-length", showBS payloadLength),
390+ ("content-length", showBS signedContentLength),
391+ ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
392+ ]
393+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
394+ requestHeaders =
395+ addContentEncoding $
396+ foldr setHeader (NC.requestHeaders req) extraHeaders
397+
398+ -- 1. Compute Seed Signature
399+ -- 1.1 Canonical Request
400+ (canonicalReq, signedHeaderKeys) =
401+ getCanonicalRequestAndSignedHeaders
402+ (IsStreamingLength payloadLength)
403+ sp
404+ req
405+ requestHeaders
406+
407+ scope = credentialScope sp
408+ accessKey = spAccessKey sp
409+ -- 1.2 String toSign
410+ stringToSign = mkStringToSign ts scope canonicalReq
411+ -- 1.3 Compute signature
412+ -- 1.3.1 compute signing key
413+ signingKey = getSigningKey sp
414+ -- 1.3.2 Compute signature
415+ seedSignature = computeSignature stringToSign signingKey
416+ -- 1.3.3 Compute Auth Header
417+ authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
418+ -- 1.4 Updated headers for the request
419+ finalReqHeaders = authHeader : requestHeaders
420+ -- headersToAdd = authHeader : datePair : streamingHeaders
421+
422+ toHexStr n = B8.pack $ printf "%x" n
423+ (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
424+ -- Function to compute string to sign for each chunk.
425+ chunkStrToSign prevSign currChunkHash =
426+ B.intercalate
427+ "\n"
428+ [ "AWS4-HMAC-SHA256-PAYLOAD",
429+ awsTimeFormatBS ts,
430+ scope,
431+ prevSign,
432+ hashSHA256 "",
433+ currChunkHash
434+ ]
435+ -- Read n byte from upstream and return a strict bytestring.
436+ mustTakeN n = do
437+ bs <- LB.toStrict <$> (C.takeCE n C..| C.sinkLazy)
438+ when (B.length bs /= n) $
439+ throwIO MErrVStreamingBodyUnexpectedEOF
440+ return bs
441+ signerConduit n lps prevSign =
442+ -- First case encodes a full chunk of length
443+ -- 'chunkSizeConstant'.
444+ if
445+ | n > 0 -> do
446+ bs <- mustTakeN chunkSizeConstant
447+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
448+ nextSign = computeSignature strToSign signingKey
449+ chunkBS =
450+ toHexStr chunkSizeConstant
451+ <> ";chunk-signature="
452+ <> nextSign
453+ <> "\r\n"
454+ <> bs
455+ <> "\r\n"
456+ C.yield chunkBS
457+ signerConduit (n - 1) lps nextSign
458+
459+ -- Second case encodes the last chunk which is smaller than
460+ -- 'chunkSizeConstant'
461+ | lps > 0 -> do
462+ bs <- mustTakeN $ fromIntegral lps
463+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
464+ nextSign = computeSignature strToSign signingKey
465+ chunkBS =
466+ toHexStr lps
467+ <> ";chunk-signature="
468+ <> nextSign
469+ <> "\r\n"
470+ <> bs
471+ <> "\r\n"
472+ C.yield chunkBS
473+ signerConduit 0 0 nextSign
474+
475+ -- Last case encodes the final signature chunk that has no
476+ -- data.
477+ | otherwise -> do
478+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")
479+ nextSign = computeSignature strToSign signingKey
480+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
481+ C.yield lastChunkBS
482+ in \src ->
483+ req
484+ { NC.requestHeaders = finalReqHeaders,
485+ NC.requestBody =
486+ NC.requestBodySource signedContentLength $
487+ src C..| signerConduit numParts lastPSize seedSignature
488+ }
489+
490+-- "setHeader r hdr" adds the hdr to r, replacing it in r if it already exists.
491+setHeader :: Header -> RequestHeaders -> RequestHeaders
492+setHeader hdr r =
493+ let r' = filter (\(name, _) -> name /= fst hdr) r
494+ in hdr : r'
src/Network/Minio/Utils.hs+167−87
@@ -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.
@@ -16,88 +16,146 @@
1616
1717module Network.Minio.Utils where
1818
19−import qualified Control.Concurrent.Async.Lifted as A
20−import qualified Control.Concurrent.QSem.Lifted as Q
21−import qualified Control.Exception.Lifted as ExL
22−import qualified Control.Monad.Catch as MC
19+import qualified Conduit as C
20+import Control.Monad.IO.Unlift (MonadUnliftIO)
2321import qualified Control.Monad.Trans.Resource as R
24−
2522import qualified Data.ByteString as B
2623import qualified Data.ByteString.Lazy as LB
27−import qualified Data.Conduit as C
24+import Data.CaseInsensitive (mk, original)
2825import qualified Data.Conduit.Binary as CB
26+import qualified Data.HashMap.Strict as H
2927import qualified Data.Text as T
30−import Data.Text.Encoding.Error (lenientDecode)
31−import Data.Text.Read (decimal)
32−import Data.Time
33−import Network.HTTP.Conduit (Response)
28+import Data.Text.Read (decimal)
29+import Data.Time
30+ ( defaultTimeLocale,
31+ parseTimeM,
32+ rfc822DateFormat,
33+ )
34+import Lib.Prelude
35+import Network.HTTP.Conduit (Response)
3436import qualified Network.HTTP.Conduit as NC
3537import qualified Network.HTTP.Types as HT
3638import qualified Network.HTTP.Types.Header as Hdr
39+import Network.Minio.Data.ByteString
40+import Network.Minio.JsonParser (parseErrResponseJSON)
41+import Network.Minio.XmlCommon (parseErrResponse)
3742import qualified System.IO as IO
38−import Data.CaseInsensitive (mk)
39−
40−
41−import Lib.Prelude
43+import qualified UnliftIO as U
44+import qualified UnliftIO.Async as A
4245
43−import Network.Minio.XmlParser (parseErrResponse)
44−
45−allocateReadFile :: (R.MonadResource m, R.MonadResourceBase m, MonadCatch m)
46− => FilePath -> m (R.ReleaseKey, Handle)
46+allocateReadFile ::
47+ (MonadUnliftIO m, R.MonadResource m) =>
48+ FilePath ->
49+ m (R.ReleaseKey, Handle)
4750allocateReadFile fp = do
4851 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
49− either (\(e :: IOException) -> throwM e) (return . (rk,)) hdlE
52+ either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
5053 where
51− openReadFile f = ExL.try $ IO.openBinaryFile f IO.ReadMode
54+ openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
5255 cleanup = either (const $ return ()) IO.hClose
5356
5457-- | Queries the file size from the handle. Catches any file operation
5558-- exceptions and returns Nothing instead.
56−getFileSize :: (R.MonadResourceBase m, R.MonadResource m)
57− => Handle -> m (Maybe Int64)
59+getFileSize ::
60+ (MonadUnliftIO m) =>
61+ Handle ->
62+ m (Maybe Int64)
5863getFileSize h = do
5964 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
6065 case resE of
61− Left (_ :: IOException) -> return Nothing
66+ Left (_ :: U.IOException) -> return Nothing
6267 Right s -> return $ Just s
6368
6469-- | Queries if handle is seekable. Catches any file operation
6570-- exceptions and return False instead.
66−isHandleSeekable :: (R.MonadResource m, R.MonadResourceBase m)
67− => Handle -> m Bool
71+isHandleSeekable ::
72+ (R.MonadResource m) =>
73+ Handle ->
74+ m Bool
6875isHandleSeekable h = do
6976 resE <- liftIO $ try $ IO.hIsSeekable h
7077 case resE of
71− Left (_ :: IOException) -> return False
78+ Left (_ :: U.IOException) -> return False
7279 Right v -> return v
7380
7481-- | Helper function that opens a handle to the filepath and performs
7582-- the given action on it. Exceptions of type MError are caught and
7683-- returned - both during file handle allocation and when the action
7784-- is run.
78−withNewHandle :: (R.MonadResourceBase m, R.MonadResource m, MonadCatch m)
79− => FilePath -> (Handle -> m a) -> m (Either IOException a)
85+withNewHandle ::
86+ (MonadUnliftIO m, R.MonadResource m) =>
87+ FilePath ->
88+ (Handle -> m a) ->
89+ m (Either U.IOException a)
8090withNewHandle fp fileAction = do
8191 -- opening a handle can throw MError exception.
82− handleE <- MC.try $ allocateReadFile fp
92+ handleE <- try $ allocateReadFile fp
8393 either (return . Left) doAction handleE
8494 where
8595 doAction (rkey, h) = do
8696 -- fileAction may also throw MError exception, so we catch and
8797 -- return it.
88− resE <- MC.try $ fileAction h
98+ resE <- try $ fileAction h
8999 R.release rkey
90100 return resE
91101
92102mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
93−mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
103+mkHeaderFromPairs = map (first mk)
94104
95105lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
96−lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
106+lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
97107
98108getETagHeader :: [HT.Header] -> Maybe Text
99109getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
100110
111+getMetadata :: [HT.Header] -> [(Text, Text)]
112+getMetadata =
113+ map (\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y))
114+
115+-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
116+-- stripped and a Just is returned.
117+userMetadataHeaderNameMaybe :: Text -> Maybe Text
118+userMetadataHeaderNameMaybe k =
119+ let prefix = T.toCaseFold "X-Amz-Meta-"
120+ n = T.length prefix
121+ in if T.toCaseFold (T.take n k) == prefix
122+ then Just (T.drop n k)
123+ else Nothing
124+
125+toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
126+toMaybeMetadataHeader (k, v) =
127+ (,v) <$> userMetadataHeaderNameMaybe k
128+
129+getNonUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
130+getNonUserMetadataMap =
131+ H.fromList
132+ . filter
133+ ( isNothing
134+ . userMetadataHeaderNameMaybe
135+ . fst
136+ )
137+
138+addXAmzMetaPrefix :: Text -> Text
139+addXAmzMetaPrefix s
140+ | isJust (userMetadataHeaderNameMaybe s) = s
141+ | otherwise = "X-Amz-Meta-" <> s
142+
143+mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
144+mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix x, encodeUtf8 y))
145+
146+-- | This function collects all headers starting with `x-amz-meta-`
147+-- and strips off this prefix, and returns a map.
148+getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
149+getUserMetadataMap =
150+ H.fromList
151+ . mapMaybe toMaybeMetadataHeader
152+
153+getHostHeader :: (ByteString, Int) -> ByteString
154+getHostHeader (host_, port_) =
155+ if port_ == 80 || port_ == 443
156+ then host_
157+ else host_ <> ":" <> show port_
158+
101159getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
102160getLastModifiedHeader hs = do
103161 modTimebs <- decodeUtf8Lenient <$> lookupHeader Hdr.hLastModified hs
@@ -106,77 +164,103 @@ getLastModifiedHeader hs = do
106164getContentLength :: [HT.Header] -> Maybe Int64
107165getContentLength hs = do
108166 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
109− fst <$> hush (decimal nbs)
110−
167+ fst <$> either (const Nothing) Just (decimal nbs)
111168
112169decodeUtf8Lenient :: ByteString -> Text
113170decodeUtf8Lenient = decodeUtf8With lenientDecode
114171
115172isSuccessStatus :: HT.Status -> Bool
116−isSuccessStatus sts = let s = HT.statusCode sts
117− in (s >= 200 && s < 300)
173+isSuccessStatus sts =
174+ let s = HT.statusCode sts
175+ in (s >= 200 && s < 300)
118176
119−httpLbs :: (R.MonadThrow m, MonadIO m)
120− => NC.Request -> NC.Manager
121− -> m (NC.Response LByteString)
177+httpLbs ::
178+ (MonadIO m) =>
179+ NC.Request ->
180+ NC.Manager ->
181+ m (NC.Response LByteString)
122182httpLbs req mgr = do
123183 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
124− resp <- either throwM return respE
184+ resp <- either throwIO return respE
125185 unless (isSuccessStatus $ NC.responseStatus resp) $
126186 case contentTypeMay resp of
127187 Just "application/xml" -> do
128188 sErr <- parseErrResponse $ NC.responseBody resp
129− throwM sErr
130−
131− _ -> throwM $ NC.HttpExceptionRequest req $
132− NC.StatusCodeException (void resp) (show resp)
189+ throwIO sErr
190+ Just "application/json" -> do
191+ sErr <- parseErrResponseJSON $ NC.responseBody resp
192+ throwIO sErr
193+ _ ->
194+ throwIO $
195+ NC.HttpExceptionRequest req $
196+ NC.StatusCodeException (void resp) (showBS resp)
133197
134198 return resp
135199 where
136− tryHttpEx :: IO (NC.Response LByteString)
137− -> IO (Either NC.HttpException (NC.Response LByteString))
200+ tryHttpEx ::
201+ IO (NC.Response LByteString) ->
202+ IO (Either NC.HttpException (NC.Response LByteString))
138203 tryHttpEx = try
139− contentTypeMay resp = lookupHeader Hdr.hContentType $
140− NC.responseHeaders resp
204+ contentTypeMay resp =
205+ lookupHeader Hdr.hContentType $
206+ NC.responseHeaders resp
141207
142−http :: (R.MonadResourceBase m, R.MonadResource m)
143− => NC.Request -> NC.Manager
144− -> m (Response (C.ResumableSource m ByteString))
208+http ::
209+ (MonadUnliftIO m, R.MonadResource m) =>
210+ NC.Request ->
211+ NC.Manager ->
212+ m (Response (C.ConduitT () ByteString m ()))
145213http req mgr = do
146214 respE <- tryHttpEx $ NC.http req mgr
147− resp <- either throwM return respE
215+ resp <- either throwIO return respE
148216 unless (isSuccessStatus $ NC.responseStatus resp) $
149217 case contentTypeMay resp of
150218 Just "application/xml" -> do
151− respBody <- NC.responseBody resp C.$$+- CB.sinkLbs
219+ respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
152220 sErr <- parseErrResponse respBody
153− throwM sErr
154−
221+ throwIO sErr
155222 _ -> do
156223 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
157− throwM $ NC.HttpExceptionRequest req $
158− NC.StatusCodeException (void resp) content
159−
224+ throwIO $
225+ NC.HttpExceptionRequest req $
226+ NC.StatusCodeException (void resp) content
160227
161228 return resp
162229 where
163− tryHttpEx :: (R.MonadResourceBase m) => m a
164− -> m (Either NC.HttpException a)
165− tryHttpEx = ExL.try
166− contentTypeMay resp = lookupHeader Hdr.hContentType $ NC.responseHeaders resp
230+ tryHttpEx ::
231+ (MonadUnliftIO m) =>
232+ m a ->
233+ m (Either NC.HttpException a)
234+ tryHttpEx = try
235+ contentTypeMay resp =
236+ lookupHeader Hdr.hContentType $
237+ NC.responseHeaders resp
167238
168239-- Similar to mapConcurrently but limits the number of threads that
169240-- can run using a quantity semaphore.
170−limitedMapConcurrently :: (MonadIO m, R.MonadBaseControl IO m)
171− => Int -> (t -> m a) -> [t] -> m [a]
241+limitedMapConcurrently ::
242+ (MonadUnliftIO m) =>
243+ Int ->
244+ (t -> m a) ->
245+ [t] ->
246+ m [a]
247+limitedMapConcurrently 0 _ _ = return []
172248limitedMapConcurrently count act args = do
173− qSem <- liftIO $ Q.newQSem count
174− threads <- mapM (A.async . wThread qSem) args
249+ t' <- U.newTVarIO count
250+ threads <- mapM (A.async . wThread t') args
175251 mapM A.wait threads
176252 where
177− -- grab 1 unit from semaphore, run action and release it
178− wThread qs arg =
179− ExL.bracket_ (Q.waitQSem qs) (Q.signalQSem qs) $ act arg
253+ wThread t arg =
254+ U.bracket_ (waitSem t) (signalSem t) $ act arg
255+ -- quantity semaphore implementation using TVar
256+ waitSem t = U.atomically $ do
257+ v <- U.readTVar t
258+ if v > 0
259+ then U.writeTVar t (v - 1)
260+ else U.retrySTM
261+ signalSem t = U.atomically $ do
262+ v <- U.readTVar t
263+ U.writeTVar t (v + 1)
180264
181265-- helper function to 'drop' empty optional parameter.
182266mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -185,21 +269,17 @@ mkQuery k mv = (k,) <$> mv
185269-- helper function to build query parameters that are optional.
186270-- don't use it with mandatory query params with empty value.
187271mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
188−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
272+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
189273
190−chunkBSConduit :: (Monad m, Integral a)
191− => [a] -> C.Conduit ByteString m ByteString
192−chunkBSConduit s = loop 0 [] s
193− where
194− loop _ _ [] = return ()
195− loop n readChunks (size:sizes) = do
196− bsMay <- C.await
197− case bsMay of
198− Nothing -> when (n > 0) $ C.yield $ B.concat readChunks
199− Just bs -> if n + fromIntegral (B.length bs) >= size
200− then do let (a, b) = B.splitAt (fromIntegral $ size - n) bs
201− chunkBS = B.concat $ readChunks ++ [a]
202− C.yield chunkBS
203− loop (fromIntegral $ B.length b) [b] sizes
204− else loop (n + fromIntegral (B.length bs))
205− (readChunks ++ [bs]) (size:sizes)
274+-- | Conduit that rechunks bytestrings into the given chunk
275+-- lengths. Stops after given chunk lengths are yielded. Stops if
276+-- there are no more chunks to yield or if a shorter chunk is
277+-- received. Does not throw any errors.
278+chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
279+chunkBSConduit [] = return ()
280+chunkBSConduit (s : ss) = do
281+ bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
282+ if
283+ | B.length bs == s -> C.yield bs >> chunkBSConduit ss
284+ | B.length bs > 0 -> C.yield bs
285+ | otherwise -> return ()
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+183−54
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,88 +15,217 @@
1515--
1616
1717module Network.Minio.XmlGenerator
18− ( mkCreateBucketConfig
19− , mkCompleteMultipartUploadRequest
20− , mkPutNotificationRequest
21− ) where
22−
18+ ( mkCreateBucketConfig,
19+ mkCompleteMultipartUploadRequest,
20+ mkPutNotificationRequest,
21+ mkSelectRequest,
22+ )
23+where
2324
2425import qualified Data.ByteString.Lazy as LBS
25−import qualified Data.Map as M
26−import qualified Data.Text as T
27−import Text.XML
28−
29−import Lib.Prelude
30−
31−import Network.Minio.Data
32−
26+import qualified Data.Text as T
27+import Network.Minio.Data
28+import Network.Minio.XmlCommon
29+import Text.XML
3330
3431-- | Create a bucketConfig request body XML
35−mkCreateBucketConfig :: Region -> ByteString
36−mkCreateBucketConfig location = LBS.toStrict $ renderLBS def bucketConfig
32+mkCreateBucketConfig :: Text -> Region -> ByteString
33+mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
3734 where
38− s3Element n = Element (s3Name n) M.empty
39− root = s3Element "CreateBucketConfiguration"
40− [ NodeElement $ s3Element "LocationConstraint"
41− [ NodeContent location]
35+ s3Element n = Element (s3Name ns n) mempty
36+ root =
37+ s3Element
38+ "CreateBucketConfiguration"
39+ [ NodeElement $
40+ s3Element
41+ "LocationConstraint"
42+ [NodeContent location]
4243 ]
43− bucketConfig = Document (Prologue [] Nothing []) root []
44+ bucketConfig = Document (Prologue [] Nothing []) root []
4445
4546-- | Create a completeMultipartUpload request body XML
4647mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
4748mkCompleteMultipartUploadRequest partInfo =
4849 LBS.toStrict $ renderLBS def cmur
4950 where
50− root = Element "CompleteMultipartUpload" M.empty $
51− map (NodeElement . mkPart) partInfo
52− mkPart (n, etag) = Element "Part" M.empty
53− [ NodeElement $ Element "PartNumber" M.empty
54− [NodeContent $ T.pack $ show n]
55− , NodeElement $ Element "ETag" M.empty
56− [NodeContent etag]
57− ]
51+ root =
52+ Element "CompleteMultipartUpload" mempty $
53+ map (NodeElement . mkPart) partInfo
54+ mkPart (n, etag) =
55+ Element
56+ "Part"
57+ mempty
58+ [ NodeElement $
59+ Element
60+ "PartNumber"
61+ mempty
62+ [NodeContent $ T.pack $ show n],
63+ NodeElement $
64+ Element
65+ "ETag"
66+ mempty
67+ [NodeContent etag]
68+ ]
5869 cmur = Document (Prologue [] Nothing []) root []
5970
6071-- Simplified XML representation without element attributes.
61−data XNode = XNode Text [XNode]
62− | XLeaf Text Text
63− deriving (Eq, Show)
64−
65−toXML :: XNode -> ByteString
66−toXML node = LBS.toStrict $ renderLBS def $
67− Document (Prologue [] Nothing []) (xmlNode node) []
72+data XNode
73+ = XNode Text [XNode]
74+ | XLeaf Text Text
75+ deriving stock (Eq, Show)
76+
77+toXML :: Text -> XNode -> ByteString
78+toXML ns node =
79+ LBS.toStrict $
80+ renderLBS def $
81+ Document (Prologue [] Nothing []) (xmlNode node) []
6882 where
6983 xmlNode :: XNode -> Element
70− xmlNode (XNode name nodes) = Element (s3Name name) M.empty $
71− map (NodeElement . xmlNode) nodes
72− xmlNode (XLeaf name content) = Element (s3Name name) M.empty
73− [NodeContent content]
84+ xmlNode (XNode name nodes) =
85+ Element (s3Name ns name) mempty $
86+ map (NodeElement . xmlNode) nodes
87+ xmlNode (XLeaf name content) =
88+ Element
89+ (s3Name ns name)
90+ mempty
91+ [NodeContent content]
7492
7593class ToXNode a where
7694 toXNode :: a -> XNode
7795
7896instance ToXNode Event where
79− toXNode = XLeaf "Event" . show
97+ toXNode = XLeaf "Event" . toText
8098
8199instance ToXNode Notification where
82− toXNode (Notification qc tc lc) = XNode "NotificationConfiguration" $
83− map (toXNodesWithArnName "QueueConfiguration" "Queue") qc ++
84− map (toXNodesWithArnName "TopicConfiguration" "Topic") tc ++
85− map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
100+ toXNode (Notification qc tc lc) =
101+ XNode "NotificationConfiguration" $
102+ map (toXNodesWithArnName "QueueConfiguration" "Queue") qc
103+ ++ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc
104+ ++ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
86105
87106toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
88−toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
89− XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events ++
90− [toXNode fRule]
107+toXNodesWithArnName eltName arnName (NotificationConfig itemId arn events fRule) =
108+ XNode eltName $
109+ [XLeaf "Id" itemId, XLeaf arnName arn]
110+ ++ map toXNode events
111+ ++ [toXNode fRule]
91112
92113instance ToXNode Filter where
93114 toXNode (Filter (FilterKey (FilterRules rules))) =
94115 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
95116
96117getFRXNode :: FilterRule -> XNode
97−getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
98− , XLeaf "Value" v
99− ]
100−
101−mkPutNotificationRequest :: Notification -> ByteString
102−mkPutNotificationRequest = toXML . toXNode
118+getFRXNode (FilterRule n v) =
119+ XNode
120+ "FilterRule"
121+ [ XLeaf "Name" n,
122+ XLeaf "Value" v
123+ ]
124+
125+mkPutNotificationRequest :: Text -> Notification -> ByteString
126+mkPutNotificationRequest ns = toXML ns . toXNode
127+
128+mkSelectRequest :: SelectRequest -> ByteString
129+mkSelectRequest r = LBS.toStrict $ renderLBS def sr
130+ where
131+ sr = Document (Prologue [] Nothing []) root []
132+ root =
133+ Element "SelectRequest" mempty $
134+ [ NodeElement
135+ ( Element
136+ "Expression"
137+ mempty
138+ [NodeContent $ srExpression r]
139+ ),
140+ NodeElement
141+ ( Element
142+ "ExpressionType"
143+ mempty
144+ [NodeContent $ show $ srExpressionType r]
145+ ),
146+ NodeElement
147+ ( Element "InputSerialization" mempty $
148+ inputSerializationNodes $
149+ srInputSerialization r
150+ ),
151+ NodeElement
152+ ( Element "OutputSerialization" mempty $
153+ outputSerializationNodes $
154+ srOutputSerialization r
155+ )
156+ ]
157+ ++ maybe [] reqProgElem (srRequestProgressEnabled r)
158+ reqProgElem enabled =
159+ [ NodeElement
160+ ( Element
161+ "RequestProgress"
162+ mempty
163+ [ NodeElement
164+ ( Element
165+ "Enabled"
166+ mempty
167+ [ NodeContent
168+ (if enabled then "TRUE" else "FALSE")
169+ ]
170+ )
171+ ]
172+ )
173+ ]
174+ inputSerializationNodes is =
175+ comprTypeNode (isCompressionType is)
176+ ++ [NodeElement $ formatNode (isFormatInfo is)]
177+ comprTypeNode (Just c) =
178+ [ NodeElement $
179+ Element
180+ "CompressionType"
181+ mempty
182+ [ NodeContent $ case c of
183+ CompressionTypeNone -> "NONE"
184+ CompressionTypeGzip -> "GZIP"
185+ CompressionTypeBzip2 -> "BZIP2"
186+ ]
187+ ]
188+ comprTypeNode Nothing = []
189+ kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
190+ formatNode (InputFormatCSV c) =
191+ Element
192+ "CSV"
193+ mempty
194+ (map (NodeElement . kvElement) (csvPropsList c))
195+ formatNode (InputFormatJSON p) =
196+ Element
197+ "JSON"
198+ mempty
199+ [ NodeElement
200+ ( Element
201+ "Type"
202+ mempty
203+ [ NodeContent $ case jsonipType p of
204+ JSONTypeDocument -> "DOCUMENT"
205+ JSONTypeLines -> "LINES"
206+ ]
207+ )
208+ ]
209+ formatNode InputFormatParquet = Element "Parquet" mempty []
210+ outputSerializationNodes (OutputSerializationJSON j) =
211+ [ NodeElement
212+ ( Element "JSON" mempty $
213+ rdElem $
214+ jsonopRecordDelimiter j
215+ )
216+ ]
217+ outputSerializationNodes (OutputSerializationCSV c) =
218+ [ NodeElement $
219+ Element
220+ "CSV"
221+ mempty
222+ (map (NodeElement . kvElement) (csvPropsList c))
223+ ]
224+ rdElem Nothing = []
225+ rdElem (Just t) =
226+ [ NodeElement $
227+ Element
228+ "RecordDelimiter"
229+ mempty
230+ [NodeContent t]
231+ ]
src/Network/Minio/XmlParser.hs+142−154
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,228 +15,216 @@
1515--
1616
1717module Network.Minio.XmlParser
18− ( parseListBuckets
19− , parseLocation
20− , parseNewMultipartUpload
21− , parseCompleteMultipartUploadResponse
22− , parseCopyObjectResponse
23− , parseListObjectsResponse
24− , parseListObjectsV1Response
25− , parseListUploadsResponse
26− , parseListPartsResponse
27− , parseErrResponse
28− , parseNotification
29− ) where
30−
31−import Control.Monad.Trans.Resource
32−import Data.List (zip3, zip4)
33−import qualified Data.Text as T
34−import Data.Text.Read (decimal)
35−import Data.Time
36−import Text.XML
37−import Text.XML.Cursor hiding (bool)
38−
39−import Lib.Prelude
40−
41−import Network.Minio.Data
42−import Network.Minio.Errors
43−
44−
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−-- | Parse time strings from XML
54−parseS3XMLTime :: (MonadThrow m) => Text -> m UTCTime
55−parseS3XMLTime = either (throwM . MErrVXmlParse) return
56− . parseTimeM True defaultTimeLocale s3TimeFormat
57− . T.unpack
58−
59−parseDecimal :: (MonadThrow m, Integral a) => Text -> m a
60−parseDecimal numStr = either (throwM . MErrVXmlParse . show) return $
61− fst <$> decimal numStr
62−
63−parseDecimals :: (MonadThrow m, Integral a) => [Text] -> m [a]
64−parseDecimals numStr = forM numStr parseDecimal
65−
66−s3Elem :: Text -> Axis
67−s3Elem = element . s3Name
68−
69−parseRoot :: (MonadThrow m) => LByteString -> m Cursor
70−parseRoot = either (throwM . MErrVXmlParse . show) (return . fromDocument)
71− . parseLBS def
18+ ( parseListBuckets,
19+ parseLocation,
20+ parseNewMultipartUpload,
21+ parseCompleteMultipartUploadResponse,
22+ parseCopyObjectResponse,
23+ parseListObjectsResponse,
24+ parseListObjectsV1Response,
25+ parseListUploadsResponse,
26+ parseListPartsResponse,
27+ parseErrResponse,
28+ parseNotification,
29+ parseSelectProgress,
30+ )
31+where
32+
33+import qualified Data.ByteString.Lazy as LB
34+import qualified Data.HashMap.Strict as H
35+import Data.List (zip4, zip6)
36+import qualified Data.Text as T
37+import Data.Time
38+import Network.Minio.Data
39+import Network.Minio.XmlCommon
40+import Text.XML.Cursor hiding (bool)
7241
7342-- | Parse the response XML of a list buckets call.
74−parseListBuckets :: (MonadThrow m) => LByteString -> m [BucketInfo]
43+parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
7544parseListBuckets xmldata = do
7645 r <- parseRoot xmldata
77− let
78− names = r $// s3Elem "Bucket" &// s3Elem "Name" &/ content
79− timeStrings = r $// s3Elem "Bucket" &// s3Elem "CreationDate" &/ content
46+ ns <- asks getSvcNamespace
47+ let s3Elem' = s3Elem ns
48+ names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
49+ timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
8050
8151 times <- mapM parseS3XMLTime timeStrings
8252 return $ zipWith BucketInfo names times
8353
8454-- | Parse the response XML of a location request.
85−parseLocation :: (MonadThrow m) => LByteString -> m Region
55+parseLocation :: (MonadIO m) => LByteString -> m Region
8656parseLocation xmldata = do
8757 r <- parseRoot xmldata
8858 let region = T.concat $ r $/ content
8959 return $ bool "us-east-1" region $ region /= ""
9060
9161-- | Parse the response XML of an newMultipartUpload call.
92−parseNewMultipartUpload :: (MonadThrow m) => LByteString -> m UploadId
62+parseNewMultipartUpload :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m UploadId
9363parseNewMultipartUpload xmldata = do
9464 r <- parseRoot xmldata
95− return $ T.concat $ r $// s3Elem "UploadId" &/ content
65+ ns <- asks getSvcNamespace
66+ let s3Elem' = s3Elem ns
67+ return $ T.concat $ r $// s3Elem' "UploadId" &/ content
9668
9769-- | Parse the response XML of completeMultipartUpload call.
98−parseCompleteMultipartUploadResponse :: (MonadThrow m) => LByteString -> m ETag
70+parseCompleteMultipartUploadResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ETag
9971parseCompleteMultipartUploadResponse xmldata = do
10072 r <- parseRoot xmldata
101− return $ T.concat $ r $// s3Elem "ETag" &/ content
73+ ns <- asks getSvcNamespace
74+ let s3Elem' = s3Elem ns
75+ return $ T.concat $ r $// s3Elem' "ETag" &/ content
10276
10377-- | Parse the response XML of copyObject and copyObjectPart
104−parseCopyObjectResponse :: (MonadThrow m) => LByteString -> m (ETag, UTCTime)
78+parseCopyObjectResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m (ETag, UTCTime)
10579parseCopyObjectResponse xmldata = do
10680 r <- parseRoot xmldata
107− let
108− mtimeStr = T.concat $ r $// s3Elem "LastModified" &/ content
81+ ns <- asks getSvcNamespace
82+ let s3Elem' = s3Elem ns
83+ mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
10984
11085 mtime <- parseS3XMLTime mtimeStr
111− return (T.concat $ r $// s3Elem "ETag" &/ content, mtime)
86+ return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
11287
11388-- | Parse the response XML of a list objects v1 call.
114−parseListObjectsV1Response :: (MonadThrow m)
115− => LByteString -> m ListObjectsV1Result
89+parseListObjectsV1Response ::
90+ (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
91+ LByteString ->
92+ m ListObjectsV1Result
11693parseListObjectsV1Response xmldata = do
11794 r <- parseRoot xmldata
118− let
119− hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
120−
121− nextMarker = headMay $ r $/ s3Elem "NextMarker" &/ content
122−
123− prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content
124−
125− keys = r $/ s3Elem "Contents" &/ s3Elem "Key" &/ content
126− modTimeStr = r $/ s3Elem "Contents" &/ s3Elem "LastModified" &/ content
127− etagsList = r $/ s3Elem "Contents" &/ s3Elem "ETag" &/ content
128− -- if response xml contains empty etag response fill them with as
129− -- many empty Text for the zip4 below to work as intended.
130− etags = etagsList ++ repeat ""
131− sizeStr = r $/ s3Elem "Contents" &/ s3Elem "Size" &/ content
95+ ns <- asks getSvcNamespace
96+ let s3Elem' = s3Elem ns
97+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
98+ nextMarker = listToMaybe $ r $/ s3Elem' "NextMarker" &/ content
99+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
100+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
101+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
102+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
103+ -- if response xml contains empty etag response fill them with as
104+ -- many empty Text for the zip4 below to work as intended.
105+ etags = etagsList ++ repeat ""
106+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
132107
133108 modTimes <- mapM parseS3XMLTime modTimeStr
134109 sizes <- parseDecimals sizeStr
135110
136− let
137− objects = map (uncurry4 ObjectInfo) $ zip4 keys modTimes etags sizes
111+ let objects =
112+ map (uncurry6 ObjectInfo) $
113+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
138114
139115 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
140116
141117-- | Parse the response XML of a list objects call.
142−parseListObjectsResponse :: (MonadThrow m) => LByteString -> m ListObjectsResult
118+parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ListObjectsResult
143119parseListObjectsResponse xmldata = do
144120 r <- parseRoot xmldata
145− let
146− hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
147−
148− nextToken = headMay $ r $/ s3Elem "NextContinuationToken" &/ content
149−
150− prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content
151−
152− keys = r $/ s3Elem "Contents" &/ s3Elem "Key" &/ content
153− modTimeStr = r $/ s3Elem "Contents" &/ s3Elem "LastModified" &/ content
154− etagsList = r $/ s3Elem "Contents" &/ s3Elem "ETag" &/ content
155− -- if response xml contains empty etag response fill them with as
156− -- many empty Text for the zip4 below to work as intended.
157− etags = etagsList ++ repeat ""
158− sizeStr = r $/ s3Elem "Contents" &/ s3Elem "Size" &/ content
121+ ns <- asks getSvcNamespace
122+ let s3Elem' = s3Elem ns
123+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
124+ nextToken = listToMaybe $ r $/ s3Elem' "NextContinuationToken" &/ content
125+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
126+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
127+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
128+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
129+ -- if response xml contains empty etag response fill them with as
130+ -- many empty Text for the zip4 below to work as intended.
131+ etags = etagsList ++ repeat ""
132+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
159133
160134 modTimes <- mapM parseS3XMLTime modTimeStr
161135 sizes <- parseDecimals sizeStr
162136
163− let
164− objects = map (uncurry4 ObjectInfo) $ zip4 keys modTimes etags sizes
137+ let objects =
138+ map (uncurry6 ObjectInfo) $
139+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
165140
166141 return $ ListObjectsResult hasMore nextToken objects prefixes
167142
168143-- | Parse the response XML of a list incomplete multipart upload call.
169−parseListUploadsResponse :: (MonadThrow m) => LByteString -> m ListUploadsResult
144+parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ListUploadsResult
170145parseListUploadsResponse xmldata = do
171146 r <- parseRoot xmldata
172− let
173− hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
174− prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content
175− nextKey = headMay $ r $/ s3Elem "NextKeyMarker" &/ content
176− nextUpload = headMay $ r $/ s3Elem "NextUploadIdMarker" &/ content
177− uploadKeys = r $/ s3Elem "Upload" &/ s3Elem "Key" &/ content
178− uploadIds = r $/ s3Elem "Upload" &/ s3Elem "UploadId" &/ content
179− uploadInitTimeStr = r $/ s3Elem "Upload" &/ s3Elem "Initiated" &/ content
147+ ns <- asks getSvcNamespace
148+ let s3Elem' = s3Elem ns
149+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
150+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
151+ nextKey = listToMaybe $ r $/ s3Elem' "NextKeyMarker" &/ content
152+ nextUpload = listToMaybe $ r $/ s3Elem' "NextUploadIdMarker" &/ content
153+ uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
154+ uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
155+ uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
180156
181157 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
182158
183− let
184− uploads = zip3 uploadKeys uploadIds uploadInitTimes
159+ let uploads = zip3 uploadKeys uploadIds uploadInitTimes
185160
186161 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
187162
188−parseListPartsResponse :: (MonadThrow m) => LByteString -> m ListPartsResult
163+parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ListPartsResult
189164parseListPartsResponse xmldata = do
190165 r <- parseRoot xmldata
191− let
192− hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
193− nextPartNumStr = headMay $ r $/ s3Elem "NextPartNumberMarker" &/ content
194− partNumberStr = r $/ s3Elem "Part" &/ s3Elem "PartNumber" &/ content
195− partModTimeStr = r $/ s3Elem "Part" &/ s3Elem "LastModified" &/ content
196− partETags = r $/ s3Elem "Part" &/ s3Elem "ETag" &/ content
197− partSizeStr = r $/ s3Elem "Part" &/ s3Elem "Size" &/ content
166+ ns <- asks getSvcNamespace
167+ let s3Elem' = s3Elem ns
168+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
169+ nextPartNumStr = listToMaybe $ r $/ s3Elem' "NextPartNumberMarker" &/ content
170+ partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
171+ partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
172+ partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
173+ partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
198174
199175 partModTimes <- mapM parseS3XMLTime partModTimeStr
200176 partSizes <- parseDecimals partSizeStr
201177 partNumbers <- parseDecimals partNumberStr
202178 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
203179
204− let
205− partInfos = map (uncurry4 ObjectPartInfo) $
206− zip4 partNumbers partETags partSizes partModTimes
180+ let partInfos =
181+ map (uncurry4 ObjectPartInfo) $
182+ zip4 partNumbers partETags partSizes partModTimes
207183
208184 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
209185
210−
211−parseErrResponse :: (MonadThrow m) => LByteString -> m ServiceErr
212−parseErrResponse xmldata = do
213− r <- parseRoot xmldata
214− let code = T.concat $ r $/ element "Code" &/ content
215− message = T.concat $ r $/ element "Message" &/ content
216− return $ toServiceErr code message
217−
218−parseNotification :: (MonadThrow m) => LByteString -> m Notification
186+parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
219187parseNotification xmldata = do
220188 r <- parseRoot xmldata
221− let qcfg = map node $ r $/ s3Elem "QueueConfiguration"
222− tcfg = map node $ r $/ s3Elem "TopicConfiguration"
223− lcfg = map node $ r $/ s3Elem "CloudFunctionConfiguration"
224− Notification <$> (mapM (parseNode "Queue") qcfg)
225− <*> (mapM (parseNode "Topic") tcfg)
226− <*> (mapM (parseNode "CloudFunction") lcfg)
189+ ns <- asks getSvcNamespace
190+ let s3Elem' = s3Elem ns
191+ qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
192+ tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
193+ lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
194+ Notification
195+ <$> mapM (parseNode ns "Queue") qcfg
196+ <*> mapM (parseNode ns "Topic") tcfg
197+ <*> mapM (parseNode ns "CloudFunction") lcfg
227198 where
228−
229− getFilterRule c =
230− let name = T.concat $ c $/ s3Elem "Name" &/ content
231− value = T.concat $ c $/ s3Elem "Value" &/ content
232− in FilterRule name value
233−
234− parseNode arnName nodeData = do
199+ getFilterRule ns c =
200+ let name = T.concat $ c $/ s3Elem ns "Name" &/ content
201+ value = T.concat $ c $/ s3Elem ns "Value" &/ content
202+ in FilterRule name value
203+ parseNode ns arnName nodeData = do
235204 let c = fromNode nodeData
236− id = T.concat $ c $/ s3Elem "Id" &/ content
237− arn = T.concat $ c $/ s3Elem arnName &/ content
238− events = catMaybes $ map textToEvent $ c $/ s3Elem "Event" &/ content
239− rules = c $/ s3Elem "Filter" &/ s3Elem "S3Key" &/
240− s3Elem "FilterRule" &| getFilterRule
241− return $ NotificationConfig id arn events
242− (Filter $ FilterKey $ FilterRules rules)
205+ itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
206+ arn = T.concat $ c $/ s3Elem ns arnName &/ content
207+ events = mapMaybe textToEvent (c $/ s3Elem ns "Event" &/ content)
208+ rules =
209+ c
210+ $/ s3Elem ns "Filter"
211+ &/ s3Elem ns "S3Key"
212+ &/ s3Elem ns "FilterRule"
213+ &| getFilterRule ns
214+ return $
215+ NotificationConfig
216+ itemId
217+ arn
218+ events
219+ (Filter $ FilterKey $ FilterRules rules)
220+
221+parseSelectProgress :: (MonadIO m) => ByteString -> m Progress
222+parseSelectProgress xmldata = do
223+ r <- parseRoot $ LB.fromStrict xmldata
224+ let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
225+ bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content
226+ bReturned = T.concat $ r $/ element "BytesReturned" &/ content
227+ Progress
228+ <$> parseDecimal bScanned
229+ <*> parseDecimal bProcessed
230+ <*> parseDecimal bReturned
stack.yaml+4−4
@@ -15,7 +15,7 @@
1515# resolver:
1616# name: custom-snapshot
1717# location: "./custom-snapshot.yaml"
18−resolver: lts-9.1
18+resolver: lts-22.19
1919
2020# User packages to be built.
2121# Various formats can be used as shown in the example below.
@@ -36,17 +36,17 @@ resolver: lts-9.1
3636# non-dependency (i.e. a user package), and its test suites and benchmarks
3737# will not be run. This is useful for tweaking upstream packages.
3838packages:
39−- '.'
39+ - "."
4040# Dependency packages to be pulled from upstream that are not in the resolver
4141# (e.g., acme-missiles-0.3)
42−extra-deps: []
42+extra-deps:
43+ - crypton-connection-0.3.2
4344
4445# Override default flag values for local packages and extra-deps
4546flags: {}
4647
4748# Extra package databases containing global packages
4849extra-package-dbs: []
49−
5050# Control whether we use the GHC we find on the path
5151# system-ghc: true
5252#
stack.yaml.locknew file+19
@@ -0,0 +1,19 @@
1+# This file was autogenerated by Stack.
2+# You should not edit this file by hand.
3+# For more information, please see the documentation at:
4+# https://docs.haskellstack.org/en/stable/lock_files
5+
6+packages:
7+- completed:
8+ hackage: crypton-connection-0.3.2@sha256:c7937edc25ab022bcf167703f2ec5ab73b62908e545bb587d2aa42b33cd6f6cc,1581
9+ pantry-tree:
10+ sha256: f986ad29b008cbe5732606e9cde1897191c486a2f1f169a4cb75fd915bce397c
11+ size: 394
12+ original:
13+ hackage: crypton-connection-0.3.2
14+snapshots:
15+- completed:
16+ sha256: e5cac927cf7ccbd52aa41476baa68b88c564ee6ddc3bc573dbf4210069287fe7
17+ size: 713340
18+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/19.yaml
19+ original: lts-22.19
test/LiveServer.hs+1135−478
@@ -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.
@@ -14,38 +14,34 @@
1414-- limitations under the License.
1515--
1616
17−import qualified Test.QuickCheck as Q
18−import Test.Tasty
19−import Test.Tasty.HUnit
20−import Test.Tasty.QuickCheck as QC
21−
22−import Lib.Prelude
23−
24−import System.Directory (getTemporaryDirectory)
25−import qualified System.IO as SIO
26−
27−import qualified Control.Monad.Catch as MC
28−import qualified Control.Monad.Trans.Resource as R
29−import qualified Data.ByteString as BS
30−import Data.Conduit (yield, ($$))
31−import qualified Data.Conduit as C
32−import qualified Data.Conduit.Binary as CB
33−import Data.Conduit.Combinators (sinkList)
34−import Data.Default (Default (..))
35−import qualified Data.Map.Strict as Map
36−import qualified Data.Text as T
37−import qualified Data.Time as Time
17+import Conduit (replicateC)
18+import qualified Conduit as C
19+import qualified Control.Monad.Trans.Resource as R
20+import qualified Data.ByteString as BS
21+import Data.Conduit (yield)
22+import qualified Data.Conduit.Binary as CB
23+import Data.Conduit.Combinators (sinkList)
24+import qualified Data.HashMap.Strict as H
25+import qualified Data.Text as T
26+import Data.Time (fromGregorian)
27+import qualified Data.Time as Time
28+import Lib.Prelude
3829import qualified Network.HTTP.Client.MultipartFormData as Form
39−import qualified Network.HTTP.Conduit as NC
40−import qualified Network.HTTP.Types as HT
41−import System.Environment (lookupEnv)
42−
43−import Network.Minio
44−import Network.Minio.Data
45−import Network.Minio.ListOps
46−import Network.Minio.PutObject
47−import Network.Minio.S3API
48−import Network.Minio.Utils
30+import qualified Network.HTTP.Conduit as NC
31+import qualified Network.HTTP.Types as HT
32+import Network.Minio
33+import Network.Minio.Credentials (Creds (CredsStatic))
34+import Network.Minio.Data
35+import Network.Minio.Data.Crypto
36+import Network.Minio.S3API
37+import Network.Minio.Utils
38+import System.Directory (getTemporaryDirectory)
39+import qualified System.Environment as Env
40+import qualified Test.QuickCheck as Q
41+import Test.Tasty
42+import Test.Tasty.HUnit
43+import Test.Tasty.QuickCheck as QC
44+import qualified UnliftIO.IO as UIO
4945
5046main :: IO ()
5147main = defaultMain tests
@@ -53,451 +49,574 @@ main = defaultMain tests
5349tests :: TestTree
5450tests = testGroup "Tests" [liveServerUnitTests]
5551
52+lookupTestEnv :: IO (Maybe ConnectInfo, (AccessKey, SecretKey))
53+lookupTestEnv = do
54+ val <- Env.lookupEnv "MINIO_LOCAL"
55+ secure <- Env.lookupEnv "MINIO_SECURE"
56+ let localServerMaybe = case (val, secure) of
57+ (Just _, Just _) -> Just "https://localhost:9000"
58+ (Just _, Nothing) -> Just "http://localhost:9000"
59+ _ -> Nothing
60+ accessKeyE <- Env.lookupEnv "MINIO_ACCESS_KEY"
61+ let accessKey = maybe "minioadmin" fromString accessKeyE
62+ secretKeyE <- Env.lookupEnv "MINIO_SECRET_KEY"
63+ let secretKey = maybe "minioadmin" fromString secretKeyE
64+ return (localServerMaybe, (accessKey, secretKey))
65+
66+loadTestServerConnInfo :: IO ConnectInfo
67+loadTestServerConnInfo = do
68+ (localServerMaybe, (ak, sk)) <- lookupTestEnv
69+ return $ case localServerMaybe of
70+ Just localServer -> do
71+ setCreds (CredentialValue ak sk mempty) localServer
72+ _ -> minioPlayCI
73+
74+loadTestServerConnInfoSTS :: IO ConnectInfo
75+loadTestServerConnInfoSTS = do
76+ (localServerMaybe, (ak, sk)) <- lookupTestEnv
77+ case localServerMaybe of
78+ Just localServer -> do
79+ let creds = CredentialValue ak sk mempty
80+ assumeRole =
81+ STSAssumeRole
82+ { sarCredentials = creds,
83+ sarOptions = defaultSTSAssumeRoleOptions
84+ }
85+ setSTSCredential assumeRole localServer
86+ Nothing -> do
87+ let creds = case connectCreds minioPlayCI of
88+ CredsStatic c -> c
89+ _ -> error "unexpected play creds"
90+ assumeRole =
91+ STSAssumeRole
92+ { sarCredentials = creds,
93+ sarOptions = defaultSTSAssumeRoleOptions
94+ }
95+ setSTSCredential assumeRole minioPlayCI
96+
5697-- conduit that generates random binary stream of given length
57−randomDataSrc :: MonadIO m => Int64 -> C.Producer m ByteString
58−randomDataSrc s' = genBS s'
98+randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
99+randomDataSrc = genBS
59100 where
60− oneMiB = 1024*1024
61−
62− concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++
63− [BS.take (fromIntegral r) bs]
64− where (q, r) = n `divMod` fromIntegral (BS.length bs)
65−
101+ concatIt bs n =
102+ BS.concat $
103+ replicate (fromIntegral q) bs
104+ ++ [BS.take (fromIntegral r) bs]
105+ where
106+ (q, r) = n `divMod` fromIntegral (BS.length bs)
66107 genBS s = do
67108 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
68109 let byteArr64 = BS.pack w8s
69110 if s < oneMiB
70111 then yield $ concatIt byteArr64 s
71− else do yield $ concatIt byteArr64 oneMiB
72− genBS (s - oneMiB)
112+ else do
113+ yield $ concatIt byteArr64 oneMiB
114+ genBS (s - oneMiB)
73115
74−mkRandFile :: R.MonadResource m => Int64 -> m FilePath
116+mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
75117mkRandFile size = do
76− dir <- liftIO $ getTemporaryDirectory
77− randomDataSrc size C.$$ CB.sinkTempFile dir "miniohstest.random"
118+ dir <- liftIO getTemporaryDirectory
119+ C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
78120
79121funTestBucketPrefix :: Text
80122funTestBucketPrefix = "miniohstest-"
81123
82−funTestWithBucket :: TestName
83− -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree
84−funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
124+funTestWithBucketNoSTS ::
125+ TestName ->
126+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
127+ TestTree
128+funTestWithBucketNoSTS t minioTest = testCaseSteps t $ \step -> do
85129 -- generate a random name for the bucket
86130 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
87131 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
88132 liftStep = liftIO . step
89− connInfo <- maybe minioPlayCI (const def) <$> lookupEnv "MINIO_LOCAL"
133+ connInfo <- loadTestServerConnInfo
90134 ret <- runMinio connInfo $ do
91135 liftStep $ "Creating bucket for test - " ++ t
92136 foundBucket <- bucketExists b
93137 liftIO $ foundBucket @?= False
94− makeBucket b def
138+ makeBucket b Nothing
95139 minioTest liftStep b
96140 deleteBucket b
97141 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
98142
99−lowLevelMultipartTest :: TestTree
100−lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
101− \step bucket -> do
102− -- low-level multipart operation tests.
103− let object = "newmpupload"
104− mb15 = 15 * 1024 * 1024
105−
106− step "Prepare for low-level multipart tests."
107− step "create new multipart upload"
108− uid <- newMultipartUpload bucket object []
109− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
110−
111− randFile <- mkRandFile mb15
112−
113− step "put object parts 1 of 1"
114− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
115− partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
116−
117− step "complete multipart"
118− void $ completeMultipartUpload bucket object uid [partInfo]
119−
120− destFile <- mkRandFile 0
121− step "Retrieve the created object and check size"
122− fGetObject bucket object destFile
123− gotSize <- withNewHandle destFile getFileSize
124− liftIO $ gotSize == Right (Just mb15) @?
125− "Wrong file size of put file after getting"
126−
127− step "Cleanup actions"
128− removeObject bucket object
129−
130−putObjectNoSizeTest :: TestTree
131−putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
132− \step bucket -> do
133− -- putObject test (conduit source, no size specified)
134− let obj = "mpart"
135− mb70 = 70 * 1024 * 1024
136−
137− step "Prepare for putObject with from source without providing size."
138− rFile <- mkRandFile mb70
139−
140− step "Upload multipart file."
141− putObject bucket obj (CB.sourceFile rFile) Nothing
143+funTestWithBucket ::
144+ TestName ->
145+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
146+ TestTree
147+funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
148+ -- generate a random name for the bucket
149+ bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
150+ let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
151+ liftStep = liftIO . step
152+ connInfo <- loadTestServerConnInfo
153+ ret <- runMinio connInfo $ do
154+ liftStep $ "Creating bucket for test - " ++ t
155+ foundBucket <- bucketExists b
156+ liftIO $ foundBucket @?= False
157+ makeBucket b Nothing
158+ minioTest liftStep b
159+ deleteBucket b
160+ isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
142161
143− step "Retrieve and verify file size"
144− destFile <- mkRandFile 0
145− fGetObject bucket obj destFile
146− gotSize <- withNewHandle destFile getFileSize
147− liftIO $ gotSize == Right (Just mb70) @?
148− "Wrong file size of put file after getting"
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')
149172
150− step "Cleanup actions"
151− deleteObject bucket obj
173+liveServerUnitTests :: TestTree
174+liveServerUnitTests =
175+ testGroup
176+ "Unit tests against a live server"
177+ [ basicTests,
178+ listingTest,
179+ highLevelListingTest,
180+ lowLevelMultipartTest,
181+ putObjectSizeTest,
182+ putObjectNoSizeTest,
183+ multipartTest,
184+ putObjectContentTypeTest,
185+ putObjectContentLanguageTest,
186+ putObjectStorageClassTest,
187+ putObjectUserMetadataTest,
188+ getObjectTest,
189+ copyObjectTests,
190+ presignedUrlFunTest,
191+ presignedPostPolicyFunTest,
192+ bucketPolicyFunTest,
193+ getNPutSSECTest,
194+ assumeRoleRequestTest
195+ ]
152196
153−highLevelListingTest :: TestTree
154−highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
197+basicTests :: TestTree
198+basicTests = funTestWithBucket "Basic tests" $
155199 \step bucket -> do
156− step "High-level listObjects Test"
157− step "put 3 objects"
158− let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3"]
159− forM_ expectedObjects $
160− \obj -> fPutObject bucket obj "/etc/lsb-release"
161−
162− step "High-level listing of objects"
163− objects <- listObjects bucket Nothing True $$ sinkList
164−
165− liftIO $ assertEqual "Objects match failed!" (sort expectedObjects)
166− (map oiObject objects)
167−
168− step "High-level listing of objects (version 1)"
169− objects <- listObjectsV1 bucket Nothing True $$ sinkList
170−
171− step "Cleanup actions"
172− forM_ expectedObjects $
173− \obj -> removeObject bucket obj
174−
175− step "High-level listIncompleteUploads Test"
176− let object = "newmpupload"
177− step "create 10 multipart uploads"
178− forM_ [1..10::Int] $ \_ -> do
179− uid <- newMultipartUpload bucket object []
180− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
181−
182− step "High-level listing of incomplete multipart uploads"
183− uploads <- listIncompleteUploads bucket Nothing True $$ sinkList
184− -- Minio server behaviour changed to list no incomplete uploads,
185− -- so the check below reflects this; this test is expected to
186− -- fail on AWS S3.
187− liftIO $ length uploads @?= 0
188−
189− step "cleanup"
190− forM_ uploads $ \(UploadInfo _ uid _ _) ->
191− abortMultipartUpload bucket object uid
192−
193− step "High-level listIncompleteParts Test"
194− let mb5 = 5 * 1024 * 1024
195−
196− step "create a multipart upload"
197− uid <- newMultipartUpload bucket object []
198− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
200+ step "getService works and contains the test bucket."
201+ buckets <- getService
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+ )
209+
210+ step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
211+ mbE <- try $ makeBucket bucket Nothing
212+ case mbE of
213+ Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
214+ _ -> return ()
215+
216+ step "makeBucket with an invalid bucket name and check for appropriate exception."
217+ invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
218+ case invalidMBE of
219+ Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
220+ _ -> return ()
221+
222+ step "getLocation works"
223+ region <- getLocation bucket
224+ liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
225+
226+ step "singlepart putObject works"
227+ testFilepath <- mkRandFile 200
228+ fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
229+
230+ step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
231+ fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
232+ case fpE of
233+ Left exn -> liftIO $ exn @?= NoSuchBucket
234+ _ -> return ()
235+
236+ outFile <- mkRandFile 0
237+ step "simple fGetObject works"
238+ fGetObject bucket "test-file" outFile defaultGetObjectOptions
239+
240+ let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
241+ step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
242+ resE <-
243+ try $
244+ fGetObject
245+ bucket
246+ "test-file"
247+ outFile
248+ defaultGetObjectOptions
249+ { gooIfUnmodifiedSince = Just unmodifiedTime
250+ }
251+ case resE of
252+ Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
253+ _ -> return ()
254+
255+ step "fGetObject an object with no matching etag, check for exception"
256+ resE1 <-
257+ try $
258+ fGetObject
259+ bucket
260+ "test-file"
261+ outFile
262+ defaultGetObjectOptions
263+ { gooIfMatch = Just "invalid-etag"
264+ }
265+ case resE1 of
266+ Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
267+ _ -> return ()
268+
269+ step "fGetObject an object with no valid range, check for exception"
270+ resE2 <-
271+ try $
272+ fGetObject
273+ bucket
274+ "test-file"
275+ outFile
276+ defaultGetObjectOptions
277+ { gooRange = Just $ HT.ByteRangeFromTo 100 300
278+ }
279+ case resE2 of
280+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
281+ _ -> return ()
282+
283+ step "fGetObject on object with a valid range"
284+ fGetObject
285+ bucket
286+ "test-file"
287+ outFile
288+ defaultGetObjectOptions
289+ { gooRange = Just $ HT.ByteRangeFrom 1
290+ }
199291
200− step "put object parts 1..10"
201− inputFile <- mkRandFile mb5
202− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
203− forM_ [1..10] $ \pnum ->
204− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
292+ step "fGetObject a non-existent object and check for NoSuchKey exception"
293+ resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
294+ case resE3 of
295+ Left exn -> liftIO $ exn @?= NoSuchKey
296+ _ -> return ()
205297
206− step "fetch list parts"
207− incompleteParts <- listIncompleteParts bucket object uid $$ sinkList
208− liftIO $ length incompleteParts @?= 10
298+ step "create new multipart upload works"
299+ uid <- newMultipartUpload bucket "newmpupload" []
300+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
209301
210− step "cleanup"
211− abortMultipartUpload bucket object uid
302+ step "abort a new multipart upload works"
303+ abortMultipartUpload bucket "newmpupload" uid
212304
213−listingTest :: TestTree
214−listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
215− step "listObjects' test"
216− step "put 10 objects"
217− let objects = (\s ->T.concat ["lsb-release", T.pack (show s)]) <$> [1..10::Int]
218−
219− forM_ [1..10::Int] $ \s ->
220− fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release"
221−
222− step "Simple list"
223− res <- listObjects' bucket Nothing Nothing Nothing Nothing
224− let expectedObjects = sort objects
225− liftIO $ assertEqual "Objects match failed!" expectedObjects
226− (map oiObject $ lorObjects res)
227−
228− step "Simple list version 1"
229− res <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
230− let expected = sort $ map (T.concat .
231− ("lsb-release":) .
232− (\x -> [x]) .
233− T.pack .
234− show) [1..10::Int]
235− liftIO $ assertEqual "Objects match failed!" expected
236− (map oiObject $ lorObjects' res)
237−
238− step "Cleanup actions"
239− forM_ objects $ \obj -> deleteObject bucket obj
240−
241− step "listIncompleteUploads' test"
242− step "create 10 multipart uploads"
243− let object = "newmpupload"
244− forM_ [1..10::Int] $ \_ -> do
245− uid <- newMultipartUpload bucket object []
246− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
247−
248− step "list incomplete multipart uploads"
249− incompleteUploads <- listIncompleteUploads' bucket Nothing Nothing
250− Nothing Nothing Nothing
251− -- Minio server behaviour changed to list no incomplete uploads,
252− -- so the check below reflects this; this test is expected to
253− -- fail on AWS S3.
254− liftIO $ (length $ lurUploads incompleteUploads) @?= 0
255−
256− step "cleanup"
257− forM_ (lurUploads incompleteUploads) $
258− \(_, uid, _) -> abortMultipartUpload bucket object uid
259−
260− step "Basic listIncompleteParts Test"
261− let mb5 = 5 * 1024 * 1024
262−
263− step "create a multipart upload"
264− uid <- newMultipartUpload bucket object []
265− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
305+ step "delete object works"
306+ deleteObject bucket "test-file"
266307
267− step "put object parts 1..10"
268− inputFile <- mkRandFile mb5
269− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
270− forM_ [1..10] $ \pnum ->
271− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
308+ step "statObject test"
309+ let object = "sample"
310+ step "create an object"
311+ inputFile <- mkRandFile 0
312+ fPutObject bucket object inputFile defaultPutObjectOptions
272313
273− step "fetch list parts"
274− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
275− liftIO $ (length $ lprParts listPartsResult) @?= 10
276− abortMultipartUpload bucket object uid
314+ step "get metadata of the object"
315+ res <- statObject bucket object defaultGetObjectOptions
316+ liftIO $ oiSize res @?= 0
277317
318+ step "delete object"
319+ deleteObject bucket object
278320
279−liveServerUnitTests :: TestTree
280−liveServerUnitTests = testGroup "Unit tests against a live server"
281− [ basicTests
282− , listingTest
283− , highLevelListingTest
284− , lowLevelMultipartTest
285− , putObjectNoSizeTest
286− , funTestWithBucket "Multipart Tests" $
287− \step bucket -> do
288− step "Prepare for putObjectInternal with non-seekable file, with size."
289− step "Upload multipart file."
290− let mb80 = 80 * 1024 * 1024
291− obj = "mpart"
292−
293− void $ putObjectInternal bucket obj $ ODFile "/dev/zero" (Just mb80)
294−
295− step "Retrieve and verify file size"
296− destFile <- mkRandFile 0
297− fGetObject bucket obj destFile
298− gotSize <- withNewHandle destFile getFileSize
299− liftIO $ gotSize == Right (Just mb80) @?
300− "Wrong file size of put file after getting"
301−
302− step "Cleanup actions"
303− removeObject bucket obj
304−
305− step "cleanup"
306− removeObject bucket "big"
307−
308− step "Prepare for removeIncompleteUpload"
309− -- low-level multipart operation tests.
310− let object = "newmpupload"
311− kb5 = 5 * 1024
312−
313− step "create new multipart upload"
314− uid <- newMultipartUpload bucket object []
315− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
316−
317− randFile <- mkRandFile kb5
318−
319− step "upload 2 parts"
320− forM_ [1,2] $ \partNum -> do
321− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
322− void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
323−
324− step "remove ongoing upload"
325− removeIncompleteUpload bucket object
326− uploads <- listIncompleteUploads bucket (Just object) False C.$$ sinkList
327− liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
328−
321+lowLevelMultipartTest :: TestTree
322+lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
323+ \step bucket -> do
324+ -- low-level multipart operation tests.
325+ let object = "newmpupload"
326+ mb15 = 15 * 1024 * 1024
327+
328+ step "Prepare for low-level multipart tests."
329+ step "create new multipart upload"
330+ uid <- newMultipartUpload bucket object []
331+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
332+
333+ randFile <- mkRandFile mb15
334+
335+ step "put object parts 1 of 1"
336+ partInfo <-
337+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
338+ putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
339+
340+ step "complete multipart"
341+ void $ completeMultipartUpload bucket object uid [partInfo]
342+
343+ destFile <- mkRandFile 0
344+ step "Retrieve the created object and check size"
345+ fGetObject bucket object destFile defaultGetObjectOptions
346+ gotSize <- withNewHandle destFile getFileSize
347+ liftIO $
348+ gotSize
349+ == Right (Just mb15)
350+ @? "Wrong file size of put file after getting"
351+
352+ step "Cleanup actions"
353+ removeObject bucket object
354+
355+putObjectSizeTest :: TestTree
356+putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
357+ \step bucket -> do
358+ -- putObject test (conduit source, size specified)
359+ let obj = "msingle"
360+ mb1 = 1 * 1024 * 1024
329361
330− , funTestWithBucket "copyObject related tests" $ \step bucket -> do
331− step "copyObjectSingle basic tests"
332− let object = "xxx"
333− objCopy = "xxxCopy"
334− size1 = 100 :: Int64
362+ step "Prepare for putObject with from source with size."
363+ rFile <- mkRandFile mb1
335364
336− step "create server object to copy"
337− inputFile <- mkRandFile size1
338− fPutObject bucket object inputFile
365+ step "Upload single file."
366+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
339367
340− step "copy object"
341− let cps = def { cpSource = format "/{}/{}" [bucket, object] }
342− (etag, modTime) <- copyObjectSingle bucket objCopy cps []
368+ step "Retrieve and verify file size"
369+ destFile <- mkRandFile 0
370+ fGetObject bucket obj destFile defaultGetObjectOptions
371+ gotSize <- withNewHandle destFile getFileSize
372+ liftIO $
373+ gotSize
374+ == Right (Just mb1)
375+ @? "Wrong file size of put file after getting"
343376
344− -- retrieve obj info to check
345− ObjectInfo _ t e s <- headObject bucket objCopy
377+ step "Cleanup actions"
378+ deleteObject bucket obj
346379
347− let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
380+putObjectNoSizeTest :: TestTree
381+putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
382+ \step bucket -> do
383+ -- putObject test (conduit source, no size specified)
384+ let obj = "mpart"
385+ mb70 = 70 * 1024 * 1024
348386
349− liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
350− "Copied object did not match expected."
387+ step "Prepare for putObject with from source without providing size."
388+ rFile <- mkRandFile mb70
351389
352− step "cleanup actions"
353− removeObject bucket object
354− removeObject bucket objCopy
390+ step "Upload multipart file."
391+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
355392
356− step "copyObjectPart basic tests"
357− let srcObj = "XXX"
358− copyObj = "XXXCopy"
393+ step "Retrieve and verify file size"
394+ destFile <- mkRandFile 0
395+ fGetObject bucket obj destFile defaultGetObjectOptions
396+ gotSize <- withNewHandle destFile getFileSize
397+ liftIO $
398+ gotSize
399+ == Right (Just mb70)
400+ @? "Wrong file size of put file after getting"
359401
360− step "Prepare"
361− let mb15 = 15 * 1024 * 1024
362− mb5 = 5 * 1024 * 1024
363− randFile <- mkRandFile mb15
364− fPutObject bucket srcObj randFile
402+ step "Cleanup actions"
403+ deleteObject bucket obj
365404
366− step "create new multipart upload"
367− uid <- newMultipartUpload bucket copyObj []
405+highLevelListingTest :: TestTree
406+highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
407+ \step bucket -> do
408+ step "High-level listObjects Test"
409+ step "put 3 objects"
410+ let extractObjectsFromList =
411+ mapM
412+ ( \case
413+ ListItemObject o -> Just $ oiObject o
414+ _ -> Nothing
415+ )
416+ extractObjectsAndDirsFromList =
417+ map
418+ ( \case
419+ ListItemObject o -> oiObject o
420+ ListItemPrefix d -> d
421+ )
422+ expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
423+ expectedNonRecList = ["o4", "dir/"]
424+
425+ testFilepath <- mkRandFile 200
426+ forM_ expectedObjects $
427+ \obj -> fPutObject bucket obj testFilepath defaultPutObjectOptions
428+
429+ step "High-level listing of objects"
430+ items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
431+ liftIO $
432+ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
433+ extractObjectsAndDirsFromList items
434+
435+ step "High-level recursive listing of objects"
436+ objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
437+
438+ liftIO
439+ $ assertEqual
440+ "Objects match failed!"
441+ (Just $ sort expectedObjects)
442+ $ extractObjectsFromList objects
443+
444+ step "High-level listing of objects (version 1)"
445+ itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
446+ liftIO $
447+ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
448+ extractObjectsAndDirsFromList itemsV1
449+
450+ step "High-level recursive listing of objects (version 1)"
451+ objectsV1 <-
452+ C.runConduit $
453+ listObjectsV1 bucket Nothing True
454+ C..| sinkList
455+
456+ liftIO
457+ $ assertEqual
458+ "Objects match failed!"
459+ (Just $ sort expectedObjects)
460+ $ extractObjectsFromList objectsV1
461+
462+ let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
463+ expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
464+ step "High-level listing with prefix"
465+ prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
466+ liftIO
467+ $ assertEqual
468+ "Objects/Dirs under prefix match failed!"
469+ expectedPrefListing
470+ $ extractObjectsAndDirsFromList prefItems
471+
472+ step "High-level listing with prefix recursive"
473+ prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
474+ liftIO
475+ $ assertEqual
476+ "Objects/Dirs under prefix match recursive failed!"
477+ expectedPrefListingRec
478+ $ extractObjectsFromList prefItemsRec
479+
480+ step "High-level listing with prefix (version 1)"
481+ prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
482+ liftIO
483+ $ assertEqual
484+ "Objects/Dirs under prefix match failed!"
485+ expectedPrefListing
486+ $ extractObjectsAndDirsFromList prefItemsV1
487+
488+ step "High-level listing with prefix recursive (version 1)"
489+ prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
490+ liftIO
491+ $ assertEqual
492+ "Objects/Dirs under prefix match recursive failed!"
493+ expectedPrefListingRec
494+ $ extractObjectsFromList prefItemsRecV1
495+
496+ step "Cleanup actions"
497+ forM_ expectedObjects $
498+ \obj -> removeObject bucket obj
499+
500+ step "High-level listIncompleteUploads Test"
501+ let object = "newmpupload"
502+ step "create 10 multipart uploads"
503+ forM_ [1 .. 10 :: Int] $ \_ -> do
504+ uid <- newMultipartUpload bucket object []
368505 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
369506
370− step "put object parts 1-3"
371− let cps' = def {cpSource = format "/{}/{}" [bucket, srcObj]}
372− parts <- forM [1..3] $ \p -> do
373− (etag', _) <- copyObjectPart bucket copyObj cps'{
374− cpSourceRange = Just ((p-1)*mb5, (p-1)*mb5 + (mb5 - 1))
375− } uid (fromIntegral p) []
376− return (fromIntegral p, etag')
377−
378− step "complete multipart"
379− void $ completeMultipartUpload bucket copyObj uid parts
380−
381− step "verify copied object size"
382− (ObjectInfo _ _ _ s') <- headObject bucket copyObj
383−
384− liftIO $ (s' == mb15) @? "Size failed to match"
385−
386− step "Cleanup actions"
387− removeObject bucket srcObj
388− removeObject bucket copyObj
389−
390− step "copyObject basic tests"
391− let srcs = ["XXX", "XXXL"]
392− copyObjs = ["XXXCopy", "XXXLCopy"]
393− sizes = map (* (1024 * 1024)) [15, 65]
394−
395− step "Prepare"
396− forM_ (zip srcs sizes) $ \(src, size) ->
397− fPutObject bucket src =<< mkRandFile size
398−
399− step "make small and large object copy"
400− forM_ (zip copyObjs srcs) $ \(cp, src) ->
401− copyObject bucket cp def{cpSource = format "/{}/{}" [bucket, src]}
402−
403− step "verify uploaded objects"
404− uploadedSizes <- fmap oiSize <$> forM copyObjs (headObject bucket)
405−
406− liftIO $ (sizes == uploadedSizes) @? "Uploaded obj sizes failed to match"
407−
408− forM_ (srcs ++ copyObjs) (removeObject bucket)
409−
410− step "copyObject with offset test "
411− let src = "XXX"
412− size = 15 * 1024 * 1024
413−
414− step "Prepare"
415− fPutObject bucket src =<< mkRandFile size
416−
417− step "copy last 10MiB of object"
418− copyObject bucket copyObj def{
419− cpSource = format "/{}/{}" [bucket, src]
420− , cpSourceRange = Just (5 * 1024 * 1024, size - 1)
421− }
422−
423− step "verify uploaded object"
424− cSize <- oiSize <$> headObject bucket copyObj
425−
426− liftIO $ (cSize == 10 * 1024 * 1024) @? "Uploaded obj size mismatched!"
427−
428− forM_ [src, copyObj] (removeObject bucket)
429−
430− , presignedUrlFunTest
431− , presignedPostPolicyFunTest
432− ]
433−
434−basicTests :: TestTree
435−basicTests = funTestWithBucket "Basic tests" $ \step bucket -> do
436− step "getService works and contains the test bucket."
437− buckets <- getService
438− unless (length (filter (== bucket) $ map biName buckets) == 1) $
439− liftIO $
440− assertFailure ("The bucket " ++ show bucket ++
441− " was expected to exist.")
442−
443− step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
444− mbE <- MC.try $ makeBucket bucket Nothing
445− case mbE of
446− Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
447− _ -> return ()
448−
449− step "makeBucket with an invalid bucket name and check for appropriate exception."
450− invalidMBE <- MC.try $ makeBucket "invalidBucketName" Nothing
451− case invalidMBE of
452− Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
453− _ -> return ()
454−
455− step "getLocation works"
456− region <- getLocation bucket
457− liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
458−
459− step "singlepart putObject works"
460− fPutObject bucket "lsb-release" "/etc/lsb-release"
461−
462− step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
463− fpE <- MC.try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release"
464− case fpE of
465− Left exn -> liftIO $ exn @?= NoSuchBucket
466− _ -> return ()
467−
468− outFile <- mkRandFile 0
469− step "simple fGetObject works"
470− fGetObject bucket "lsb-release" outFile
471−
472− step "fGetObject a non-existent object and check for NoSuchKey exception"
473− resE <- MC.try $ fGetObject bucket "noSuchKey" outFile
474− case resE of
475− Left exn -> liftIO $ exn @?= NoSuchKey
476− _ -> return ()
507+ step "High-level listing of incomplete multipart uploads"
508+ uploads <-
509+ C.runConduit $
510+ listIncompleteUploads bucket (Just "newmpupload") True
511+ C..| sinkList
512+ liftIO $ length uploads @?= 10
477513
514+ step "cleanup"
515+ forM_ uploads $ \(UploadInfo _ uid _ _) ->
516+ abortMultipartUpload bucket object uid
478517
479− step "create new multipart upload works"
480− uid <- newMultipartUpload bucket "newmpupload" []
481− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
518+ step "High-level listIncompleteParts Test"
519+ let mb5 = 5 * 1024 * 1024
482520
483− step "abort a new multipart upload works"
484− abortMultipartUpload bucket "newmpupload" uid
521+ step "create a multipart upload"
522+ uid <- newMultipartUpload bucket object []
523+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
485524
486− step "delete object works"
487− deleteObject bucket "lsb-release"
525+ step "put object parts 1..10"
526+ inputFile <- mkRandFile mb5
527+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
528+ forM_ [1 .. 10] $ \pnum ->
529+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
488530
489− step "statObject test"
490− let object = "sample"
491− step "create an object"
492− inputFile <- mkRandFile 0
493− fPutObject bucket object inputFile
531+ step "fetch list parts"
532+ incompleteParts <-
533+ C.runConduit $
534+ listIncompleteParts bucket object uid
535+ C..| sinkList
536+ liftIO $ length incompleteParts @?= 10
494537
495− step "get metadata of the object"
496− res <- statObject bucket object
497− liftIO $ (oiSize res) @?= 0
538+ step "cleanup"
539+ abortMultipartUpload bucket object uid
498540
499− step "delete object"
500− deleteObject bucket object
541+listingTest :: TestTree
542+listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
543+ step "listObjects' test"
544+ step "put 10 objects"
545+ let objects = (\s -> T.concat ["test-file-", T.pack (show s)]) <$> [1 .. 10 :: Int]
546+
547+ testFilepath <- mkRandFile 200
548+ forM_ [1 .. 10 :: Int] $ \s ->
549+ fPutObject bucket (T.concat ["test-file-", T.pack (show s)]) testFilepath defaultPutObjectOptions
550+
551+ step "Simple list"
552+ res <- listObjects' bucket Nothing Nothing Nothing Nothing
553+ let expectedObjects = sort objects
554+ liftIO $
555+ assertEqual
556+ "Objects match failed!"
557+ expectedObjects
558+ (map oiObject $ lorObjects res)
559+
560+ step "Simple list version 1"
561+ resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
562+ let expected =
563+ sort $
564+ map
565+ ( T.concat
566+ . ("test-file-" :)
567+ . (: [])
568+ . T.pack
569+ . show
570+ )
571+ [1 .. 10 :: Int]
572+ liftIO $
573+ assertEqual
574+ "Objects match failed!"
575+ expected
576+ (map oiObject $ lorObjects' resV1)
577+
578+ step "Cleanup actions"
579+ forM_ objects $ \obj -> deleteObject bucket obj
580+
581+ step "listIncompleteUploads' test"
582+ step "create 10 multipart uploads"
583+ let object = "newmpupload"
584+ forM_ [1 .. 10 :: Int] $ \_ -> do
585+ uid <- newMultipartUpload bucket object []
586+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
587+
588+ step "list incomplete multipart uploads"
589+ incompleteUploads <-
590+ listIncompleteUploads'
591+ bucket
592+ (Just "newmpupload")
593+ Nothing
594+ Nothing
595+ Nothing
596+ Nothing
597+ liftIO $ length (lurUploads incompleteUploads) @?= 10
598+
599+ step "cleanup"
600+ forM_ (lurUploads incompleteUploads) $
601+ \(_, uid, _) -> abortMultipartUpload bucket object uid
602+
603+ step "Basic listIncompleteParts Test"
604+ let mb5 = 5 * 1024 * 1024
605+
606+ step "create a multipart upload"
607+ uid <- newMultipartUpload bucket object []
608+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
609+
610+ step "put object parts 1..10"
611+ inputFile <- mkRandFile mb5
612+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
613+ forM_ [1 .. 10] $ \pnum ->
614+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
615+
616+ step "fetch list parts"
617+ listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
618+ liftIO $ length (lprParts listPartsResult) @?= 10
619+ abortMultipartUpload bucket object uid
501620
502621presignedUrlFunTest :: TestTree
503622presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -509,97 +628,120 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
509628 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
510629
511630 step "PUT object presigned URL - makePresignedUrl"
512− putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
513− (Just obj) (Just "us-east-1") [] []
514−
631+ putUrl <-
632+ makePresignedUrl
633+ 3600
634+ HT.methodPut
635+ (Just bucket)
636+ (Just obj)
637+ (Just "us-east-1")
638+ []
639+ []
640+
641+ -- print putUrl
515642 let size1 = 1000 :: Int64
516643 inputFile <- mkRandFile size1
517644
518645 -- attempt to upload using the presigned URL
519646 putResp <- putR size1 inputFile mgr putUrl
520− liftIO $ (NC.responseStatus putResp == HT.status200) @?
521− "presigned PUT failed"
647+ liftIO $
648+ (NC.responseStatus putResp == HT.status200)
649+ @? "presigned PUT failed"
522650
523651 step "GET object presigned URL - makePresignedUrl"
524− getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
525− (Just obj) (Just "us-east-1") [] []
652+ getUrl <-
653+ makePresignedUrl
654+ 3600
655+ HT.methodGet
656+ (Just bucket)
657+ (Just obj)
658+ (Just "us-east-1")
659+ []
660+ []
526661
527662 getResp <- getR mgr getUrl
528− liftIO $ (NC.responseStatus getResp == HT.status200) @?
529− "presigned GET failed"
663+ liftIO $
664+ (NC.responseStatus getResp == HT.status200)
665+ @? "presigned GET failed"
530666
531667 -- read content from file to compare with response above
532− bs <- CB.sourceFile inputFile $$ CB.sinkLbs
533− liftIO $ (bs == NC.responseBody getResp) @?
534− "presigned put and get got mismatched data"
668+ bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
669+ liftIO $
670+ (bs == NC.responseBody getResp)
671+ @? "presigned put and get got mismatched data"
535672
536673 step "PUT object presigned - presignedPutObjectURL"
537− putUrl2 <- presignedPutObjectUrl bucket obj2 3600 []
674+ putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
538675
539676 let size2 = 1200
540677 testFile <- mkRandFile size2
541678
542679 putResp2 <- putR size2 testFile mgr putUrl2
543− liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
544− "presigned PUT failed (presignedPutObjectUrl)"
680+ liftIO $
681+ (NC.responseStatus putResp2 == HT.status200)
682+ @? "presigned PUT failed (presignedPutObjectUrl)"
545683
546684 step "HEAD object presigned URL - presignedHeadObjectUrl"
547685 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
548686
549− headResp <- do req <- NC.parseRequest $ toS headUrl
550− NC.httpLbs (req {NC.method = HT.methodHead}) mgr
551− liftIO $ (NC.responseStatus headResp == HT.status200) @?
552− "presigned HEAD failed (presignedHeadObjectUrl)"
687+ headResp <- do
688+ let req = NC.parseRequest_ $ decodeUtf8 headUrl
689+ NC.httpLbs (req {NC.method = HT.methodHead}) mgr
690+ liftIO $
691+ (NC.responseStatus headResp == HT.status200)
692+ @? "presigned HEAD failed (presignedHeadObjectUrl)"
553693
554694 -- check that header info is accurate
555− let h = Map.fromList $ NC.responseHeaders headResp
556− cLen = Map.findWithDefault "0" HT.hContentLength h
557− liftIO $ (cLen == show size2) @? "Head req returned bad content length"
695+ let h = H.fromList $ NC.responseHeaders headResp
696+ cLen = H.lookupDefault "0" HT.hContentLength h
697+ liftIO $ (cLen == showBS size2) @? "Head req returned bad content length"
558698
559699 step "GET object presigned URL - presignedGetObjectUrl"
560700 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
561701
562702 getResp2 <- getR mgr getUrl2
563− liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
564− "presigned GET failed (presignedGetObjectUrl)"
703+ liftIO $
704+ (NC.responseStatus getResp2 == HT.status200)
705+ @? "presigned GET failed (presignedGetObjectUrl)"
565706
566707 -- read content from file to compare with response above
567− bs2 <- CB.sourceFile testFile $$ CB.sinkLbs
568− liftIO $ (bs2 == NC.responseBody getResp2) @?
569− "presigned put and get got mismatched data (presigned*Url)"
570−
708+ bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
709+ liftIO $
710+ (bs2 == NC.responseBody getResp2)
711+ @? "presigned put and get got mismatched data (presigned*Url)"
571712
572713 mapM_ (removeObject bucket) [obj, obj2]
573714 where
574715 putR size filePath mgr url = do
575− req <- NC.parseRequest $ toS url
576− let req' = req { NC.method = HT.methodPut
577− , NC.requestBody = NC.requestBodySource size $
578− CB.sourceFile filePath}
716+ let req = NC.parseRequest_ $ decodeUtf8 url
717+ let req' =
718+ req
719+ { NC.method = HT.methodPut,
720+ NC.requestBody =
721+ NC.requestBodySource size $
722+ CB.sourceFile filePath
723+ }
579724 NC.httpLbs req' mgr
580−
581725 getR mgr url = do
582− req <- NC.parseRequest $ toS url
726+ let req = NC.parseRequest_ $ decodeUtf8 url
583727 NC.httpLbs req mgr
584728
585729presignedPostPolicyFunTest :: TestTree
586−presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
730+presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
587731 \step bucket -> do
588−
589732 step "presignedPostPolicy basic test"
590− now <- liftIO $ Time.getCurrentTime
733+ now <- liftIO Time.getCurrentTime
591734
592735 let key = "presignedPostPolicyTest/myfile"
593− policyConds = [ ppCondBucket bucket
594− , ppCondKey key
595− , ppCondContentLengthRange 1 1000
596− , ppCondContentType "application/octet-stream"
597− , ppCondSuccessActionStatus 200
598− ]
599−
736+ policyConds =
737+ [ ppCondBucket bucket,
738+ ppCondKey key,
739+ ppCondContentLengthRange 1 1000,
740+ ppCondContentType "application/octet-stream",
741+ ppCondSuccessActionStatus 200
742+ ]
600743 expirationTime = Time.addUTCTime 3600 now
601744 postPolicyE = newPostPolicy expirationTime policyConds
602−
603745 size = 1000 :: Int64
604746
605747 inputFile <- mkRandFile size
@@ -610,17 +752,532 @@ presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
610752 (url, formData) <- presignedPostPolicy postPolicy
611753 -- liftIO (print url) >> liftIO (print formData)
612754 result <- liftIO $ postForm url formData inputFile
613− liftIO $ (NC.responseStatus result == HT.status200) @?
614− "presigned POST failed"
755+ liftIO $
756+ (NC.responseStatus result == HT.status200)
757+ @? "presigned POST failed"
615758
616759 mapM_ (removeObject bucket) [key]
617− where
618−
619− postForm url formData inputFile = do
620− req <- NC.parseRequest $ toS url
621− let parts = map (\(x, y) -> Form.partBS x y) $
622− Map.toList formData
623− parts' = parts ++ [Form.partFile "file" inputFile]
624− req' <- Form.formDataBody parts' req
625− mgr <- NC.newManager NC.tlsManagerSettings
626− NC.httpLbs req' mgr
760+ where
761+ postForm url formData inputFile = do
762+ req <- NC.parseRequest $ decodeUtf8 url
763+ let parts =
764+ map (uncurry Form.partBS) $
765+ H.toList formData
766+ parts' = parts ++ [Form.partFile "file" inputFile]
767+ req' <- Form.formDataBody parts' req
768+ mgr <- NC.newManager NC.tlsManagerSettings
769+ NC.httpLbs req' mgr
770+
771+bucketPolicyFunTest :: TestTree
772+bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
773+ \step bucket -> do
774+ step "bucketPolicy basic test - no policy exception"
775+ resE <- try $ getBucketPolicy bucket
776+ case resE of
777+ Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
778+ _ -> return ()
779+
780+ resE' <- try $ setBucketPolicy bucket T.empty
781+ case resE' of
782+ Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
783+ _ -> return ()
784+
785+ let expectedPolicyJSON = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket/*\"]}]}"
786+
787+ step "try a malformed policy, expect error"
788+ resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
789+ case resE'' of
790+ Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
791+ _ -> return ()
792+
793+ let expectedPolicyJSON' = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "/*\"]}]}"
794+
795+ step "set bucket policy"
796+ setBucketPolicy bucket expectedPolicyJSON'
797+
798+ let obj = "myobject"
799+
800+ step "verify bucket policy: (1) create `myobject`"
801+ putObject bucket obj (replicateC 100 "c") Nothing defaultPutObjectOptions
802+
803+ step "verify bucket policy: (2) get `myobject` anonymously"
804+ connInfo <- asks mcConnInfo
805+ let proto = bool "http://" "https://" $ connectIsSecure connInfo
806+ url =
807+ BS.concat
808+ [ proto,
809+ getHostAddr connInfo,
810+ "/",
811+ encodeUtf8 bucket,
812+ "/",
813+ encodeUtf8 obj
814+ ]
815+ respE <-
816+ liftIO $
817+ fmap (Right . toStrictBS) (NC.simpleHttp $ decodeUtf8 url)
818+ `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
819+ case respE of
820+ Left err -> liftIO $ assertFailure $ show err
821+ Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
822+
823+ deleteObject bucket obj
824+
825+ step "delete bucket policy"
826+ setBucketPolicy bucket T.empty
827+
828+multipartTest :: TestTree
829+multipartTest = funTestWithBucket "Multipart Tests" $
830+ \step bucket -> do
831+ -- Commenting out test since it's platform specific.
832+ -- FIXME: Need to find a platform agnostic way to test this.
833+ -- step "Prepare for putObjectInternal with non-seekable file, with size."
834+ -- step "Upload multipart file."
835+ -- let mb80 = 80 * 1024 * 1024
836+ -- obj = "mpart"
837+
838+ -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
839+
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"
847+
848+ -- step "Cleanup actions"
849+ -- removeObject bucket obj
850+
851+ -- step "cleanup"
852+ -- removeObject bucket "big"
853+
854+ step "Prepare for removeIncompleteUpload"
855+ -- low-level multipart operation tests.
856+ let object = "newmpupload"
857+ kb5 = 5 * 1024
858+
859+ step "create new multipart upload"
860+ uid <- newMultipartUpload bucket object []
861+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
862+
863+ randFile <- mkRandFile kb5
864+
865+ step "upload 2 parts"
866+ forM_ [1, 2] $ \partNum -> do
867+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
868+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
869+
870+ step "remove ongoing upload"
871+ removeIncompleteUpload bucket object
872+ uploads <-
873+ C.runConduit $
874+ listIncompleteUploads bucket (Just object) False
875+ C..| sinkList
876+ liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
877+
878+putObjectContentTypeTest :: TestTree
879+putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
880+ \step bucket -> do
881+ step "fPutObject content type test"
882+ let object = "xxx-content-type"
883+ size1 = 100 :: Int64
884+
885+ step "create server object with content-type"
886+ inputFile <- mkRandFile size1
887+ fPutObject
888+ bucket
889+ object
890+ inputFile
891+ defaultPutObjectOptions
892+ { pooContentType = Just "application/javascript"
893+ }
894+
895+ -- retrieve obj info to check
896+ oi <- headObject bucket object []
897+ let m = oiMetadata oi
898+
899+ step "Validate content-type"
900+ liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
901+
902+ step "upload object with content-encoding set to identity"
903+ fPutObject
904+ bucket
905+ object
906+ inputFile
907+ defaultPutObjectOptions
908+ { pooContentEncoding = Just "identity"
909+ }
910+
911+ oiCE <- headObject bucket object []
912+ let m' = oiMetadata oiCE
913+
914+ step "Validate content-encoding"
915+ liftIO $
916+ assertEqual
917+ "Content-Encoding did not match"
918+ (Just "identity")
919+ (H.lookup "Content-Encoding" m')
920+
921+ step "Cleanup actions"
922+
923+ removeObject bucket object
924+
925+putObjectContentLanguageTest :: TestTree
926+putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage tests" $
927+ \step bucket -> do
928+ step "fPutObject content language test"
929+ let object = "xxx-content-language"
930+ size1 = 100 :: Int64
931+
932+ step "create server object with content-language"
933+ inputFile <- mkRandFile size1
934+ fPutObject
935+ bucket
936+ object
937+ inputFile
938+ defaultPutObjectOptions
939+ { pooContentLanguage = Just "en-US"
940+ }
941+
942+ -- retrieve obj info to check
943+ oi <- headObject bucket object []
944+ let m = oiMetadata oi
945+
946+ step "Validate content-language"
947+ liftIO $
948+ assertEqual
949+ "content-language did not match"
950+ (Just "en-US")
951+ (H.lookup "Content-Language" m)
952+ step "Cleanup actions"
953+
954+ removeObject bucket object
955+
956+putObjectUserMetadataTest :: TestTree
957+putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
958+ \step bucket -> do
959+ step "putObject user-metadata test"
960+ let object = "object-with-metadata"
961+ size1 = 100 :: Int64
962+
963+ step "create server object with usermetdata"
964+ inputFile <- mkRandFile size1
965+
966+ fPutObject
967+ bucket
968+ object
969+ inputFile
970+ defaultPutObjectOptions
971+ { pooUserMetadata =
972+ [ ("x-Amz-meta-mykey1", "myval1"),
973+ ("mykey2", "myval2")
974+ ]
975+ }
976+
977+ step "Validate user-metadata"
978+ -- retrieve obj info to check
979+ oi <- headObject bucket object []
980+ let m = oiUserMetadata oi
981+ -- need to do a case-insensitive comparison
982+ sortedMeta =
983+ sort $
984+ map (bimap T.toLower T.toLower) $
985+ H.toList m
986+ ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
987+
988+ liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
989+
990+ step "Cleanup actions"
991+ removeObject bucket object
992+
993+getObjectTest :: TestTree
994+getObjectTest = funTestWithBucket "getObject test" $
995+ \step bucket -> do
996+ step "putObject with some metadata"
997+ let object = "object-with-metadata"
998+ size1 = 100 :: Int64
999+
1000+ inputFile <- mkRandFile size1
1001+ fPutObject
1002+ bucket
1003+ object
1004+ inputFile
1005+ defaultPutObjectOptions
1006+ { pooUserMetadata =
1007+ [ ("x-Amz-meta-mykey1", "myval1"),
1008+ ("mykey2", "myval2")
1009+ ]
1010+ }
1011+
1012+ step "get the object - check the metadata matches"
1013+ -- retrieve obj info to check
1014+ gor <- getObject bucket object defaultGetObjectOptions
1015+ let m = oiUserMetadata $ gorObjectInfo gor
1016+ -- need to do a case-insensitive comparison
1017+ sortedMeta =
1018+ sort $
1019+ map (bimap T.toLower T.toLower) $
1020+ H.toList m
1021+ ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
1022+
1023+ liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
1024+
1025+ step "get the object content"
1026+ getObjectHash <- hashSHA256FromSource $ gorObjectStream gor
1027+ inputHash <- hashSHA256FromSource $ C.sourceFile inputFile
1028+ liftIO $ (getObjectHash == inputHash) @? "Input file and output file mismatched!"
1029+
1030+ step "Cleanup actions"
1031+ removeObject bucket object
1032+
1033+putObjectStorageClassTest :: TestTree
1034+putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
1035+ \step bucket -> do
1036+ step "fPutObject storage class test"
1037+ let object = "xxx-storage-class-standard"
1038+ object' = "xxx-storage-class-reduced"
1039+ object'' = "xxx-storage-class-invalid"
1040+ size1 = 100 :: Int64
1041+ size0 = 0 :: Int64
1042+
1043+ step "create server objects with storageClass"
1044+ inputFile <- mkRandFile size1
1045+ inputFile' <- mkRandFile size1
1046+ inputFile'' <- mkRandFile size0
1047+
1048+ fPutObject
1049+ bucket
1050+ object
1051+ inputFile
1052+ defaultPutObjectOptions
1053+ { pooStorageClass = Just "STANDARD"
1054+ }
1055+
1056+ fPutObject
1057+ bucket
1058+ object'
1059+ inputFile'
1060+ defaultPutObjectOptions
1061+ { pooStorageClass = Just "REDUCED_REDUNDANCY"
1062+ }
1063+
1064+ removeObject bucket object
1065+
1066+ -- retrieve obj info to check
1067+ oi' <- headObject bucket object' []
1068+ let m' = oiMetadata oi'
1069+
1070+ step "Validate x-amz-storage-class rrs"
1071+ liftIO $
1072+ assertEqual
1073+ "storageClass did not match"
1074+ (Just "REDUCED_REDUNDANCY")
1075+ (H.lookup "X-Amz-Storage-Class" m')
1076+
1077+ fpE <-
1078+ try $
1079+ fPutObject
1080+ bucket
1081+ object''
1082+ inputFile''
1083+ defaultPutObjectOptions
1084+ { pooStorageClass = Just "INVALID_STORAGE_CLASS"
1085+ }
1086+ case fpE of
1087+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
1088+ _ -> return ()
1089+
1090+ step "Cleanup actions"
1091+
1092+ removeObject bucket object'
1093+
1094+copyObjectTests :: TestTree
1095+copyObjectTests = funTestWithBucket "copyObject related tests" $
1096+ \step bucket -> do
1097+ step "copyObjectSingle basic tests"
1098+ let object = "xxx"
1099+ objCopy = "xxxCopy"
1100+ size1 = 100 :: Int64
1101+
1102+ step "create server object to copy"
1103+ inputFile <- mkRandFile size1
1104+ fPutObject bucket object inputFile defaultPutObjectOptions
1105+
1106+ step "copy object"
1107+ let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
1108+ (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
1109+
1110+ -- retrieve obj info to check
1111+ oi <- headObject bucket objCopy []
1112+ let t = oiModTime oi
1113+ let e = oiETag oi
1114+ let s = oiSize oi
1115+
1116+ let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
1117+
1118+ liftIO $
1119+ (s == size1 && e == etag && isMTimeDiffOk)
1120+ @? "Copied object did not match expected."
1121+
1122+ step "cleanup actions"
1123+ removeObject bucket object
1124+ removeObject bucket objCopy
1125+
1126+ step "copyObjectPart basic tests"
1127+ let srcObj = "XXX"
1128+ copyObj = "XXXCopy"
1129+
1130+ step "Prepare"
1131+ let mb15 = 15 * 1024 * 1024
1132+ mb5 = 5 * 1024 * 1024
1133+ randFile <- mkRandFile mb15
1134+ fPutObject bucket srcObj randFile defaultPutObjectOptions
1135+
1136+ step "create new multipart upload"
1137+ uid <- newMultipartUpload bucket copyObj []
1138+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
1139+
1140+ step "put object parts 1-3"
1141+ let srcInfo' = defaultSourceInfo {srcBucket = bucket, srcObject = srcObj}
1142+ dstInfo' = defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1143+ parts <- forM [1 .. 3] $ \p -> do
1144+ (etag', _) <-
1145+ copyObjectPart
1146+ dstInfo'
1147+ srcInfo'
1148+ { srcRange = Just $ (,) ((p - 1) * mb5) ((p - 1) * mb5 + (mb5 - 1))
1149+ }
1150+ uid
1151+ (fromIntegral p)
1152+ []
1153+ return (fromIntegral p, etag')
1154+
1155+ step "complete multipart"
1156+ void $ completeMultipartUpload bucket copyObj uid parts
1157+
1158+ step "verify copied object size"
1159+ oi' <- headObject bucket copyObj []
1160+ let s' = oiSize oi'
1161+
1162+ liftIO $ (s' == mb15) @? "Size failed to match"
1163+
1164+ step "Cleanup actions"
1165+ removeObject bucket srcObj
1166+ removeObject bucket copyObj
1167+
1168+ step "copyObject basic tests"
1169+ let srcs = ["XXX", "XXXL"]
1170+ copyObjs = ["XXXCopy", "XXXLCopy"]
1171+ sizes = map (* (1024 * 1024)) [15, 65]
1172+
1173+ step "Prepare"
1174+ forM_ (zip srcs sizes) $ \(src, size) -> do
1175+ inputFile' <- mkRandFile size
1176+ fPutObject bucket src inputFile' defaultPutObjectOptions
1177+
1178+ step "make small and large object copy"
1179+ forM_ (zip copyObjs srcs) $ \(cp, src) ->
1180+ copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = cp} defaultSourceInfo {srcBucket = bucket, srcObject = src}
1181+
1182+ step "verify uploaded objects"
1183+ uploadedSizes <- fmap oiSize <$> forM copyObjs (\o -> headObject bucket o [])
1184+
1185+ liftIO $ (sizes == uploadedSizes) @? "Uploaded obj sizes failed to match"
1186+
1187+ forM_ (srcs ++ copyObjs) (removeObject bucket)
1188+
1189+ step "copyObject with offset test "
1190+ let src = "XXX"
1191+ size = 15 * 1024 * 1024
1192+
1193+ step "Prepare"
1194+ inputFile' <- mkRandFile size
1195+ fPutObject bucket src inputFile' defaultPutObjectOptions
1196+
1197+ step "copy last 10MiB of object"
1198+ copyObject
1199+ defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1200+ defaultSourceInfo
1201+ { srcBucket = bucket,
1202+ srcObject = src,
1203+ srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
1204+ }
1205+
1206+ step "verify uploaded object"
1207+ cSize <- oiSize <$> headObject bucket copyObj []
1208+
1209+ liftIO $ (cSize == 10 * 1024 * 1024) @? "Uploaded obj size mismatched!"
1210+
1211+ forM_ [src, copyObj] (removeObject bucket)
1212+
1213+getNPutSSECTest :: TestTree
1214+getNPutSSECTest =
1215+ funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
1216+ -- Skip this test if the server is not using TLS as encryption is
1217+ -- disabled anyway.
1218+ isTLSConn <- asks (connectIsSecure . mcConnInfo)
1219+ if isTLSConn
1220+ then do
1221+ step "Make an encryption key"
1222+ key <- case mkSSECKey $ BS.pack [0 .. 31] of
1223+ Nothing -> liftIO $ assertFailure "This should not happen"
1224+ Just k -> return k
1225+
1226+ let mb1 = 1024 * 1024
1227+ obj = "1"
1228+ step "Upload an object using the encryption key"
1229+ rFile <- mkRandFile mb1
1230+ let putOpts = defaultPutObjectOptions {pooSSE = Just $ SSEC key}
1231+ fPutObject bucket obj rFile putOpts
1232+
1233+ step "Stat object without key - should fail"
1234+ headRes <- try $ statObject bucket obj defaultGetObjectOptions
1235+ case headRes of
1236+ Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
1237+ Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
1238+ | NC.responseStatus rsp == HT.status400 -> return ()
1239+ | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1240+ Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1241+
1242+ step "Get file and check length"
1243+ dstFile <- mkRandFile 0
1244+ let getOpts = defaultGetObjectOptions {gooSSECKey = Just key}
1245+ fGetObject bucket obj dstFile getOpts
1246+
1247+ gotSize <- withNewHandle dstFile getFileSize
1248+ liftIO $
1249+ gotSize
1250+ == Right (Just mb1)
1251+ @? "Wrong file size of object when getting"
1252+
1253+ step "Cleanup"
1254+ deleteObject bucket obj
1255+ else step "Skipping encryption test as server is not using TLS"
1256+
1257+assumeRoleRequestTest :: TestTree
1258+assumeRoleRequestTest = testCaseSteps "Assume Role STS API" $ \step -> do
1259+ step "Load credentials"
1260+ (_, (ak, sk)) <- lookupTestEnv
1261+ val <- Env.lookupEnv "MINIO_LOCAL"
1262+ isSecure <- Env.lookupEnv "MINIO_SECURE"
1263+ let localMinioCred = Just $ CredentialValue ak sk mempty
1264+ playCreds =
1265+ case connectCreds minioPlayCI of
1266+ CredsStatic c -> Just c
1267+ _ -> Nothing
1268+ (cvMay, loc) =
1269+ case (val, isSecure) of
1270+ (Just _, Just _) -> (localMinioCred, "https://localhost:9000")
1271+ (Just _, Nothing) -> (localMinioCred, "http://localhost:9000")
1272+ (Nothing, _) -> (playCreds, "https://play.min.io:9000")
1273+ cv <- maybe (assertFailure "bad creds") return cvMay
1274+ let assumeRole =
1275+ STSAssumeRole cv $
1276+ defaultSTSAssumeRoleOptions
1277+ { saroLocation = Just "us-east-1",
1278+ saroEndpoint = Just loc
1279+ }
1280+ step "AssumeRole request"
1281+ res <- requestSTSCredential assumeRole
1282+ let v = credentialValueText $ fst res
1283+ print (v, snd res)
test/Network/Minio/API/Test.hs+91−25
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,36 +15,102 @@
1515--
1616
1717module Network.Minio.API.Test
18− ( bucketNameValidityTests
19− , objectNameValidityTests
20− ) where
18+ ( bucketNameValidityTests,
19+ objectNameValidityTests,
20+ parseServerInfoJSONTest,
21+ parseHealStatusTest,
22+ parseHealStartRespTest,
23+ )
24+where
2125
26+import Data.Aeson (eitherDecode)
27+import Network.Minio.API
28+import Network.Minio.AdminAPI
2229import Test.Tasty
2330import Test.Tasty.HUnit
2431
25−import Lib.Prelude
26−
27−import Network.Minio.API
28−
32+assertBool' :: Bool -> Assertion
2933assertBool' = assertBool "Test failed!"
3034
3135bucketNameValidityTests :: TestTree
32−bucketNameValidityTests = testGroup "Bucket Name Validity Tests"
33− [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName ""
34− , testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab"
35− , testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
36− , testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD"
37− , testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2"
38− , testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-"
39− , testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg"
40− , testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1"
41− , testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea"
42− , testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d"
43− , testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
44− ]
36+bucketNameValidityTests =
37+ testGroup
38+ "Bucket Name Validity Tests"
39+ [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName "",
40+ testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab",
41+ testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
42+ testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD",
43+ testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2",
44+ testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-",
45+ testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg",
46+ testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1",
47+ testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea",
48+ testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d",
49+ testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
50+ ]
4551
4652objectNameValidityTests :: TestTree
47−objectNameValidityTests = testGroup "Object Name Validity Tests"
48− [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""
49− , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
50− ]
53+objectNameValidityTests =
54+ testGroup
55+ "Object Name Validity Tests"
56+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName "",
57+ testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
58+ ]
59+
60+parseServerInfoJSONTest :: TestTree
61+parseServerInfoJSONTest =
62+ testGroup "Parse MinIO Admin API ServerInfo JSON test" $
63+ map
64+ ( \(tName, tDesc, tfn, tVal) ->
65+ testCase tName $
66+ assertBool tDesc $
67+ tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
68+ )
69+ testCases
70+ where
71+ testCases =
72+ [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON),
73+ ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON),
74+ ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
75+ ]
76+ fsJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":20530,\"Backend\":{\"Type\":1,\"OnlineDisks\":0,\"OfflineDisks\":0,\"StandardSCData\":0,\"StandardSCParity\":0,\"RRSCData\":0,\"RRSCParity\":0,\"Sets\":null}},\"network\":{\"transferred\":808,\"received\":1160},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":5992503019270,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
77+ erasureJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":2,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
78+ invalidJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":42,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
79+
80+parseHealStatusTest :: TestTree
81+parseHealStatusTest =
82+ testGroup "Parse MinIO Admin API HealStatus JSON test" $
83+ map
84+ ( \(tName, tDesc, tfn, tVal) ->
85+ testCase tName $
86+ assertBool tDesc $
87+ tfn (eitherDecode tVal :: Either [Char] HealStatus)
88+ )
89+ testCases
90+ where
91+ testCases =
92+ [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON'),
93+ ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON'),
94+ ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
95+ ]
96+ erasureJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
97+ invalidJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]"
98+ invalidItemType = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"hello\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
99+
100+parseHealStartRespTest :: TestTree
101+parseHealStartRespTest =
102+ testGroup "Parse MinIO Admin API HealStartResp JSON test" $
103+ map
104+ ( \(tName, tDesc, tfn, tVal) ->
105+ testCase tName $
106+ assertBool tDesc $
107+ tfn (eitherDecode tVal :: Either [Char] HealStartResp)
108+ )
109+ testCases
110+ where
111+ testCases =
112+ [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON),
113+ ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
114+ ]
115+ hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
116+ missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
test/Network/Minio/JsonParser/Test.hsnew file+63
@@ -0,0 +1,63 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018 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.JsonParser.Test
18+ ( jsonParserTests,
19+ )
20+where
21+
22+import Lib.Prelude
23+import Network.Minio.Errors
24+import Network.Minio.JsonParser
25+import Test.Tasty
26+import Test.Tasty.HUnit
27+import UnliftIO (MonadUnliftIO)
28+
29+jsonParserTests :: TestTree
30+jsonParserTests =
31+ testGroup
32+ "JSON Parser Tests"
33+ [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34+ ]
35+
36+tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
37+tryValidationErr = try
38+
39+assertValidationErr :: MErrV -> Assertion
40+assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
41+
42+testParseErrResponseJSON :: Assertion
43+testParseErrResponseJSON = do
44+ -- 1. Test parsing of an invalid error json.
45+ parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
46+ when (isRight parseResE) $
47+ assertFailure $
48+ "Parsing should have failed => " ++ show parseResE
49+
50+ forM_ cases $ \(jsondata, sErr) -> do
51+ parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
52+ either assertValidationErr (@?= sErr) parseErr
53+ where
54+ cases =
55+ [ -- 2. Test parsing of a valid error json.
56+ ( "{\"Code\":\"InvalidAccessKeyId\",\"Message\":\"The access key ID you provided does not exist in our records.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
57+ ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58+ ),
59+ -- 3. Test parsing of a valid, empty Resource.
60+ ( "{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
61+ ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
62+ )
63+ ]
test/Network/Minio/TestHelpers.hsnew file+32
@@ -0,0 +1,32 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018 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.TestHelpers
18+ ( runTestNS,
19+ )
20+where
21+
22+import Network.Minio.Data
23+
24+newtype TestNS = TestNS {testNamespace :: Text}
25+
26+instance HasSvcNamespace TestNS where
27+ getSvcNamespace = testNamespace
28+
29+runTestNS :: ReaderT TestNS m a -> m a
30+runTestNS =
31+ flip runReaderT $
32+ TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs+14−16
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,33 +15,31 @@
1515--
1616
1717module Network.Minio.Utils.Test
18− (
19− limitedMapConcurrentlyTests
20− ) where
18+ ( limitedMapConcurrentlyTests,
19+ )
20+where
2121
22+import Network.Minio.Utils
2223import Test.Tasty
2324import Test.Tasty.HUnit
2425
25−import Lib.Prelude
26−
27−import Network.Minio.Utils
28−
2926limitedMapConcurrentlyTests :: TestTree
30−limitedMapConcurrentlyTests = testGroup "limitedMapConcurrently Tests"
31− [ testCase "Test with various thread counts" testLMC
32− ]
27+limitedMapConcurrentlyTests =
28+ testGroup
29+ "limitedMapConcurrently Tests"
30+ [ testCase "Test with various thread counts" testLMC
31+ ]
3332
3433testLMC :: Assertion
3534testLMC = do
3635 let maxNum = 50
3736 -- test with thread count of 1 to 2*maxNum
38− forM_ [1..(2*maxNum)] $ \threads -> do
39− res <- limitedMapConcurrently threads compute [1..maxNum]
37+ forM_ [1 .. (2 * maxNum)] $ \threads -> do
38+ res <- limitedMapConcurrently threads compute [1 .. maxNum]
4039 sum res @?= overallResultCheck maxNum
4140 where
4241 -- simple function to run in each thread
4342 compute :: Int -> IO Int
44− compute n = return $ sum [1..n]
45−
43+ compute n = return $ sum [1 .. n]
4644 -- function to check overall result
47− overallResultCheck n = sum $ map (\t -> (t * (t+1)) `div` 2) [1..n]
45+ overallResultCheck n = sum $ map (\t -> (t * (t + 1)) `div` 2) [1 .. n]
test/Network/Minio/XmlGenerator/Test.hs+148−61
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,83 +13,170 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16+{-# LANGUAGE QuasiQuotes #-}
1617
1718module Network.Minio.XmlGenerator.Test
18− ( xmlGeneratorTests
19− ) where
19+ ( xmlGeneratorTests,
20+ )
21+where
2022
21−import Test.Tasty
22−import Test.Tasty.HUnit
23−
24−import Lib.Prelude
25−
26−import Data.Default (def)
27−
28−import Network.Minio.Data
29−import Network.Minio.XmlGenerator
30−import Network.Minio.XmlParser (parseNotification)
23+import qualified Data.ByteString.Lazy as LBS
24+import Lib.Prelude
25+import Network.Minio.Data
26+import Network.Minio.TestHelpers
27+import Network.Minio.XmlGenerator
28+import Network.Minio.XmlParser (parseNotification)
29+import Test.Tasty
30+import Test.Tasty.HUnit
31+import Text.RawString.QQ (r)
32+import Text.XML (def, parseLBS)
3133
3234xmlGeneratorTests :: TestTree
33−xmlGeneratorTests = testGroup "XML Generator Tests"
34− [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig
35− , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest
36− , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest
37− ]
35+xmlGeneratorTests =
36+ testGroup
37+ "XML Generator Tests"
38+ [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig,
39+ testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest,
40+ testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest,
41+ testCase "Test mkSelectRequest" testMkSelectRequest
42+ ]
3843
3944testMkCreateBucketConfig :: Assertion
4045testMkCreateBucketConfig = do
46+ let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
4147 assertEqual "CreateBucketConfiguration xml should match: " expected $
42− mkCreateBucketConfig "EU"
48+ mkCreateBucketConfig ns "EU"
4349 where
44− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
45− \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
46− \<LocationConstraint>EU</LocationConstraint>\
47− \</CreateBucketConfiguration>"
50+ expected =
51+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
52+ \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
53+ \<LocationConstraint>EU</LocationConstraint>\
54+ \</CreateBucketConfiguration>"
4855
4956testMkCompleteMultipartUploadRequest :: Assertion
5057testMkCompleteMultipartUploadRequest =
5158 assertEqual "completeMultipartUpload xml should match: " expected $
52− mkCompleteMultipartUploadRequest [(1, "abc")]
59+ mkCompleteMultipartUploadRequest [(1, "abc")]
5360 where
54− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
55− \<CompleteMultipartUpload>\
56− \<Part>\
57− \<PartNumber>1</PartNumber><ETag>abc</ETag>\
58− \</Part>\
59− \</CompleteMultipartUpload>"
61+ expected =
62+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
63+ \<CompleteMultipartUpload>\
64+ \<Part>\
65+ \<PartNumber>1</PartNumber><ETag>abc</ETag>\
66+ \</Part>\
67+ \</CompleteMultipartUpload>"
6068
6169testMkPutNotificationRequest :: Assertion
6270testMkPutNotificationRequest =
6371 forM_ cases $ \val -> do
64− let result = toS $ mkPutNotificationRequest val
65− ntf <- runExceptT $ parseNotification result
66− either (\_ -> assertFailure "XML Parse Error!")
67− (@?= val) ntf
72+ let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
73+ result = fromStrictBS $ mkPutNotificationRequest ns val
74+ ntf <- runExceptT $ runTestNS $ parseNotification result
75+ either
76+ (\_ -> assertFailure "XML Parse Error!")
77+ (@?= val)
78+ ntf
79+ where
80+ cases =
81+ [ Notification
82+ []
83+ [ NotificationConfig
84+ "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
85+ "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
86+ [ReducedRedundancyLostObject, ObjectCreated]
87+ defaultFilter
88+ ]
89+ [],
90+ Notification
91+ [ NotificationConfig
92+ "1"
93+ "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
94+ [ObjectCreatedPut]
95+ ( Filter $
96+ FilterKey $
97+ FilterRules
98+ [ FilterRule "prefix" "images/",
99+ FilterRule "suffix" ".jpg"
100+ ]
101+ ),
102+ NotificationConfig
103+ ""
104+ "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
105+ [ObjectCreated]
106+ defaultFilter
107+ ]
108+ [ NotificationConfig
109+ ""
110+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
111+ [ReducedRedundancyLostObject]
112+ defaultFilter
113+ ]
114+ [ NotificationConfig
115+ "ObjectCreatedEvents"
116+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
117+ [ObjectCreated]
118+ defaultFilter
119+ ]
120+ ]
121+
122+testMkSelectRequest :: Assertion
123+testMkSelectRequest = mapM_ assertFn cases
68124 where
69− cases = [ Notification []
70− [ NotificationConfig
71− "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
72− "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
73− [ReducedRedundancyLostObject, ObjectCreated] def
74− ]
75− []
76− , Notification
77− [ NotificationConfig
78− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
79− [ObjectCreatedPut]
80− (Filter $ FilterKey $ FilterRules
81− [ FilterRule "prefix" "images/"
82− , FilterRule "suffix" ".jpg"])
83− , NotificationConfig
84− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
85− [ObjectCreated] def
86− ]
87− [ NotificationConfig
88− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
89− [ReducedRedundancyLostObject] def
90− ]
91− [ NotificationConfig
92− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
93− [ObjectCreated] def
94− ]
95− ]
125+ assertFn (a, b) =
126+ let generatedReqDoc = parseLBS def $ LBS.fromStrict $ mkSelectRequest a
127+ expectedReqDoc = parseLBS def $ LBS.fromStrict b
128+ in case (generatedReqDoc, expectedReqDoc) of
129+ (Right genDoc, Right expDoc) -> assertEqual "selectRequest XML should match: " expDoc genDoc
130+ (Left err, _) -> assertFailure $ "Generated selectRequest failed to parse as XML" ++ show err
131+ (_, Left err) -> assertFailure $ "Expected selectRequest failed to parse as XML" ++ show err
132+ cases =
133+ [ ( SelectRequest
134+ "Select * from S3Object"
135+ SQL
136+ ( InputSerialization
137+ (Just CompressionTypeGzip)
138+ ( InputFormatCSV $
139+ fileHeaderInfo FileHeaderIgnore
140+ <> recordDelimiter "\n"
141+ <> fieldDelimiter ","
142+ <> quoteCharacter "\""
143+ <> quoteEscapeCharacter "\""
144+ )
145+ )
146+ ( OutputSerializationCSV $
147+ quoteFields QuoteFieldsAsNeeded
148+ <> recordDelimiter "\n"
149+ <> fieldDelimiter ","
150+ <> quoteCharacter "\""
151+ <> quoteEscapeCharacter "\""
152+ )
153+ (Just False),
154+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><FieldDelimiter>,</FieldDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><RecordDelimiter>
155+</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
156+</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
157+ ),
158+ ( setRequestProgressEnabled False $
159+ setInputCompressionType CompressionTypeGzip $
160+ selectRequest
161+ "Select * from S3Object"
162+ documentJsonInput
163+ (outputJSONFromRecordDelimiter "\n"),
164+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter>
165+</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
166+ ),
167+ ( setRequestProgressEnabled False $
168+ setInputCompressionType CompressionTypeNone $
169+ selectRequest
170+ "Select * from S3Object"
171+ defaultParquetInput
172+ ( outputCSVFromProps $
173+ quoteFields QuoteFieldsAsNeeded
174+ <> recordDelimiter "\n"
175+ <> fieldDelimiter ","
176+ <> quoteCharacter "\""
177+ <> quoteEscapeCharacter "\""
178+ ),
179+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
180+</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
181+ )
182+ ]
test/Network/Minio/XmlParser/Test.hs+319−274
@@ -1,5 +1,5 @@
11--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,46 +13,49 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16+{-# LANGUAGE QuasiQuotes #-}
1617
1718module Network.Minio.XmlParser.Test
18− (
19− xmlParserTests
20− ) where
19+ ( xmlParserTests,
20+ )
21+where
2122
22−import qualified Control.Monad.Catch as MC
23−import Data.Time (fromGregorian)
24−import Test.Tasty
25−import Test.Tasty.HUnit
26−
27−import Lib.Prelude
28−
29−import Data.Default (def)
30−
31−import Network.Minio.Data
32−import Network.Minio.Errors
33−import Network.Minio.XmlParser
23+import qualified Data.HashMap.Strict as H
24+import Data.Time (fromGregorian)
25+import Lib.Prelude
26+import Network.Minio.Data
27+import Network.Minio.Errors
28+import Network.Minio.TestHelpers
29+import Network.Minio.XmlParser
30+import Test.Tasty
31+import Test.Tasty.HUnit
32+import Text.RawString.QQ (r)
33+import UnliftIO (MonadUnliftIO)
3434
3535xmlParserTests :: TestTree
36−xmlParserTests = testGroup "XML Parser Tests"
37− [ testCase "Test parseLocation" testParseLocation
38− , testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload
39− , testCase "Test parseListObjectsResponse" testParseListObjectsResult
40− , testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result
41− , testCase "Test parseListUploadsresponse" testParseListIncompleteUploads
42− , testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse
43− , testCase "Test parseListPartsResponse" testParseListPartsResponse
44− , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse
45− , testCase "Test parseNotification" testParseNotification
46− ]
36+xmlParserTests =
37+ testGroup
38+ "XML Parser Tests"
39+ [ testCase "Test parseLocation" testParseLocation,
40+ testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload,
41+ testCase "Test parseListObjectsResponse" testParseListObjectsResult,
42+ testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result,
43+ testCase "Test parseListUploadsresponse" testParseListIncompleteUploads,
44+ testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse,
45+ testCase "Test parseListPartsResponse" testParseListPartsResponse,
46+ testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse,
47+ testCase "Test parseNotification" testParseNotification,
48+ testCase "Test parseSelectProgress" testParseSelectProgress
49+ ]
4750
48−tryValidationErr :: (MC.MonadCatch m) => m a -> m (Either MErrV a)
49−tryValidationErr act = MC.try act
51+tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
52+tryValidationErr = try
5053
5154assertValidtionErr :: MErrV -> Assertion
5255assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
5356
5457eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
55−eitherValidationErr (Left e) _ = assertValidtionErr e
58+eitherValidationErr (Left e) _ = assertValidtionErr e
5659eitherValidationErr (Right a) f = f a
5760
5861testParseLocation :: Assertion
@@ -60,299 +63,341 @@ testParseLocation = do
6063 -- 1. Test parsing of an invalid location constraint xml.
6164 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
6265 when (isRight parseResE) $
63− assertFailure $ "Parsing should have failed => " ++ show parseResE
66+ assertFailure $
67+ "Parsing should have failed => " ++ show parseResE
6468
6569 forM_ cases $ \(xmldata, expectedLocation) -> do
6670 parseLocE <- tryValidationErr $ parseLocation xmldata
6771 either assertValidtionErr (@?= expectedLocation) parseLocE
6872 where
69− cases = [
70− -- 2. Test parsing of a valid location xml.
71− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
72− \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
73− "EU"
74− )
75− ,
76− -- 3. Test parsing of a valid, empty location xml.
77− ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
78− "us-east-1"
79− )
73+ cases =
74+ [ -- 2. Test parsing of a valid location xml.
75+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
76+ \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
77+ "EU"
78+ ),
79+ -- 3. Test parsing of a valid, empty location xml.
80+ ( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
81+ "us-east-1"
82+ )
8083 ]
8184
82−
8385testParseNewMultipartUpload :: Assertion
8486testParseNewMultipartUpload = do
8587 forM_ cases $ \(xmldata, expectedUploadId) -> do
86− parsedUploadIdE <- tryValidationErr $ parseNewMultipartUpload xmldata
88+ parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
8789 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
8890 where
89− cases = [
90− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
91− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
92− \ <Bucket>example-bucket</Bucket>\
93− \ <Key>example-object</Key>\
94− \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
95− \</InitiateMultipartUploadResult>",
96− "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
97− ),
98− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
99− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
100− \ <Bucket>example-bucket</Bucket>\
101− \ <Key>example-object</Key>\
102− \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
103− \</InitiateMultipartUploadResult>",
104− "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
105− )
91+ cases =
92+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
93+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
94+ \ <Bucket>example-bucket</Bucket>\
95+ \ <Key>example-object</Key>\
96+ \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
97+ \</InitiateMultipartUploadResult>",
98+ "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
99+ ),
100+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
101+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
102+ \ <Bucket>example-bucket</Bucket>\
103+ \ <Key>example-object</Key>\
104+ \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
105+ \</InitiateMultipartUploadResult>",
106+ "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
107+ )
106108 ]
107109
108110testParseListObjectsResult :: Assertion
109111testParseListObjectsResult = do
110− let
111− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
112− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
113− \<Name>bucket</Name>\
114− \<Prefix/>\
115− \<NextContinuationToken>opaque</NextContinuationToken>\
116− \<KeyCount>1000</KeyCount>\
117− \<MaxKeys>1000</MaxKeys>\
118− \<IsTruncated>true</IsTruncated>\
119− \<Contents>\
120− \<Key>my-image.jpg</Key>\
121− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
122− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
123− \<Size>434234</Size>\
124− \<StorageClass>STANDARD</StorageClass>\
125− \</Contents>\
126− \</ListBucketResult>"
127−
128− expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
129− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234
130− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
112+ let xmldata =
113+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
114+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
115+ \<Name>bucket</Name>\
116+ \<Prefix/>\
117+ \<NextContinuationToken>opaque</NextContinuationToken>\
118+ \<KeyCount>1000</KeyCount>\
119+ \<MaxKeys>1000</MaxKeys>\
120+ \<IsTruncated>true</IsTruncated>\
121+ \<Contents>\
122+ \<Key>my-image.jpg</Key>\
123+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
124+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
125+ \<Size>434234</Size>\
126+ \<StorageClass>STANDARD</StorageClass>\
127+ \</Contents>\
128+ \</ListBucketResult>"
129+ expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
130+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
131+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
131132
132− parsedListObjectsResult <- tryValidationErr $ parseListObjectsResponse xmldata
133+ parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
133134 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
134135
135136testParseListObjectsV1Result :: Assertion
136137testParseListObjectsV1Result = do
137− let
138− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
139− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
140− \<Name>bucket</Name>\
141− \<Prefix/>\
142− \<NextMarker>my-image1.jpg</NextMarker>\
143− \<KeyCount>1000</KeyCount>\
144− \<MaxKeys>1000</MaxKeys>\
145− \<IsTruncated>true</IsTruncated>\
146− \<Contents>\
147− \<Key>my-image.jpg</Key>\
148− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
149− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
150− \<Size>434234</Size>\
151− \<StorageClass>STANDARD</StorageClass>\
152− \</Contents>\
153− \</ListBucketResult>"
138+ let xmldata =
139+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
140+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
141+ \<Name>bucket</Name>\
142+ \<Prefix/>\
143+ \<NextMarker>my-image1.jpg</NextMarker>\
144+ \<KeyCount>1000</KeyCount>\
145+ \<MaxKeys>1000</MaxKeys>\
146+ \<IsTruncated>true</IsTruncated>\
147+ \<Contents>\
148+ \<Key>my-image.jpg</Key>\
149+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
150+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
151+ \<Size>434234</Size>\
152+ \<StorageClass>STANDARD</StorageClass>\
153+ \</Contents>\
154+ \</ListBucketResult>"
155+ expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
156+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
157+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
154158
155− expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
156− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234
157− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
158−
159− parsedListObjectsV1Result <- tryValidationErr $ parseListObjectsV1Response xmldata
159+ parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
160160 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
161161
162162testParseListIncompleteUploads :: Assertion
163163testParseListIncompleteUploads = do
164− let
165− xmldata = "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
166− \<Bucket>example-bucket</Bucket>\
167− \<KeyMarker/>\
168− \<UploadIdMarker/>\
169− \<NextKeyMarker>sample.jpg</NextKeyMarker>\
170− \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
171− \<Delimiter>/</Delimiter>\
172− \<Prefix/>\
173− \<MaxUploads>1000</MaxUploads>\
174− \<IsTruncated>false</IsTruncated>\
175− \<Upload>\
176− \<Key>sample.jpg</Key>\
177− \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
178− \<Initiator>\
179− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
180− \<DisplayName>s3-nickname</DisplayName>\
181− \</Initiator>\
182− \<Owner>\
183− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
184− \<DisplayName>s3-nickname</DisplayName>\
185− \</Owner>\
186− \<StorageClass>STANDARD</StorageClass>\
187− \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
188− \</Upload>\
189− \<CommonPrefixes>\
190− \<Prefix>photos/</Prefix>\
191− \</CommonPrefixes>\
192− \<CommonPrefixes>\
193− \<Prefix>videos/</Prefix>\
194− \</CommonPrefixes>\
195− \</ListMultipartUploadsResult>"
196− expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
197− uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
198− initTime = UTCTime (fromGregorian 2010 11 26) 69857
199− prefixes = ["photos/", "videos/"]
164+ let xmldata =
165+ "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
166+ \<Bucket>example-bucket</Bucket>\
167+ \<KeyMarker/>\
168+ \<UploadIdMarker/>\
169+ \<NextKeyMarker>sample.jpg</NextKeyMarker>\
170+ \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
171+ \<Delimiter>/</Delimiter>\
172+ \<Prefix/>\
173+ \<MaxUploads>1000</MaxUploads>\
174+ \<IsTruncated>false</IsTruncated>\
175+ \<Upload>\
176+ \<Key>sample.jpg</Key>\
177+ \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
178+ \<Initiator>\
179+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
180+ \<DisplayName>s3-nickname</DisplayName>\
181+ \</Initiator>\
182+ \<Owner>\
183+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
184+ \<DisplayName>s3-nickname</DisplayName>\
185+ \</Owner>\
186+ \<StorageClass>STANDARD</StorageClass>\
187+ \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
188+ \</Upload>\
189+ \<CommonPrefixes>\
190+ \<Prefix>photos/</Prefix>\
191+ \</CommonPrefixes>\
192+ \<CommonPrefixes>\
193+ \<Prefix>videos/</Prefix>\
194+ \</CommonPrefixes>\
195+ \</ListMultipartUploadsResult>"
196+ expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
197+ uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
198+ initTime = UTCTime (fromGregorian 2010 11 26) 69857
199+ prefixes = ["photos/", "videos/"]
200200
201− parsedListUploadsResult <- tryValidationErr $ parseListUploadsResponse xmldata
201+ parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
202202 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
203203
204−
205204testParseCompleteMultipartUploadResponse :: Assertion
206205testParseCompleteMultipartUploadResponse = do
207− let
208− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
209−\<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
210− \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
211− \<Bucket>Example-Bucket</Bucket>\
212− \<Key>Example-Object</Key>\
213− \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
214−\</CompleteMultipartUploadResult>"
215− expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
206+ let xmldata =
207+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
208+ \<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
209+ \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
210+ \<Bucket>Example-Bucket</Bucket>\
211+ \<Key>Example-Object</Key>\
212+ \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
213+ \</CompleteMultipartUploadResult>"
214+ expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
216215
217− parsedETagE <- runExceptT $ parseCompleteMultipartUploadResponse xmldata
216+ parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
218217 eitherValidationErr parsedETagE (@?= expectedETag)
219218
220219testParseListPartsResponse :: Assertion
221220testParseListPartsResponse = do
222− let
223− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
224−\<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
225− \<Bucket>example-bucket</Bucket>\
226− \<Key>example-object</Key>\
227− \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
228− \<Initiator>\
229− \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
230− \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
231− \</Initiator>\
232− \<Owner>\
233− \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
234− \<DisplayName>someName</DisplayName>\
235− \</Owner>\
236− \<StorageClass>STANDARD</StorageClass>\
237− \<PartNumberMarker>1</PartNumberMarker>\
238− \<NextPartNumberMarker>3</NextPartNumberMarker>\
239− \<MaxParts>2</MaxParts>\
240− \<IsTruncated>true</IsTruncated>\
241− \<Part>\
242− \<PartNumber>2</PartNumber>\
243− \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
244− \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
245− \<Size>10485760</Size>\
246− \</Part>\
247− \<Part>\
248− \<PartNumber>3</PartNumber>\
249− \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
250− \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
251− \<Size>10485760</Size>\
252− \</Part>\
253−\</ListPartsResult>"
254−
255− expectedListResult = ListPartsResult True (Just 3) [part1, part2]
256− part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
257− modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
258− part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
259− modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
221+ let xmldata =
222+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
223+ \<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
224+ \<Bucket>example-bucket</Bucket>\
225+ \<Key>example-object</Key>\
226+ \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
227+ \<Initiator>\
228+ \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
229+ \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
230+ \</Initiator>\
231+ \<Owner>\
232+ \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
233+ \<DisplayName>someName</DisplayName>\
234+ \</Owner>\
235+ \<StorageClass>STANDARD</StorageClass>\
236+ \<PartNumberMarker>1</PartNumberMarker>\
237+ \<NextPartNumberMarker>3</NextPartNumberMarker>\
238+ \<MaxParts>2</MaxParts>\
239+ \<IsTruncated>true</IsTruncated>\
240+ \<Part>\
241+ \<PartNumber>2</PartNumber>\
242+ \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
243+ \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
244+ \<Size>10485760</Size>\
245+ \</Part>\
246+ \<Part>\
247+ \<PartNumber>3</PartNumber>\
248+ \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
249+ \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
250+ \<Size>10485760</Size>\
251+ \</Part>\
252+ \</ListPartsResult>"
253+ expectedListResult = ListPartsResult True (Just 3) [part1, part2]
254+ part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
255+ modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
256+ part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
257+ modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
260258
261− parsedListPartsResult <- runExceptT $ parseListPartsResponse xmldata
259+ parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
262260 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
263261
264262testParseCopyObjectResponse :: Assertion
265263testParseCopyObjectResponse = do
266− let
267− cases = [ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
268−\<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
269− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
270− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
271−\</CopyObjectResult>",
272− ("\"9b2cf535f27731c974343645a3985328\"",
273− UTCTime (fromGregorian 2009 10 28) 81120))
274− , ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
275−\<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
276− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
277− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
278−\</CopyPartResult>",
279− ("\"9b2cf535f27731c974343645a3985328\"",
280− UTCTime (fromGregorian 2009 10 28) 81120))]
264+ let cases =
265+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
266+ \<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
267+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
268+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
269+ \</CopyObjectResult>",
270+ ( "\"9b2cf535f27731c974343645a3985328\"",
271+ UTCTime (fromGregorian 2009 10 28) 81120
272+ )
273+ ),
274+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
275+ \<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
276+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
277+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
278+ \</CopyPartResult>",
279+ ( "\"9b2cf535f27731c974343645a3985328\"",
280+ UTCTime (fromGregorian 2009 10 28) 81120
281+ )
282+ )
283+ ]
281284
282285 forM_ cases $ \(xmldata, (etag, modTime)) -> do
283− parseResult <- runExceptT $ parseCopyObjectResponse xmldata
286+ parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
284287 eitherValidationErr parseResult (@?= (etag, modTime))
285288
286289testParseNotification :: Assertion
287290testParseNotification = do
288− let
289− cases = [ ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
290−\ <TopicConfiguration>\
291−\ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
292−\ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
293−\ <Event>s3:ReducedRedundancyLostObject</Event>\
294−\ <Event>s3:ObjectCreated:*</Event>\
295−\ </TopicConfiguration>\
296−\</NotificationConfiguration>",
297− Notification []
298− [ NotificationConfig
291+ let cases =
292+ [ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
293+ \ <TopicConfiguration>\
294+ \ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
295+ \ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
296+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
297+ \ <Event>s3:ObjectCreated:*</Event>\
298+ \ </TopicConfiguration>\
299+ \</NotificationConfiguration>",
300+ Notification
301+ []
302+ [ NotificationConfig
299303 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
300304 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
301− [ReducedRedundancyLostObject, ObjectCreated] def
302− ]
303− [])
304− , ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
305−\ <CloudFunctionConfiguration>\
306−\ <Id>ObjectCreatedEvents</Id>\
307−\ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
308−\ <Event>s3:ObjectCreated:*</Event>\
309−\ </CloudFunctionConfiguration>\
310−\ <QueueConfiguration>\
311−\ <Id>1</Id>\
312−\ <Filter>\
313−\ <S3Key>\
314−\ <FilterRule>\
315−\ <Name>prefix</Name>\
316−\ <Value>images/</Value>\
317−\ </FilterRule>\
318−\ <FilterRule>\
319−\ <Name>suffix</Name>\
320−\ <Value>.jpg</Value>\
321−\ </FilterRule>\
322−\ </S3Key>\
323−\ </Filter>\
324−\ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
325−\ <Event>s3:ObjectCreated:Put</Event>\
326−\ </QueueConfiguration>\
327−\ <TopicConfiguration>\
328−\ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
329−\ <Event>s3:ReducedRedundancyLostObject</Event>\
330−\ </TopicConfiguration>\
331−\ <QueueConfiguration>\
332−\ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
333−\ <Event>s3:ObjectCreated:*</Event>\
334−\ </QueueConfiguration>)\
335−\</NotificationConfiguration>",
336− Notification [ NotificationConfig
337− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
338− [ObjectCreatedPut]
339− (Filter $ FilterKey $ FilterRules
340− [FilterRule "prefix" "images/",
341− FilterRule "suffix" ".jpg"])
342− , NotificationConfig
343− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
344− [ObjectCreated] def
345− ]
346− [ NotificationConfig
347− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
348− [ReducedRedundancyLostObject] def
349− ]
350− [ NotificationConfig
351− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
352− [ObjectCreated] def
353− ])
354− ]
305+ [ReducedRedundancyLostObject, ObjectCreated]
306+ defaultFilter
307+ ]
308+ []
309+ ),
310+ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
311+ \ <CloudFunctionConfiguration>\
312+ \ <Id>ObjectCreatedEvents</Id>\
313+ \ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
314+ \ <Event>s3:ObjectCreated:*</Event>\
315+ \ </CloudFunctionConfiguration>\
316+ \ <QueueConfiguration>\
317+ \ <Id>1</Id>\
318+ \ <Filter>\
319+ \ <S3Key>\
320+ \ <FilterRule>\
321+ \ <Name>prefix</Name>\
322+ \ <Value>images/</Value>\
323+ \ </FilterRule>\
324+ \ <FilterRule>\
325+ \ <Name>suffix</Name>\
326+ \ <Value>.jpg</Value>\
327+ \ </FilterRule>\
328+ \ </S3Key>\
329+ \ </Filter>\
330+ \ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
331+ \ <Event>s3:ObjectCreated:Put</Event>\
332+ \ </QueueConfiguration>\
333+ \ <TopicConfiguration>\
334+ \ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
335+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
336+ \ </TopicConfiguration>\
337+ \ <QueueConfiguration>\
338+ \ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
339+ \ <Event>s3:ObjectCreated:*</Event>\
340+ \ </QueueConfiguration>)\
341+ \</NotificationConfiguration>",
342+ Notification
343+ [ NotificationConfig
344+ "1"
345+ "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
346+ [ObjectCreatedPut]
347+ ( Filter $
348+ FilterKey $
349+ FilterRules
350+ [ FilterRule "prefix" "images/",
351+ FilterRule "suffix" ".jpg"
352+ ]
353+ ),
354+ NotificationConfig
355+ ""
356+ "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
357+ [ObjectCreated]
358+ defaultFilter
359+ ]
360+ [ NotificationConfig
361+ ""
362+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
363+ [ReducedRedundancyLostObject]
364+ defaultFilter
365+ ]
366+ [ NotificationConfig
367+ "ObjectCreatedEvents"
368+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
369+ [ObjectCreated]
370+ defaultFilter
371+ ]
372+ )
373+ ]
355374
356375 forM_ cases $ \(xmldata, val) -> do
357− result <- runExceptT $ parseNotification xmldata
376+ result <- runExceptT $ runTestNS $ parseNotification xmldata
358377 eitherValidationErr result (@?= val)
378+
379+-- | Tests parsing of both progress and stats
380+testParseSelectProgress :: Assertion
381+testParseSelectProgress = do
382+ let cases =
383+ [ ( [r|<?xml version="1.0" encoding="UTF-8"?>
384+<Progress>
385+ <BytesScanned>512</BytesScanned>
386+ <BytesProcessed>1024</BytesProcessed>
387+ <BytesReturned>1024</BytesReturned>
388+</Progress>|],
389+ Progress 512 1024 1024
390+ ),
391+ ( [r|<?xml version="1.0" encoding="UTF-8"?>
392+<Stats>
393+ <BytesScanned>512</BytesScanned>
394+ <BytesProcessed>1024</BytesProcessed>
395+ <BytesReturned>1024</BytesReturned>
396+</Stats>|],
397+ Progress 512 1024 1024
398+ )
399+ ]
400+
401+ forM_ cases $ \(xmldata, progress) -> do
402+ result <- runExceptT $ parseSelectProgress xmldata
403+ eitherValidationErr result (@?= progress)
test/Spec.hs+81−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.
@@ -14,18 +14,17 @@
1414-- limitations under the License.
1515--
1616
17−import Test.Tasty
18−import Test.Tasty.QuickCheck as QC
19−
17+import qualified Data.ByteString as B
2018import qualified Data.List as L
21−
22−import Lib.Prelude
23−
24−import Network.Minio.API.Test
25−import Network.Minio.PutObject
26−import Network.Minio.Utils.Test
27−import Network.Minio.XmlGenerator.Test
28−import Network.Minio.XmlParser.Test
19+import Lib.Prelude
20+import Network.Minio.API.Test
21+import Network.Minio.CopyObject
22+import Network.Minio.Data
23+import Network.Minio.Utils.Test
24+import Network.Minio.XmlGenerator.Test
25+import Network.Minio.XmlParser.Test
26+import Test.Tasty
27+import Test.Tasty.QuickCheck as QC
2928
3029main :: IO ()
3130main = defaultMain tests
@@ -48,71 +47,87 @@ properties = testGroup "Properties" [qcProps] -- [scProps]
4847-- ]
4948
5049qcProps :: TestTree
51−qcProps = testGroup "(checked by QuickCheck)"
52− [ QC.testProperty "selectPartSizes:" $
53− \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
54−
50+qcProps =
51+ testGroup
52+ "(checked by QuickCheck)"
53+ [ QC.testProperty "selectPartSizes:" $
54+ \n ->
55+ let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
5556 -- check that pns increments from 1.
56− isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1..]
57−
57+ isPNumsAscendingFrom1 = all (uncurry (==)) $ zip pns [1 ..]
5858 consPairs [] = []
5959 consPairs [_] = []
60− consPairs (a:(b:c)) = (a, b):(consPairs (b:c))
61−
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
64−
62+ isOffsetsAsc = all (uncurry (<)) $ consPairs offs
6563 -- check sizes sums to n.
6664 isSumSizeOk = sum sizes == n
67−
6865 -- check sizes are constant except last
6966 isSizesConstantExceptLast =
70− all (\(a, b) -> a == b) (consPairs $ L.init sizes)
71−
67+ all (uncurry (==)) (consPairs $ L.init sizes)
7268 -- check each part except last is at least minPartSize;
7369 -- last part may be 0 only if it is the only part.
7470 nparts = length sizes
7571 isMinPartSizeOk =
76− if | nparts > 1 -> -- last part can be smaller but > 0
77− all (>= minPartSize) (take (nparts - 1) sizes) &&
78− all (\s -> s > 0) (drop (nparts - 1) sizes)
79− | nparts == 1 -> -- size may be 0 here.
80− maybe True (\x -> x >= 0 && x <= minPartSize) $
81− headMay sizes
82− | otherwise -> False
83−
84− in n < 0 ||
85− (isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk &&
86− isSizesConstantExceptLast && isMinPartSizeOk)
87−
88− , QC.testProperty "selectCopyRanges:" $
89− \(start, end) ->
90− let (_, pairs) = L.unzip (selectCopyRanges (start, end))
91−
92− -- is last part's snd offset end?
93− isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
94− -- is first part's fst offset start
95− isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
96−
97− -- each pair is >=64MiB except last, and all those parts
98− -- have same size.
99− initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs
100− isPartSizesOk = all (>= minPartSize) initSizes &&
101− maybe True (\k -> all (== k) initSizes)
102− (headMay initSizes)
103−
104− -- returned offsets are contiguous.
105− fsts = drop 1 $ map fst pairs
106− snds = take (length pairs - 1) $ map snd pairs
107− isContParts = length fsts == length snds &&
108− and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
109−
110− in start < 0 || start > end ||
111− (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts)
112− ]
72+ if
73+ | nparts > 1 -> -- last part can be smaller but > 0
74+ all (>= minPartSize) (take (nparts - 1) sizes)
75+ && all (> 0) (drop (nparts - 1) sizes)
76+ | nparts == 1 -> -- size may be 0 here.
77+ maybe True (\x -> x >= 0 && x <= minPartSize) $
78+ listToMaybe sizes
79+ | otherwise -> False
80+ in n < 0
81+ || ( isPNumsAscendingFrom1
82+ && isOffsetsAsc
83+ && isSumSizeOk
84+ && isSizesConstantExceptLast
85+ && isMinPartSizeOk
86+ ),
87+ QC.testProperty "selectCopyRanges:" $
88+ \(start, end) ->
89+ let (_, pairs) = L.unzip (selectCopyRanges (start, end))
90+ -- is last part's snd offset end?
91+ isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
92+ -- is first part's fst offset start
93+ isFirstPartOk = maybe False ((start ==) . fst) $ listToMaybe pairs
94+ -- each pair is >=64MiB except last, and all those parts
95+ -- have same size.
96+ initSizes = maybe [] (map (\(a, b) -> b - a + 1) . init) (nonEmpty pairs)
97+ isPartSizesOk =
98+ all (>= minPartSize) initSizes
99+ && maybe
100+ True
101+ (\k -> all (== k) initSizes)
102+ (listToMaybe initSizes)
103+ -- returned offsets are contiguous.
104+ fsts = drop 1 $ map fst pairs
105+ snds = take (length pairs - 1) $ map snd pairs
106+ isContParts =
107+ length fsts == length snds
108+ && all (\(a, b) -> a == b + 1) (zip fsts snds)
109+ in start < 0
110+ || start > end
111+ || (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts),
112+ QC.testProperty "mkSSECKey:" $
113+ \w8s ->
114+ let bs = B.pack w8s
115+ r = mkSSECKey bs
116+ in case r of
117+ Just _ -> B.length bs == 32
118+ Nothing -> B.length bs /= 32
119+ ]
113120
114121unitTests :: TestTree
115−unitTests = testGroup "Unit tests" [xmlGeneratorTests, xmlParserTests,
116− bucketNameValidityTests,
117− objectNameValidityTests,
118− limitedMapConcurrentlyTests]
122+unitTests =
123+ testGroup
124+ "Unit tests"
125+ [ xmlGeneratorTests,
126+ xmlParserTests,
127+ bucketNameValidityTests,
128+ objectNameValidityTests,
129+ parseServerInfoJSONTest,
130+ parseHealStatusTest,
131+ parseHealStartRespTest,
132+ limitedMapConcurrentlyTests
133+ ]
test/cert/private.keynew file+28
@@ -0,0 +1,28 @@
1+-----BEGIN PRIVATE KEY-----
2+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC3G9IiC+adjf0p
3+i/2KYc+4dizeuzUFN7wraSdhiOMdQgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwI
4+rPJ61dRGQSuN12l+mzngFJQjE0sysZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPY
5+fhcUcq03onMGq44yOfE6mIhoe0Y9wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq
6+5KjHL8WW2vGg7G9edpYdxINA/A2fdLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L
7+9yRqItqddriRxJFwOXb5OPW8xx2WGaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A8
8+4TD/RXMbAgMBAAECggEBAJ7r1oUWLyGvinn0tijUm6RNbMQjVvEgXoCO008jr3pF
9+PqxVpgEMrOa/4tmwFBus0jcCNF4t3r2zhddBw3I5A/O1vEdvHnBz6NdDBQ8sP6fP
10+1fF50iEe1Y2MBibQkXFxxVMG2QRB1Gt5nuvXA9ELdqtCovK3EsMk5ukkWb/UvjH5
11+8hcmQsaSqvzFEF4wJSY2mkeGSGIJTphPhhuA22xbhaBMInQyhZu8EHsn0h6s/Wgy
12+C4Cp2+4qZTKaaf6x3/ZjJ8CuKiSX+ZsJKjOEv8sqx7j/Y7QFOmJPewInKDhwazr/
13+xIK+N0KXPbUzeSEz6ZvExNDTxtR5ZlQP2UrRDg28yQECgYEA4Is1O2BvKVzNFOkj
14+bTVz25a/bb0Xrcfgi0Y9rdfLzlNdItFjAkxLTVRSW2Hv9ICl0RDDAG+wTlktXRdh
15+rfvDjwG2CvLQo1VEdMWTTkKVg03SwMEy2hFiWV69lENFGSaY8Y6unZDbia5HQinA
16+EgSS4sCojS+a2jtzG5FVVHJDKlkCgYEA0MKhMhD4SUhr2y1idPBrmLxuW5mVozuW
17+8bYaBeSzmfS0BRsN4fP9JGODPBPDdNbfGfGC9ezWLgD/lmCgjIEyBOq8EmqWSsiS
18+Kihds1+Z7hXtbzGsFGAFJJTIh7blBCsK5QFuyuih2UG0fL9z6K/dy+UUJkzrYqph
19+vSfKixyM8pMCgYEAmUPLsNyw4325aeV8TeWnUCJERaZFDFQa21W1cfyS2yEhuEtN
20+llr3JzBACqn9vFk3VU1onNqfb8sE4L696KCpKeqUFEMK0AG6eS4Gzus53Gb5TKJS
21+kHA/PhshsZp9Bp7G1FJ8s4YVo5N2hh2zQVkn3Wh9Y+kzfHQJrK51nO9lEvkCgYBi
22+BuKWle1gzAcJdnhDHRoJMIJJtQbVDYhFnBMALXJAmu1lcFzGe0GlMq1PKqCfXr6I
23+eiXawQmZtJJP1LPPBmOsd2U06KQGHcS00xucvQmVCOrjSdnZ/3SqxsqbH8DOgj+t
24+ZUzXLwHA+N99rJEK9Hob4kfh7ECjpgobPnIXfKKazQKBgQChAuiXHtf/Qq18hY3u
25+x48zFWjGgfd6GpOBZYkXOwGdCJgnYjZbE26LZEnYbwPh8ZUA2vp7mgHRJkD5e3Fj
26+ERuJLCw86WqyYZmLEuBciYGjCZqR5nbavfwsziWD00jeNruds2ZwKxRfFm4V7o2S
27+WLd/RUatd2Uu9f3B2J78OUdnxg==
28+-----END PRIVATE KEY-----
test/cert/public.crtnew file+19
@@ -0,0 +1,19 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDCzCCAfOgAwIBAgIUaIUOMI78LCu+r1zl0mmFHK8n5/AwDQYJKoZIhvcNAQEL
3+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE5MTAyNDE5NTMxOVoYDzIxMTkw
4+OTMwMTk1MzE5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
5+AQUAA4IBDwAwggEKAoIBAQC3G9IiC+adjf0pi/2KYc+4dizeuzUFN7wraSdhiOMd
6+QgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwIrPJ61dRGQSuN12l+mzngFJQjE0sy
7+sZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPYfhcUcq03onMGq44yOfE6mIhoe0Y9
8+wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq5KjHL8WW2vGg7G9edpYdxINA/A2f
9+dLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L9yRqItqddriRxJFwOXb5OPW8xx2W
10+GaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A84TD/RXMbAgMBAAGjUzBRMB0GA1Ud
11+DgQWBBSEWXQ2JRD+OK7/KTmlD+OW16pGmzAfBgNVHSMEGDAWgBSEWXQ2JRD+OK7/
12+KTmlD+OW16pGmzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCF
13+0zYRaabB3X0jzGI9/Lr3Phrb90GvoL1DFLRuiOuTlDkz0vrm/HrZskwHCgMNrkCj
14+OTD9Vpas4D1QZBbQbRzfnf3OOoG4bgmcCwLFZl3dy27yIDAhrmbUP++g9l1Jmy4v
15+vBR/M4lt2scQ8LcZYEPqhEaE5EzFQEjtaxDcKdWDNKY9W1NUzSIABhF9eHiAUNdH
16+AFNJlYeBlCHxcWIeqgon184Dqp/CsvKtz3z3Ni+rlwPM/zuJCFHh1VF+z++0LJjG
17+roBCV0Tro4XyiEz9yp7Cb5kQYMaj1KL9TqBG0tZx0pmv7y+lXc4TT6DEllXz6USy
18+rbIba9/uUet3BqeIMTqj
19+-----END CERTIFICATE-----