git.iko.soy/typeable/minio-hs
on master help sign in
.github/workflows/ci.ymlnew file+266
since splitsnapshots
unifiedsplit
@@ -0,0 +1,266 @@
1+name: CI
2+
3+# Trigger the workflow on push or pull request, but only for the master branch
4+on:
5+ pull_request:
6+ branches: [master]
7+ push:
8+ branches: [master]
9+
10+# This ensures that previous jobs for the PR are canceled when the PR is
11+# updated.
12+concurrency:
13+ group: ${{ github.workflow }}-${{ github.head_ref }}
14+ cancel-in-progress: true
15+
16+# Env vars for tests
17+env:
18+ MINIO_ACCESS_KEY: minio
19+ MINIO_SECRET_KEY: minio123
20+ MINIO_LOCAL: 1
21+
22+jobs:
23+ ormolu:
24+ runs-on: ubuntu-latest
25+ steps:
26+ - uses: actions/checkout@v4
27+ - uses: haskell-actions/run-ormolu@v15
28+ with:
29+ version: "0.5.0.1"
30+
31+ hlint:
32+ runs-on: ubuntu-latest
33+ steps:
34+ - uses: actions/checkout@v4
35+
36+ - name: "Set up HLint"
37+ uses: haskell-actions/hlint-setup@v2
38+ with:
39+ version: "3.5"
40+
41+ - name: "Run HLint"
42+ uses: haskell-actions/hlint-run@v2
43+ with:
44+ path: '["src/", "test/", "examples"]'
45+ fail-on: warning
46+
47+ cabal:
48+ name: ${{ matrix.os }} / ghc-${{ matrix.ghc }} / cabal-${{ matrix.cabal }}
49+ runs-on: ${{ matrix.os }}
50+ needs: ormolu
51+ strategy:
52+ matrix:
53+ os: [ubuntu-latest, windows-latest, macos-latest]
54+ cabal: ["3.8", "latest"]
55+ ghc:
56+ - "9.8"
57+ - "9.6"
58+ - "9.4"
59+ - "9.2"
60+ - "9.0"
61+ - "8.10"
62+ exclude:
63+ # macos llvm issue for versions less than 9.2
64+ - os: macos-latest
65+ ghc: "8.10"
66+ - os: macos-latest
67+ ghc: "9.0"
68+ # Cabal 3.8 supports GHC < 9.6
69+ - cabal: "3.8"
70+ ghc: "9.6"
71+ - cabal: "3.8"
72+ ghc: "9.8"
73+
74+ steps:
75+ - uses: actions/checkout@v4
76+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
77+
78+ - uses: haskell-actions/setup@v2
79+ id: setup
80+ name: Setup Haskell
81+ with:
82+ ghc-version: ${{ matrix.ghc }}
83+ cabal-version: ${{ matrix.cabal }}
84+ cabal-update: true
85+
86+ - name: Configure
87+ run: |
88+ cabal configure --enable-tests --enable-benchmarks --test-show-details=direct -fexamples -fdev -flive-test
89+ cabal build all --dry-run
90+ # The last step generates dist-newstyle/cache/plan.json for the cache key.
91+
92+ - name: Restore cached dependencies
93+ uses: actions/cache/restore@v4
94+ id: cache
95+ env:
96+ key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
97+ with:
98+ path: ${{ steps.setup.outputs.cabal-store }}
99+ key: ${{ env.key }}-plan-${{ hashFiles('**/plan.json') }}
100+ restore-keys: ${{ env.key }}-
101+
102+ - name: Install dependencies
103+ # If we had an exact cache hit, the dependencies will be up to date.
104+ if: steps.cache.outputs.cache-hit != 'true'
105+ run: cabal build all --only-dependencies
106+
107+ # Cache dependencies already here, so that we do not have to rebuild them should the subsequent steps fail.
108+ - name: Save cached dependencies
109+ uses: actions/cache/save@v4
110+ # If we had an exact cache hit, trying to save the cache would error because of key clash.
111+ if: steps.cache.outputs.cache-hit != 'true'
112+ with:
113+ path: ${{ steps.setup.outputs.cabal-store }}
114+ key: ${{ steps.cache.outputs.cache-primary-key }}
115+
116+ - name: Build
117+ run: |
118+ cabal build all
119+
120+ - name: Setup TLS certs for MinIO for testing (Linux)
121+ if: matrix.os == 'ubuntu-latest'
122+ run: |
123+ mkdir -p /tmp/minio /tmp/minio-config/certs
124+ cp test/cert/* /tmp/minio-config/certs/
125+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
126+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
127+ sudo update-ca-certificates
128+
129+ ## Currently disable TLS setup for MacOS due to issues in trusting it on MacOS.
130+ - name: Setup TLS certs for MinIO for testing (MacOS)
131+ if: matrix.os == 'macos-latest'
132+ run: |
133+ mkdir -p /tmp/minio /tmp/minio-config/certs
134+ cp test/cert/* /tmp/minio-config/certs/
135+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
136+ # sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
137+
138+ - name: Setup MinIO for testing (Windows)
139+ if: matrix.os == 'windows-latest'
140+ run: |
141+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
142+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
143+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
144+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
145+
146+ - name: Test (Linux)
147+ if: matrix.os == 'ubuntu-latest'
148+ env:
149+ MINIO_SECURE: 1
150+ run: |
151+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
152+ ghc --version
153+ cabal --version
154+ cabal test all
155+
156+ - name: Test (MacOS)
157+ if: matrix.os == 'macos-latest'
158+ # # Leave MINIO_SECURE unset to disable TLS in tests.
159+ # env:
160+ # MINIO_SECURE: 1
161+ run: |
162+ /tmp/minio/minio server --quiet data1 data2 data3 data4 2>&1 > minio.log &
163+ ghc --version
164+ cabal --version
165+ cabal test all
166+
167+ - name: Test (Windows)
168+ if: matrix.os == 'windows-latest'
169+ env:
170+ MINIO_SECURE: 1
171+ run: |
172+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
173+ ghc --version
174+ cabal --version
175+ cabal test all
176+
177+ stack:
178+ name: stack / ghc ${{ matrix.ghc }}
179+ runs-on: ${{ matrix.os }}
180+ env:
181+ MINIO_SECURE: 1
182+ strategy:
183+ matrix:
184+ ghc:
185+ - "8.10.7"
186+ - "9.0.2"
187+ - "9.2.8"
188+ - "9.4.8"
189+ - "9.6.5"
190+ - "9.8.2"
191+ os: [ubuntu-latest]
192+
193+ steps:
194+ - uses: actions/checkout@v4
195+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
196+
197+ - uses: haskell-actions/setup@v2
198+ with:
199+ ghc-version: ${{ matrix.ghc }}
200+ enable-stack: true
201+ stack-version: "latest"
202+
203+ - uses: actions/cache@v4
204+ name: Cache ~/.stack
205+ with:
206+ path: ~/.stack
207+ key: ${{ runner.os }}-stack-global-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}
208+ restore-keys: |
209+ ${{ runner.os }}-stack-global-
210+ - uses: actions/cache@v4
211+ name: Cache .stack-work
212+ with:
213+ path: .stack-work
214+ key: ${{ runner.os }}-stack-work-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}-${{ hashFiles('**/*.hs') }}
215+ restore-keys: |
216+ ${{ runner.os }}-stack-work-
217+
218+ - name: Install dependencies
219+ run: |
220+ stack --version
221+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
222+
223+ - name: Build
224+ run: |
225+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --flag minio-hs:examples --flag minio-hs:live-test --flag minio-hs:dev
226+
227+ - name: Setup MinIO for testing (Linux)
228+ if: matrix.os == 'ubuntu-latest'
229+ run: |
230+ mkdir -p /tmp/minio /tmp/minio-config/certs
231+ cp test/cert/* /tmp/minio-config/certs/
232+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
233+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
234+ sudo update-ca-certificates
235+
236+ - name: Setup MinIO for testing (MacOS)
237+ if: matrix.os == 'macos-latest'
238+ run: |
239+ mkdir -p /tmp/minio /tmp/minio-config/certs
240+ cp test/cert/* /tmp/minio-config/certs/
241+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
242+ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
243+
244+ - name: Setup MinIO for testing (Windows)
245+ if: matrix.os == 'windows-latest'
246+ run: |
247+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
248+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
249+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
250+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
251+
252+ - name: Test (Non-Windows)
253+ if: matrix.os != 'windows-latest'
254+ run: |
255+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
256+ ghc --version
257+ stack --version
258+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
259+
260+ - name: Test (Windows)
261+ if: matrix.os == 'windows-latest'
262+ run: |
263+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
264+ ghc --version
265+ cabal --version
266+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
.stylish-haskell.yamldeleted−233
@@ -1,233 +0,0 @@
1−# stylish-haskell configuration file
2−# ==================================
3−
4−# The stylish-haskell tool is mainly configured by specifying steps. These steps
5−# are a list, so they have an order, and one specific step may appear more than
6−# once (if needed). Each file is processed by these steps in the given order.
7−steps:
8− # Convert some ASCII sequences to their Unicode equivalents. This is disabled
9− # by default.
10− # - unicode_syntax:
11− # # In order to make this work, we also need to insert the UnicodeSyntax
12− # # language pragma. If this flag is set to true, we insert it when it's
13− # # not already present. You may want to disable it if you configure
14− # # language extensions using some other method than pragmas. Default:
15− # # true.
16− # add_language_pragma: true
17−
18− # Align the right hand side of some elements. This is quite conservative
19− # and only applies to statements where each element occupies a single
20− # line.
21− - simple_align:
22− cases: true
23− top_level_patterns: true
24− records: true
25−
26− # Import cleanup
27− - imports:
28− # There are different ways we can align names and lists.
29− #
30− # - global: Align the import names and import list throughout the entire
31− # file.
32− #
33− # - file: Like global, but don't add padding when there are no qualified
34− # imports in the file.
35− #
36− # - group: Only align the imports per group (a group is formed by adjacent
37− # import lines).
38− #
39− # - none: Do not perform any alignment.
40− #
41− # Default: global.
42− align: global
43−
44− # The following options affect only import list alignment.
45− #
46− # List align has following options:
47− #
48− # - after_alias: Import list is aligned with end of import including
49− # 'as' and 'hiding' keywords.
50− #
51− # > import qualified Data.List as List (concat, foldl, foldr, head,
52− # > init, last, length)
53− #
54− # - with_alias: Import list is aligned with start of alias or hiding.
55− #
56− # > import qualified Data.List as List (concat, foldl, foldr, head,
57− # > init, last, length)
58− #
59− # - new_line: Import list starts always on new line.
60− #
61− # > import qualified Data.List as List
62− # > (concat, foldl, foldr, head, init, last, length)
63− #
64− # Default: after_alias
65− list_align: after_alias
66−
67− # Right-pad the module names to align imports in a group:
68− #
69− # - true: a little more readable
70− #
71− # > import qualified Data.List as List (concat, foldl, foldr,
72− # > init, last, length)
73− # > import qualified Data.List.Extra as List (concat, foldl, foldr,
74− # > init, last, length)
75− #
76− # - false: diff-safe
77− #
78− # > import qualified Data.List as List (concat, foldl, foldr, init,
79− # > last, length)
80− # > import qualified Data.List.Extra as List (concat, foldl, foldr,
81− # > init, last, length)
82− #
83− # Default: true
84− pad_module_names: true
85−
86− # Long list align style takes effect when import is too long. This is
87− # determined by 'columns' setting.
88− #
89− # - inline: This option will put as much specs on same line as possible.
90− #
91− # - new_line: Import list will start on new line.
92− #
93− # - new_line_multiline: Import list will start on new line when it's
94− # short enough to fit to single line. Otherwise it'll be multiline.
95− #
96− # - multiline: One line per import list entry.
97− # Type with constructor list acts like single import.
98− #
99− # > import qualified Data.Map as M
100− # > ( empty
101− # > , singleton
102− # > , ...
103− # > , delete
104− # > )
105− #
106− # Default: inline
107− long_list_align: inline
108−
109− # Align empty list (importing instances)
110− #
111− # Empty list align has following options
112− #
113− # - inherit: inherit list_align setting
114− #
115− # - right_after: () is right after the module name:
116− #
117− # > import Vector.Instances ()
118− #
119− # Default: inherit
120− empty_list_align: inherit
121−
122− # List padding determines indentation of import list on lines after import.
123− # This option affects 'long_list_align'.
124− #
125− # - <integer>: constant value
126− #
127− # - module_name: align under start of module name.
128− # Useful for 'file' and 'group' align settings.
129− list_padding: 4
130−
131− # Separate lists option affects formatting of import list for type
132− # or class. The only difference is single space between type and list
133− # of constructors, selectors and class functions.
134− #
135− # - true: There is single space between Foldable type and list of it's
136− # functions.
137− #
138− # > import Data.Foldable (Foldable (fold, foldl, foldMap))
139− #
140− # - false: There is no space between Foldable type and list of it's
141− # functions.
142− #
143− # > import Data.Foldable (Foldable(fold, foldl, foldMap))
144− #
145− # Default: true
146− separate_lists: true
147−
148− # Space surround option affects formatting of import lists on a single
149− # line. The only difference is single space after the initial
150− # parenthesis and a single space before the terminal parenthesis.
151− #
152− # - true: There is single space associated with the enclosing
153− # parenthesis.
154− #
155− # > import Data.Foo ( foo )
156− #
157− # - false: There is no space associated with the enclosing parenthesis
158− #
159− # > import Data.Foo (foo)
160− #
161− # Default: false
162− space_surround: false
163−
164− # Language pragmas
165− - language_pragmas:
166− # We can generate different styles of language pragma lists.
167− #
168− # - vertical: Vertical-spaced language pragmas, one per line.
169− #
170− # - compact: A more compact style.
171− #
172− # - compact_line: Similar to compact, but wrap each line with
173− # `{-#LANGUAGE #-}'.
174− #
175− # Default: vertical.
176− style: vertical
177−
178− # Align affects alignment of closing pragma brackets.
179− #
180− # - true: Brackets are aligned in same column.
181− #
182− # - false: Brackets are not aligned together. There is only one space
183− # between actual import and closing bracket.
184− #
185− # Default: true
186− align: true
187−
188− # stylish-haskell can detect redundancy of some language pragmas. If this
189− # is set to true, it will remove those redundant pragmas. Default: true.
190− remove_redundant: true
191−
192− # Replace tabs by spaces. This is disabled by default.
193− # - tabs:
194− # # Number of spaces to use for each tab. Default: 8, as specified by the
195− # # Haskell report.
196− # spaces: 8
197−
198− # Remove trailing whitespace
199− - trailing_whitespace: {}
200−
201−# A common setting is the number of columns (parts of) code will be wrapped
202−# to. Different steps take this into account. Default: 80.
203−columns: 80
204−
205−# By default, line endings are converted according to the OS. You can override
206−# preferred format here.
207−#
208−# - native: Native newline format. CRLF on Windows, LF on other OSes.
209−#
210−# - lf: Convert to LF ("\n").
211−#
212−# - crlf: Convert to CRLF ("\r\n").
213−#
214−# Default: native.
215−newline: native
216−
217−# Sometimes, language extensions are specified in a cabal file or from the
218−# command line instead of using language pragmas in the file. stylish-haskell
219−# needs to be aware of these, so it can parse the file correctly.
220−#
221−# No language extensions are enabled by default.
222−language_extensions:
223− - BangPatterns
224− - FlexibleContexts
225− - FlexibleInstances
226− - MultiParamTypeClasses
227− - MultiWayIf
228− - NoImplicitPrelude
229− - OverloadedStrings
230− - RankNTypes
231− - ScopedTypeVariables
232− - TupleSections
233− - TypeFamilies
.travis.ymldeleted−61
@@ -1,61 +0,0 @@
1−sudo: true
2−language: haskell
3−
4−git:
5− depth: 5
6−
7−cabal: "3.0"
8−
9−cache:
10− directories:
11− - "$HOME/.cabal/store"
12− - "$HOME/.stack"
13− - "$TRAVIS_BUILD_DIR/.stack-work"
14−
15−matrix:
16− include:
17−
18− # Cabal
19− - ghc: 8.4.4
20− - ghc: 8.6.5
21− - ghc: 8.8.2
22−
23− # Stack
24− - ghc: 8.6.5
25− env: STACK_YAML="$TRAVIS_BUILD_DIR/stack.yaml"
26−
27−before_install:
28− - sudo apt-get install devscripts
29− - mkdir /tmp/minio /tmp/certs
30− - (cd /tmp/minio; wget https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
31− - (cd /tmp/certs; openssl req -newkey rsa:2048 -nodes -keyout private.key -x509 -days 36500 -out public.crt -subj "/C=US/ST=NRW/L=Earth/O=CompanyName/OU=IT/CN=localhost/emailAddress=email@example.com")
32− - sudo cp /tmp/certs/public.crt /usr/local/share/ca-certificates/
33− - sudo update-ca-certificates
34− - MINIO_ACCESS_KEY=minio MINIO_SECRET_KEY=minio123 /tmp/minio/minio server --quiet --certs-dir /tmp/certs data 2>&1 > minio.log &
35−
36−install:
37− - |
38− if [ -z "$STACK_YAML" ]; then
39− ghc --version
40− cabal --version
41− cabal new-update
42− cabal new-build --enable-tests --enable-benchmarks -fexamples
43− else
44− # install stack
45− curl -sSL https://get.haskellstack.org/ | sh
46−
47− # build project with stack
48− stack --version
49− stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --flag minio-hs:examples
50− fi
51−
52−script:
53− - |
54− if [ -z "$STACK_YAML" ]; then
55− MINIO_LOCAL=1 MINIO_SECURE=1 cabal new-test --enable-tests -flive-test
56− else
57− MINIO_LOCAL=1 MINIO_SECURE=1 stack test --system-ghc --flag minio-hs:live-test
58− fi
59−
60−notifications:
61− email: false
CHANGELOG.md+31
@@ -1,6 +1,37 @@
1Changelog
1Changelog
2==========
2==========
3
3
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+
4## Version 1.5.2
35## Version 1.5.2
5
36
6* Fix region `us-west-2` for AWS S3 (#139)
37* Fix region `us-west-2` for AWS S3 (#139)
CNAMEnew file+1
@@ -0,0 +1 @@
1+minio-hs.min.io
README.md+50−29
@@ -1,10 +1,8 @@
1−# MinIO Client SDK for Haskell [![Build Status](https://travis-ci.org/minio/minio-hs.svg?branch=master)](https://travis-ci.org/minio/minio-hs)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
1+# MinIO Haskell Client SDK for Amazon S3 Compatible Cloud Storage [![CI](https://github.com/minio/minio-hs/actions/workflows/ci.yml/badge.svg)](https://github.com/minio/minio-hs/actions/workflows/ci.yml)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
2
2
3−The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and Amazon S3 compatible object storage server.
3+The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
4
4
5−## Minimum Requirements
5+This guide assumes that you have a working [Haskell development environment](https://www.haskell.org/downloads/).
6−
7−- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
8
6
9## Installation
7## Installation
10
8
@@ -12,20 +10,35 @@ The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.
12
10
13Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
11Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
14
12
15−### Try it out directly with `ghci`
13+### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
14+
15+#### For a cabal based environment
16+
17+Download the library source and change to the extracted directory:
18+
19+``` sh
20+$ cabal get minio-hs
21+$ cd minio-hs-1.6.0/ # directory name could be different
22+```
23+
24+Then load the `ghci` REPL environment with the library and browse the available APIs:
25+
26+``` sh
27+$ cabal repl
28+ghci> :browse Network.Minio
29+```
30+
31+#### For a stack based environment
16
32
17From your home folder or any non-haskell project directory, just run:
33From your home folder or any non-haskell project directory, just run:
18
34
19```sh
35```sh
20−
21stack install minio-hs
36stack install minio-hs
22−
23```
37```
24
38
25Then start an interpreter session and browse the available APIs with:
39Then start an interpreter session and browse the available APIs with:
26
40
27```sh
41```sh
28−
29$ stack ghci
42$ stack ghci
30> :browse Network.Minio
43> :browse Network.Minio
31```
44```
@@ -134,44 +147,52 @@ main = do
134
147
135### Development
148### Development
136
149
137−To setup:
150+#### Download the source
138
151
139```sh
152```sh
140−git clone https://github.com/minio/minio-hs.git
153+$ git clone https://github.com/minio/minio-hs.git
141−
142−cd minio-hs/
154+$ cd minio-hs/
155+```
143
156
144−stack install
157+#### Build the package:
145−```
146
158
147−Tests can be run with:
159+With `cabal`:
148
160
149```sh
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+```
150
166
151−stack test
167+With `stack`:
152
168
169+``` sh
170+$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
153```
171```
172+#### Running tests:
154
173
155−A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
174+A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000` with the credentials `access_key=minio` and `secret_key=minio123`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
156
175
157−To run the live server tests, set a build flag as shown below:
176+With `cabal`:
158
177
159```sh
178```sh
179+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
180+$ cabal test
181+```
160
182
161−stack test --flag minio-hs:live-test
183+With `stack`:
162−
163−# OR against a local MinIO server with:
164−
165−MINIO_LOCAL=1 stack test --flag minio-hs:live-test
166
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
167```
188```
168
189
169−The configured CI system always runs both test-suites for every change.
190+This will run all the test suites.
170
191
171−Documentation can be locally built with:
192+#### Building documentation:
172
193
173```sh
194```sh
174−
195+$ cabal haddock
196+$ # OR
175−stack haddock
197+$ stack haddock
176−
177```
198```
Setup.hsdeleted−18
@@ -1,18 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−import Distribution.Simple
18−main = defaultMain
examples/AssumeRole.hsnew file+47
@@ -0,0 +1,47 @@
1+--
2+-- MinIO Haskell SDK, (C) 2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE OverloadedStrings #-}
17+
18+import Control.Monad.IO.Class (liftIO)
19+import Network.Minio
20+import Prelude
21+
22+main :: IO ()
23+main = do
24+ -- Use play credentials for example.
25+ let assumeRole =
26+ STSAssumeRole
27+ ( CredentialValue
28+ "Q3AM3UQ867SPQQA43P2F"
29+ "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
30+ Nothing
31+ )
32+ $ defaultSTSAssumeRoleOptions
33+ { saroLocation = Just "us-east-1",
34+ saroEndpoint = Just "https://play.min.io:9000"
35+ }
36+
37+ -- Retrieve temporary credentials and print them.
38+ cv <- requestSTSCredential assumeRole
39+ print $ "Temporary credentials" ++ show (credentialValueText $ fst cv)
40+ print $ "Expiry" ++ show (snd cv)
41+
42+ -- Configure 'ConnectInfo' to request temporary credentials on demand.
43+ ci <- setSTSCredential assumeRole "https://play.min.io"
44+ res <- runMinio ci $ do
45+ buckets <- listBuckets
46+ liftIO $ print $ "Top 5 buckets: " ++ show (take 5 buckets)
47+ print res
examples/BucketExists.hs+4−7
@@ -16,20 +16,17 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.IO.Class (liftIO)
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27-- https://play.min.io. The endpoint and associated
26-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35 let bucket = "missingbucket"
32 let bucket = "missingbucket"
@@ -39,5 +36,5 @@ main = do
39 liftIO $ putStrLn $ "Does " ++ show bucket ++ " exist? - " ++ show foundBucket
36 liftIO $ putStrLn $ "Does " ++ show bucket ++ " exist? - " ++ show foundBucket
40
37
41 case res1 of
38 case res1 of
42− Left e -> putStrLn $ "bucketExists failed." ++ show e
39+ Left e -> putStrLn $ "bucketExists failed." ++ show e
43 Right () -> return ()
40 Right () -> return ()
examples/CopyObject.hs+12−14
@@ -16,42 +16,40 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
21+import Network.Minio
23−import UnliftIO.Exception (catch, throwIO)
22+import UnliftIO.Exception (catch, throwIO)
24
23
25-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
26-- https://play.min.io. The endpoint and associated
25-- https://play.min.io. The endpoint and associated
27-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
28--
27--
29-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
30−--
31−
32main :: IO ()
29main :: IO ()
33main = do
30main = do
34− let
35− bucket = "test"
31+ let bucket = "test"
36 object = "obj"
32 object = "obj"
37 objectCopy = "obj-copy"
33 objectCopy = "obj-copy"
38 localFile = "/etc/lsb-release"
34 localFile = "/etc/lsb-release"
39
35
40 res1 <- runMinio minioPlayCI $ do
36 res1 <- runMinio minioPlayCI $ do
41 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
37 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
38+ catch
42− catch (makeBucket bucket Nothing) (
39+ (makeBucket bucket Nothing)
43− \e -> case e of
40+ ( \e -> case e of
44− BucketAlreadyOwnedByYou -> return ()
41+ BucketAlreadyOwnedByYou -> return ()
45− _ -> throwIO e
42+ _ -> throwIO e
46 )
43 )
47
44
48 -- 2. Upload a file to bucket/object.
45 -- 2. Upload a file to bucket/object.
49 fPutObject bucket object localFile defaultPutObjectOptions
46 fPutObject bucket object localFile defaultPutObjectOptions
50
47
51 -- 3. Copy bucket/object to bucket/objectCopy.
48 -- 3. Copy bucket/object to bucket/objectCopy.
49+ copyObject
52− copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
50+ defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
53− defaultSourceInfo { srcBucket = bucket , srcObject = object }
51+ defaultSourceInfo {srcBucket = bucket, srcObject = object}
54
52
55 case res1 of
53 case res1 of
56− Left e -> putStrLn $ "copyObject failed." ++ show e
54+ Left e -> putStrLn $ "copyObject failed." ++ show e
57 Right () -> putStrLn "copyObject succeeded."
55 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+23−24
@@ -16,40 +16,39 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−
21−{-# LANGUAGE OverloadedStrings #-}
19+{-# LANGUAGE OverloadedStrings #-}
22{-# LANGUAGE ScopedTypeVariables #-}
20{-# LANGUAGE ScopedTypeVariables #-}
23−import Network.Minio
24
21
25−import Data.Monoid ((<>))
26−import Data.Text (pack)
22+import Data.Text (pack)
23+import Network.Minio
27−import Options.Applicative
24+import Options.Applicative
28−import System.FilePath.Posix
25+import System.FilePath.Posix
29−import UnliftIO (throwIO, try)
26+import UnliftIO (throwIO, try)
30−
31−import Prelude
27+import Prelude
32
28
33-- | The following example uses minio's play server at
29-- | The following example uses minio's play server at
34-- https://play.min.io. The endpoint and associated
30-- https://play.min.io. The endpoint and associated
35-- credentials are provided via the libary constant,
31-- credentials are provided via the libary constant,
36--
32--
37-- > minioPlayCI :: ConnectInfo
33-- > minioPlayCI :: ConnectInfo
38−--
39
34
40-- optparse-applicative package based command-line parsing.
35-- optparse-applicative package based command-line parsing.
41fileNameArgs :: Parser FilePath
36fileNameArgs :: Parser FilePath
42−fileNameArgs = strArgument
37+fileNameArgs =
38+ strArgument
43− (metavar "FILENAME"
39+ ( metavar "FILENAME"
44− <> help "Name of file to upload to AWS S3 or a MinIO server")
40+ <> help "Name of file to upload to AWS S3 or a MinIO server"
41+ )
45
42
46cmdParser :: ParserInfo FilePath
43cmdParser :: ParserInfo FilePath
47−cmdParser = info
44+cmdParser =
45+ info
48− (helper <*> fileNameArgs)
46+ (helper <*> fileNameArgs)
49− (fullDesc
47+ ( fullDesc
50− <> progDesc "FileUploader"
48+ <> progDesc "FileUploader"
51− <> header
49+ <> header
52− "FileUploader - a simple file-uploader program using minio-hs")
50+ "FileUploader - a simple file-uploader program using minio-hs"
51+ )
53
52
54main :: IO ()
53main :: IO ()
55main = do
54main = do
@@ -64,12 +63,12 @@ main = do
64 bErr <- try $ makeBucket bucket Nothing
63 bErr <- try $ makeBucket bucket Nothing
65 case bErr of
64 case bErr of
66 Left BucketAlreadyOwnedByYou -> return ()
65 Left BucketAlreadyOwnedByYou -> return ()
67− Left e -> throwIO e
66+ Left e -> throwIO e
68− Right _ -> return ()
67+ Right _ -> return ()
69
68
70 -- Upload filepath to bucket; object is derived from filepath.
69 -- Upload filepath to bucket; object is derived from filepath.
71 fPutObject bucket object filepath defaultPutObjectOptions
70 fPutObject bucket object filepath defaultPutObjectOptions
72
71
73 case res of
72 case res of
74− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
73+ Left e -> putStrLn $ "file upload failed due to " ++ show e
75 Right () -> putStrLn "file upload succeeded."
74 Right () -> putStrLn "file upload succeeded."
examples/GetConfig.hs+6−6
@@ -17,14 +17,14 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
26+ res <-
27+ runMinio
28− res <- runMinio minioPlayCI $
28+ minioPlayCI
29 getConfig
29 getConfig
30 print res
30 print res
examples/GetObject.hs+5−10
@@ -16,31 +16,26 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import qualified Data.Conduit as C
21+import qualified Data.Conduit as C
24import qualified Data.Conduit.Binary as CB
22import qualified Data.Conduit.Binary as CB
25−
23+import Network.Minio
26−import Prelude
24+import Prelude
27
25
28-- | The following example uses minio's play server at
26-- | The following example uses minio's play server at
29-- https://play.min.io. The endpoint and associated
27-- https://play.min.io. The endpoint and associated
30-- credentials are provided via the libary constant,
28-- credentials are provided via the libary constant,
31--
29--
32-- > minioPlayCI :: ConnectInfo
30-- > minioPlayCI :: ConnectInfo
33−--
34−
35main :: IO ()
31main :: IO ()
36main = do
32main = do
37− let
38− bucket = "my-bucket"
33+ let bucket = "my-bucket"
39 object = "my-object"
34 object = "my-object"
40 res <- runMinio minioPlayCI $ do
35 res <- runMinio minioPlayCI $ do
41 src <- getObject bucket object defaultGetObjectOptions
36 src <- getObject bucket object defaultGetObjectOptions
42 C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
37 C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
43
38
44 case res of
39 case res of
45− Left e -> putStrLn $ "getObject failed." ++ (show e)
40+ Left e -> putStrLn $ "getObject failed." ++ show e
46 Right _ -> putStrLn "getObject succeeded."
41 Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs+8−11
@@ -16,28 +16,25 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.S3API
23
20
21+import Network.Minio
22+import Network.Minio.S3API
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27-- https://play.min.io. The endpoint and associated
26-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
37 object = "passwd"
33 object = "passwd"
34+ res <-
38− res <- runMinio minioPlayCI $
35+ runMinio minioPlayCI $
39− headObject bucket object []
36+ headObject bucket object []
40
37
41 case res of
38 case res of
42− Left e -> putStrLn $ "headObject failed." ++ show e
39+ Left e -> putStrLn $ "headObject failed." ++ show e
43 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
40 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
examples/Heal.hs+11−8
@@ -17,18 +17,21 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
28 res <- runMinio minioPlayCI $
26 res <- runMinio minioPlayCI $
29 do
27 do
28+ hsr <-
29+ startHeal
30+ Nothing
31+ Nothing
32+ HealOpts
30− hsr <- startHeal Nothing Nothing HealOpts { hoRecursive = True
33+ { hoRecursive = True,
31− , hoDryRun = False
34+ hoDryRun = False
32− }
35+ }
33 getHealStatus Nothing Nothing (hsrClientToken hsr)
36 getHealStatus Nothing Nothing (hsrClientToken hsr)
34 print res
37 print res
examples/ListBuckets.hs+3−5
@@ -16,19 +16,17 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.IO.Class (liftIO)
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27-- https://play.min.io. The endpoint and associated
26-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32
30
33-- This example list buckets that belongs to the user and returns
31-- This example list buckets that belongs to the user and returns
34-- region of the first bucket returned.
32-- region of the first bucket returned.
examples/ListIncompleteUploads.hs+17−19
@@ -16,38 +16,36 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Conduit
21+import Conduit
22+import Network.Minio
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27-- https://play.min.io. The endpoint and associated
26-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
37
33
38 -- Performs a recursive listing of incomplete uploads under bucket "test"
34 -- Performs a recursive listing of incomplete uploads under bucket "test"
39 -- on a local minio server.
35 -- on a local minio server.
36+ res <-
40− res <- runMinio minioPlayCI $
37+ runMinio minioPlayCI $
38+ runConduit $
41− runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
39+ listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
42 print res
40 print res
43
41
44− {-
42+{-
45− Following is the output of the above program on a local MinIO server.
43+ Following is the output of the above program on a local MinIO server.
46
44
47− Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
45+ Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
48− , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
46+ , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
49− , uiInitTime = 2017-03-01 10:16:25.698 UTC
47+ , uiInitTime = 2017-03-01 10:16:25.698 UTC
50− , uiSize = 17731794
48+ , uiSize = 17731794
51− }
49+ }
52− ]
50+ ]
53− -}
51+-}
examples/ListObjects.hs+13−15
@@ -16,33 +16,31 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−
23−import Conduit
24−import Prelude
25
20
21+import Conduit
22+import Network.Minio
23+import Prelude
26
24
27-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
28-- https://play.min.io. The endpoint and associated
26-- https://play.min.io. The endpoint and associated
29-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
30--
28--
31-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
32−--
33−
34main :: IO ()
30main :: IO ()
35main = do
31main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
38
33
39 -- Performs a recursive listing of all objects under bucket "test"
34 -- Performs a recursive listing of all objects under bucket "test"
40 -- on play.min.io.
35 -- on play.min.io.
36+ res <-
41− res <- runMinio minioPlayCI $
37+ runMinio minioPlayCI $
38+ runConduit $
42− runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
39+ listObjects bucket Nothing True .| mapM_C (liftIO . print)
43 print res
40 print res
44− {-
45− Following is the output of the above program on a local MinIO server.
46
41
42+{-
43+ Following is the output of the above program on a local MinIO server.
44+
47− Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]
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}]
48− -}
46+-}
examples/MakeBucket.hs+6−9
@@ -16,24 +16,21 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−
21{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
23
20
21+import Network.Minio
24−import Prelude
22+import Prelude
25
23
26-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
27-- https://play.min.io. The endpoint and associated
25-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
29--
27--
30-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
29main :: IO ()
34main = do
30main = do
35 let bucket = "my-bucket"
31 let bucket = "my-bucket"
32+ res <-
36− res <- runMinio minioPlayCI $
33+ runMinio minioPlayCI $
37− -- N B the region provided for makeBucket is optional.
34+ -- N B the region provided for makeBucket is optional.
38− makeBucket bucket (Just "us-east-1")
35+ makeBucket bucket (Just "us-east-1")
39 print res
36 print res
examples/PresignedGetObject.hs+32−28
@@ -16,39 +16,37 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.IO.Class (liftIO)
21+import Control.Monad.IO.Class (liftIO)
24−import qualified Data.ByteString.Char8 as B
22+import qualified Data.ByteString.Char8 as B
25−import Data.CaseInsensitive (original)
23+import Data.CaseInsensitive (original)
26import qualified Data.Conduit.Combinators as CC
24import 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
28
27
29-- | The following example uses minio's play server at
28-- | The following example uses minio's play server at
30-- https://play.min.io. The endpoint and associated
29-- https://play.min.io. The endpoint and associated
31-- credentials are provided via the libary constant,
30-- credentials are provided via the libary constant,
32--
31--
33-- > minioPlayCI :: ConnectInfo
32-- > minioPlayCI :: ConnectInfo
34−--
35−
36main :: IO ()
33main :: IO ()
37main = do
34main = do
38− let
39− bucket = "my-bucket"
35+ let bucket = "my-bucket"
40− object = "my-object"
36+ object = "my-object"
41− kb15 = 15*1024
37+ kb15 = 15 * 1024
42−
43− -- Set query parameter to modify content disposition response
38+ -- Set query parameter to modify content disposition response
44− -- header
39+ -- header
40+ queryParam =
45− queryParam = [("response-content-disposition",
41+ [ ( "response-content-disposition",
46− Just "attachment; filename=\"your-filename.txt\"")]
42+ Just "attachment; filename=\"your-filename.txt\""
43+ )
44+ ]
47
45
48 res <- runMinio minioPlayCI $ do
46 res <- runMinio minioPlayCI $ do
49 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
47 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
50 putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
48 putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
51− liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
49+ liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
52
50
53 -- Extract Etag of uploaded object
51 -- Extract Etag of uploaded object
54 oi <- statObject bucket object defaultGetObjectOptions
52 oi <- statObject bucket object defaultGetObjectOptions
@@ -61,23 +59,29 @@ main = do
61 -- Generate a URL with 7 days expiry time - note that the headers
59 -- Generate a URL with 7 days expiry time - note that the headers
62 -- used above must be added to the request with the signed URL
60 -- used above must be added to the request with the signed URL
63 -- generated.
61 -- generated.
62+ url <-
64− url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)
63+ presignedGetObjectUrl
64+ "my-bucket"
65+ "my-object"
66+ (7 * 24 * 3600)
65− queryParam headers
67+ queryParam
68+ headers
66
69
67 return (headers, etag, url)
70 return (headers, etag, url)
68
71
69 case res of
72 case res of
70 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
73 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
71 Right (headers, _, url) -> do
74 Right (headers, _, url) -> do
72−
73 -- We generate a curl command to demonstrate usage of the signed
75 -- We generate a curl command to demonstrate usage of the signed
74 -- URL.
76 -- URL.
75− let
76− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
77+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
78+ curlCmd =
77− curlCmd = B.intercalate " " $
79+ B.intercalate " " $
80+ ["curl --fail"]
78− ["curl --fail"] ++ map hdrOpt headers ++
81+ ++ map hdrOpt headers
79− ["-o /tmp/myfile", B.concat ["'", url, "'"]]
82+ ++ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
80
83
84+ putStrLn $
81− putStrLn $ "The following curl command would use the presigned " ++
85+ "The following curl command would use the presigned "
82− "URL to fetch the object and write it to \"/tmp/myfile\":"
86+ ++ "URL to fetch the object and write it to \"/tmp/myfile\":"
83 B.putStrLn curlCmd
87 B.putStrLn curlCmd
examples/PresignedPostPolicy.hs+42−39
@@ -16,69 +16,72 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import qualified Data.ByteString as B
21+import qualified Data.ByteString as B
24import qualified Data.ByteString.Char8 as Char8
22import qualified Data.ByteString.Char8 as Char8
25−import qualified Data.HashMap.Strict as H
23+import qualified Data.HashMap.Strict as H
26−import qualified Data.Text.Encoding as Enc
24+import qualified Data.Text.Encoding as Enc
27−import qualified Data.Time as Time
25+import qualified Data.Time as Time
26+import Network.Minio
28
27
29-- | The following example uses minio's play server at
28-- | The following example uses minio's play server at
30-- https://play.min.io. The endpoint and associated
29-- https://play.min.io. The endpoint and associated
31-- credentials are provided via the libary constant,
30-- credentials are provided via the libary constant,
32--
31--
33-- > minioPlayCI :: ConnectInfo
32-- > minioPlayCI :: ConnectInfo
34−--
35−
36main :: IO ()
33main :: IO ()
37main = do
34main = do
38 now <- Time.getCurrentTime
35 now <- Time.getCurrentTime
39− let
40− bucket = "my-bucket"
36+ let bucket = "my-bucket"
41− object = "photos/my-object"
37+ object = "photos/my-object"
42−
43− -- set an expiration time of 10 days
38+ -- set an expiration time of 10 days
44− expireTime = Time.addUTCTime (3600 * 24 * 10) now
39+ expireTime = Time.addUTCTime (3600 * 24 * 10) now
45−
46− -- create a policy with expiration time and conditions - since the
40+ -- create a policy with expiration time and conditions - since the
47− -- conditions are validated, newPostPolicy returns an Either value
41+ -- conditions are validated, newPostPolicy returns an Either value
42+ policyE =
48− policyE = newPostPolicy expireTime
43+ newPostPolicy
44+ expireTime
49− [ -- set the object name condition
45+ [ -- set the object name condition
50− ppCondKey object
46+ ppCondKey object,
51− -- set the bucket name condition
47+ -- set the bucket name condition
52− , ppCondBucket bucket
48+ ppCondBucket bucket,
53− -- set the size range of object as 1B to 10MiB
49+ -- set the size range of object as 1B to 10MiB
54− , ppCondContentLengthRange 1 (10*1024*1024)
50+ ppCondContentLengthRange 1 (10 * 1024 * 1024),
55− -- set content type as jpg image
51+ -- set content type as jpg image
56− , ppCondContentType "image/jpeg"
52+ ppCondContentType "image/jpeg",
57− -- on success set the server response code to 200
53+ -- on success set the server response code to 200
58− , ppCondSuccessActionStatus 200
54+ ppCondSuccessActionStatus 200
59− ]
55+ ]
60
56
61 case policyE of
57 case policyE of
62− Left err -> putStrLn $ show err
58+ Left err -> print err
63 Right policy -> do
59 Right policy -> do
64 res <- runMinio minioPlayCI $ do
60 res <- runMinio minioPlayCI $ do
65 (url, formData) <- presignedPostPolicy policy
61 (url, formData) <- presignedPostPolicy policy
66
62
67 -- a curl command is output to demonstrate using the generated
63 -- a curl command is output to demonstrate using the generated
68 -- URL and form-data
64 -- URL and form-data
69− let
65+ let formFn (k, v) =
66+ B.concat
67+ [ "-F ",
70− formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
68+ Enc.encodeUtf8 k,
69+ "=",
70+ "'",
71− "'", v, "'"]
71+ v,
72+ "'"
73+ ]
72− formOptions = B.intercalate " " $ map formFn $ H.toList formData
74+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
73−
74
75
76+ return $
75− return $ B.intercalate " " $
77+ B.intercalate
78+ " "
76− ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
79+ ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
77
80
78 case res of
81 case res of
79− Left e -> putStrLn $ "post-policy error: " ++ (show e)
82+ Left e -> putStrLn $ "post-policy error: " ++ show e
80 Right cmd -> do
83 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"
82
85
83 -- print the generated curl command
86 -- print the generated curl command
84 Char8.putStrLn cmd
87 Char8.putStrLn cmd
examples/PresignedPutObject.hs+20−21
@@ -16,44 +16,43 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23import qualified Data.ByteString.Char8 as B
21import qualified Data.ByteString.Char8 as B
24−import Data.CaseInsensitive (original)
22+import Data.CaseInsensitive (original)
23+import Network.Minio
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27-- https://play.min.io. The endpoint and associated
26-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35− let
36− -- Use headers to set user-metadata - note that this header will
32+ let -- 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.
33+ -- need to be set when the URL is used to make an upload.
34+ headers =
38− headers = [("x-amz-meta-url-creator",
35+ [ ( "x-amz-meta-url-creator",
39− "minio-hs-presigned-put-example")]
36+ "minio-hs-presigned-put-example"
37+ )
38+ ]
40 res <- runMinio minioPlayCI $ do
39 res <- runMinio minioPlayCI $ do
41−
42 -- generate a URL with 7 days expiry time
40 -- 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
44
42
45 case res of
43 case res of
46 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
44 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
47 Right url -> do
45 Right url -> do
48−
49 -- We generate a curl command to demonstrate usage of the signed
46 -- We generate a curl command to demonstrate usage of the signed
50 -- URL.
47 -- URL.
51− let
52− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
48+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
49+ curlCmd =
53− curlCmd = B.intercalate " " $
50+ B.intercalate " " $
51+ ["curl "]
54− ["curl "] ++ map hdrOpt headers ++
52+ ++ map hdrOpt headers
55− ["-T /tmp/myfile", B.concat ["'", url, "'"]]
53+ ++ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
56−
54+
55+ putStrLn $
57− putStrLn $ "The following curl command would use the presigned " ++
56+ "The following curl command would use the presigned "
58− "URL to upload the file at \"/tmp/myfile\":"
57+ ++ "URL to upload the file at \"/tmp/myfile\":"
59 B.putStrLn curlCmd
58 B.putStrLn curlCmd
examples/PutObject.hs+11−14
@@ -16,39 +16,36 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23import qualified Data.Conduit.Combinators as CC
21import qualified Data.Conduit.Combinators as CC
24−
22+import Network.Minio
25−import Prelude
23+import Prelude
26
24
27-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
28-- https://play.min.io. The endpoint and associated
26-- https://play.min.io. The endpoint and associated
29-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
30--
28--
31-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
32−--
33−
34main :: IO ()
30main :: IO ()
35main = do
31main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
38 object = "obj"
33 object = "obj"
39 localFile = "/etc/lsb-release"
34 localFile = "/etc/lsb-release"
40 kb15 = 15 * 1024
35 kb15 = 15 * 1024
41
36
42 -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
37 -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
38+ res1 <-
43− res1 <- runMinio minioPlayCI $
39+ runMinio minioPlayCI $
44− putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
40+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
45 case res1 of
41 case res1 of
46− Left e -> putStrLn $ "putObject failed." ++ show e
42+ Left e -> putStrLn $ "putObject failed." ++ show e
47 Right () -> putStrLn "putObject succeeded."
43 Right () -> putStrLn "putObject succeeded."
48
44
49 -- Eg 2. Upload a file using fPutObject with default options.
45 -- Eg 2. Upload a file using fPutObject with default options.
46+ res2 <-
50− res2 <- runMinio minioPlayCI $
47+ runMinio minioPlayCI $
51− fPutObject bucket object localFile defaultPutObjectOptions
48+ fPutObject bucket object localFile defaultPutObjectOptions
52 case res2 of
49 case res2 of
53− Left e -> putStrLn $ "fPutObject failed." ++ show e
50+ Left e -> putStrLn $ "fPutObject failed." ++ show e
54 Right () -> putStrLn "fPutObject succeeded."
51 Right () -> putStrLn "fPutObject succeeded."
examples/RemoveBucket.hs+3−8
@@ -16,23 +16,18 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−
21{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
23
20
21+import Network.Minio
24−import Prelude
22+import Prelude
25
23
26-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
27-- https://play.min.io. The endpoint and associated
25-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
29--
27--
30-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
29main :: IO ()
34main = do
30main = do
35− let
36− bucket = "my-bucket"
31+ let bucket = "my-bucket"
37 res <- runMinio minioPlayCI $ removeBucket bucket
32 res <- runMinio minioPlayCI $ removeBucket bucket
38 print res
33 print res
examples/RemoveIncompleteUpload.hs+7−10
@@ -16,27 +16,24 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
21+import Network.Minio
23−import Prelude
22+import Prelude
24
23
25-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
26-- https://play.min.io. The endpoint and associated
25-- https://play.min.io. The endpoint and associated
27-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
28--
27--
29-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
30−--
31−
32main :: IO ()
29main :: IO ()
33main = do
30main = do
34− let
35− bucket = "mybucket"
31+ let bucket = "mybucket"
36− object = "myobject"
32+ object = "myobject"
37
33
34+ res <-
38− res <- runMinio minioPlayCI $
35+ runMinio minioPlayCI $
39− removeIncompleteUpload bucket object
36+ removeIncompleteUpload bucket object
40
37
41 case res of
38 case res of
42 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
39 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/RemoveObject.hs+7−8
@@ -16,20 +16,19 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21
20
22−import Network.Minio
21+import Network.Minio
23−import Prelude
22+import Prelude
24
23
25main :: IO ()
24main :: IO ()
26main = do
25main = do
27− let
28− bucket = "mybucket"
26+ let bucket = "mybucket"
29− object = "myobject"
27+ object = "myobject"
30
28
29+ res <-
31− res <- runMinio minioPlayCI $
30+ runMinio minioPlayCI $
32− removeObject bucket object
31+ removeObject bucket object
33
32
34 case res of
33 case res of
35 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
34 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/SelectObject.hs+24−26
@@ -16,34 +16,32 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−
23−import qualified Conduit as C
24−import Control.Monad (when)
25
20
21+import qualified Conduit as C
22+import Control.Monad (unless)
23+import Network.Minio
26−import Prelude
24+import Prelude
27
25
28main :: IO ()
26main :: IO ()
29main = do
27main = do
30− let bucket = "selectbucket"
28+ let bucket = "selectbucket"
31− object = "1.csv"
29+ object = "1.csv"
30+ content =
32− content = "Name,Place,Temperature\n"
31+ "Name,Place,Temperature\n"
33− <> "James,San Jose,76\n"
32+ <> "James,San Jose,76\n"
34− <> "Alicia,San Leandro,88\n"
33+ <> "Alicia,San Leandro,88\n"
35− <> "Mark,San Carlos,90\n"
34+ <> "Mark,San Carlos,90\n"
36−
35+
37− res <- runMinio minioPlayCI $ do
36+ res <- runMinio minioPlayCI $ do
38−
39− exists <- bucketExists bucket
37+ exists <- bucketExists bucket
40− when (not exists) $
38+ unless exists $
41− makeBucket bucket Nothing
39+ makeBucket bucket Nothing
42−
40+
43− C.liftIO $ putStrLn "Uploading csv object"
41+ C.liftIO $ putStrLn "Uploading csv object"
44− putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
42+ putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
45−
43+
46− let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
44+ let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
47− res <- selectObjectContent bucket object sr
45+ res <- selectObjectContent bucket object sr
48− C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
46+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
49− print res
47+ print res
examples/ServerInfo.hs+7−7
@@ -17,14 +17,14 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
26+ res <-
27+ runMinio
28− res <- runMinio minioPlayCI $
28+ minioPlayCI
29− getServerInfo
29+ getServerInfo
30 print res
30 print res
examples/ServiceSendRestart.hs+6−7
@@ -17,14 +17,13 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
26+ res <-
28− res <- runMinio minioPlayCI $
27+ runMinio minioPlayCI $
29− serviceSendAction ServiceActionRestart
28+ serviceSendAction ServiceActionRestart
30 print res
29 print res
examples/ServiceSendStop.hs+6−7
@@ -17,14 +17,13 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
26+ res <-
28− res <- runMinio minioPlayCI $
27+ runMinio minioPlayCI $
29− serviceSendAction ServiceActionStop
28+ serviceSendAction ServiceActionStop
30 print res
29 print res
examples/ServiceStatus.hs+7−7
@@ -17,14 +17,14 @@
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19
19
20−{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
20+import Network.Minio
22−import Network.Minio.AdminAPI
21+import Network.Minio.AdminAPI
23−
24−import Prelude
22+import Prelude
25
23
26main :: IO ()
24main :: IO ()
27main = do
25main = do
26+ res <-
27+ runMinio
28− res <- runMinio minioPlayCI $
28+ minioPlayCI
29− serviceStatus
29+ serviceStatus
30 print res
30 print res
examples/SetConfig.hs+3−4
@@ -16,12 +16,11 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
23
20
21+import Network.Minio
22+import Network.Minio.AdminAPI
24−import Prelude
23+import Prelude
25
24
26main :: IO ()
25main :: IO ()
27main = do
26main = do
minio-hs.cabal+73−17
@@ -1,6 +1,6 @@
1−cabal-version: 2.2
1+cabal-version: 2.4
2name: minio-hs
2name: minio-hs
3−version: 1.5.2
3+version: 1.7.0
4synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
4synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
5 storage.
5 storage.
6description: The MinIO Haskell client library provides simple APIs to
6description: The MinIO Haskell client library provides simple APIs to
@@ -14,29 +14,70 @@ maintainer: dev@min.io
14category: Network, AWS, Object Storage
14category: Network, AWS, Object Storage
15build-type: Simple
15build-type: Simple
16stability: Experimental
16stability: Experimental
17−extra-source-files:
17+extra-doc-files:
18 CHANGELOG.md
18 CHANGELOG.md
19 CONTRIBUTING.md
19 CONTRIBUTING.md
20 docs/API.md
20 docs/API.md
21− examples/*.hs
22 README.md
21 README.md
22+extra-source-files:
23+ examples/*.hs
23 stack.yaml
24 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
24
31
32+source-repository head
33+ type: git
34+ location: https://github.com/minio/minio-hs.git
35+
36+Flag dev
37+ Description: Build package in development mode
38+ Default: False
39+ Manual: True
25
40
26common base-settings
41common base-settings
27 ghc-options: -Wall
42 ghc-options: -Wall
43+ -Wcompat
44+ -Widentities
45+ -Wincomplete-uni-patterns
46+ -Wincomplete-record-updates
47+ -haddock
48+ if impl(ghc >= 8.0)
49+ ghc-options: -Wredundant-constraints
50+ if impl(ghc >= 8.2)
51+ ghc-options: -fhide-source-paths
52+ if impl(ghc >= 8.4)
53+ ghc-options: -Wpartial-fields
54+ -- -Wmissing-export-lists
55+ if impl(ghc >= 8.8)
56+ ghc-options: -Wmissing-deriving-strategies
57+ -Werror=missing-deriving-strategies
58+ -- if impl(ghc >= 8.10)
59+ -- ghc-options: -Wunused-packages -- disabled due to bug related to mixin config
60+ if impl(ghc >= 9.0)
61+ ghc-options: -Winvalid-haddock
62+ if impl(ghc >= 9.2)
63+ ghc-options: -Wredundant-bang-patterns
64+ if flag(dev)
65+ ghc-options: -Werror
66+
28 default-language: Haskell2010
67 default-language: Haskell2010
68+
29 default-extensions: BangPatterns
69 default-extensions: BangPatterns
70+ , DerivingStrategies
30 , FlexibleContexts
71 , FlexibleContexts
31 , FlexibleInstances
72 , FlexibleInstances
73+ , LambdaCase
32 , MultiParamTypeClasses
74 , MultiParamTypeClasses
33 , MultiWayIf
75 , MultiWayIf
34− , NoImplicitPrelude
35 , OverloadedStrings
76 , OverloadedStrings
36 , RankNTypes
77 , RankNTypes
37 , ScopedTypeVariables
78 , ScopedTypeVariables
38− , TypeFamilies
39 , TupleSections
79 , TupleSections
80+
40 other-modules: Lib.Prelude
81 other-modules: Lib.Prelude
41 , Network.Minio.API
82 , Network.Minio.API
42 , Network.Minio.APICommon
83 , Network.Minio.APICommon
@@ -54,22 +95,31 @@ common base-settings
54 , Network.Minio.Utils
95 , Network.Minio.Utils
55 , Network.Minio.XmlGenerator
96 , Network.Minio.XmlGenerator
56 , Network.Minio.XmlParser
97 , Network.Minio.XmlParser
98+ , Network.Minio.XmlCommon
57 , Network.Minio.JsonParser
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+
58 build-depends: base >= 4.7 && < 5
108 build-depends: base >= 4.7 && < 5
59− , protolude >= 0.2 && < 0.3
109+ , relude >= 0.7 && < 2
60− , aeson >= 1.2
110+ , aeson >= 1.2 && < 3
61 , base64-bytestring >= 1.0
111 , base64-bytestring >= 1.0
62 , binary >= 0.8.5.0
112 , binary >= 0.8.5.0
63 , bytestring >= 0.10
113 , bytestring >= 0.10
64 , case-insensitive >= 1.2
114 , case-insensitive >= 1.2
65 , conduit >= 1.3
115 , conduit >= 1.3
66 , conduit-extra >= 1.3
116 , conduit-extra >= 1.3
67− , connection
117+ , crypton-connection
68 , cryptonite >= 0.25
118 , cryptonite >= 0.25
69 , cryptonite-conduit >= 0.2
119 , cryptonite-conduit >= 0.2
120+ , data-default-class
70 , digest >= 0.0.1
121 , digest >= 0.0.1
71 , directory
122 , directory
72− , exceptions
73 , filepath >= 1.4
123 , filepath >= 1.4
74 , http-client >= 0.5
124 , http-client >= 0.5
75 , http-client-tls
125 , http-client-tls
@@ -77,14 +127,15 @@ common base-settings
77 , http-types >= 0.12
127 , http-types >= 0.12
78 , ini
128 , ini
79 , memory >= 0.14
129 , memory >= 0.14
80− , raw-strings-qq >= 1
130+ , network-uri
81 , resourcet >= 1.2
131 , resourcet >= 1.2
82 , retry
132 , retry
83 , text >= 1.2
133 , text >= 1.2
84− , time >= 1.8
134+ , time >= 1.9
135+ , time-units ^>= 1.0.0
85 , transformers >= 0.5
136 , transformers >= 0.5
86− , unliftio >= 0.2
137+ , unliftio >= 0.2 && < 0.3
87− , unliftio-core >= 0.1 && < 0.2
138+ , unliftio-core >= 0.2 && < 0.3
88 , unordered-containers >= 0.2
139 , unordered-containers >= 0.2
89 , xml-conduit >= 1.8
140 , xml-conduit >= 1.8
90
141
@@ -115,7 +166,9 @@ test-suite minio-hs-live-server-test
115 , Network.Minio.Utils.Test
166 , Network.Minio.Utils.Test
116 , Network.Minio.XmlGenerator.Test
167 , Network.Minio.XmlGenerator.Test
117 , Network.Minio.XmlParser.Test
168 , Network.Minio.XmlParser.Test
169+ , Network.Minio.Credentials
118 build-depends: minio-hs
170 build-depends: minio-hs
171+ , raw-strings-qq
119 , tasty
172 , tasty
120 , tasty-hunit
173 , tasty-hunit
121 , tasty-quickcheck
174 , tasty-quickcheck
@@ -130,6 +183,7 @@ test-suite minio-hs-test
130 hs-source-dirs: test, src
183 hs-source-dirs: test, src
131 main-is: Spec.hs
184 main-is: Spec.hs
132 build-depends: minio-hs
185 build-depends: minio-hs
186+ , raw-strings-qq
133 , QuickCheck
187 , QuickCheck
134 , tasty
188 , tasty
135 , tasty-hunit
189 , tasty-hunit
@@ -146,6 +200,7 @@ test-suite minio-hs-test
146 , Network.Minio.Utils.Test
200 , Network.Minio.Utils.Test
147 , Network.Minio.XmlGenerator.Test
201 , Network.Minio.XmlGenerator.Test
148 , Network.Minio.XmlParser.Test
202 , Network.Minio.XmlParser.Test
203+ , Network.Minio.Credentials
149
204
150Flag examples
205Flag examples
151 Description: Build the examples
206 Description: Build the examples
@@ -292,6 +347,7 @@ executable SetConfig
292 scope: private
347 scope: private
293 main-is: SetConfig.hs
348 main-is: SetConfig.hs
294
349
295−source-repository head
350+executable AssumeRole
296− type: git
351+ import: examples-settings
297− location: https://github.com/minio/minio-hs
352+ scope: private
353+ main-is: AssumeRole.hs
src/Lib/Prelude.hs+33−11
@@ -15,19 +15,41 @@
15--
15--
16
16
17module Lib.Prelude
17module Lib.Prelude
18− ( module Exports
18+ ( module Exports,
19− , both
19+ both,
20+ showBS,
21+ toStrictBS,
22+ fromStrictBS,
23+ lastMay,
24+ )
20− ) where
25+where
21
26
22−import Protolude as Exports hiding (catch, catches,
23− throwIO, try)
24−
25−import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
27+import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
28+import qualified Data.ByteString.Lazy as LB
26−import Data.Time as Exports (UTCTime (..),
29+import Data.Time as Exports
30+ ( UTCTime (..),
27− diffUTCTime)
31+ diffUTCTime,
32+ )
28−import UnliftIO as Exports (catch, catches, throwIO,
33+import UnliftIO as Exports
34+ ( Handler,
35+ catch,
36+ catches,
37+ throwIO,
29− try)
38+ try,
39+ )
30
40
31-- | Apply a function on both elements of a pair
41-- | Apply a function on both elements of a pair
32both :: (a -> b) -> (a, a) -> (b, b)
42both :: (a -> b) -> (a, a) -> (b, b)
33both f (a, b) = (f a, f b)
43both f (a, b) = (f a, f b)
44+
45+showBS :: (Show a) => a -> ByteString
46+showBS a = encodeUtf8 (show a :: Text)
47+
48+toStrictBS :: LByteString -> ByteString
49+toStrictBS = LB.toStrict
50+
51+fromStrictBS :: ByteString -> LByteString
52+fromStrictBS = LB.fromStrict
53+
54+lastMay :: [a] -> Maybe a
55+lastMay a = last <$> nonEmpty a
src/Network/Minio.hs+249−218
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -16,224 +16,237 @@
16
16
17-- |
17-- |
18-- Module: Network.Minio
18-- Module: Network.Minio
19−-- Copyright: (c) 2017-2019 MinIO Dev Team
19+-- Copyright: (c) 2017-2023 MinIO Dev Team
20-- License: Apache 2.0
20-- License: Apache 2.0
21-- Maintainer: MinIO Dev Team <dev@min.io>
21-- Maintainer: MinIO Dev Team <dev@min.io>
22--
22--
23-- Types and functions to conveniently access S3 compatible object
23-- Types and functions to conveniently access S3 compatible object
24-- storage servers like MinIO.
24-- storage servers like MinIO.
25−
26module Network.Minio
25module Network.Minio
27− (
28− -- * Credentials
26+ ( -- * Credentials
29− Credentials (..)
27+ CredentialValue (..),
30−
28+ credentialValueText,
29+ AccessKey (..),
30+ SecretKey (..),
31+ SessionToken (..),
32+
31− -- ** Credential providers
33+ -- ** Credential Loaders
34+
32− -- | Run actions that retrieve 'Credentials' from the environment or
35+ -- | Run actions that retrieve 'CredentialValue's from the environment or
33− -- files or other custom sources.
36+ -- files or other custom sources.
34− , Provider
37+ CredentialLoader,
35− , fromAWSConfigFile
38+ fromAWSConfigFile,
36− , fromAWSEnv
39+ fromAWSEnv,
37− , fromMinioEnv
40+ fromMinioEnv,
38− , findFirst
41+ findFirst,
39−
42+
40− -- * Connecting to object storage
43+ -- * Connecting to object storage
41− , ConnectInfo
44+ ConnectInfo,
42− , setRegion
45+ setRegion,
43− , setCreds
46+ setCreds,
44− , setCredsFrom
47+ setCredsFrom,
45− , isConnectInfoSecure
48+ isConnectInfoSecure,
46− , disableTLSCertValidation
49+ disableTLSCertValidation,
47− , MinioConn
50+ MinioConn,
48− , mkMinioConn
51+ mkMinioConn,
49−
52+
50− -- ** Connection helpers
53+ -- ** Connection helpers
54+
51− -- | These are helpers to construct 'ConnectInfo' values for common
55+ -- | These are helpers to construct 'ConnectInfo' values for common
52− -- cases.
56+ -- cases.
53− , minioPlayCI
57+ minioPlayCI,
54− , awsCI
58+ awsCI,
55− , gcsCI
59+ gcsCI,
56−
60+
61+ -- ** STS Credential types
62+ STSAssumeRole (..),
63+ STSAssumeRoleOptions (..),
64+ defaultSTSAssumeRoleOptions,
65+ requestSTSCredential,
66+ setSTSCredential,
67+ ExpiryTime (..),
68+ STSCredentialProvider,
69+
57− -- * Minio Monad
70+ -- * Minio Monad
71+
58− ----------------
72+ ----------------
73+
59− -- | The Minio Monad provides connection-reuse, bucket-location
74+ -- | The Minio Monad provides connection-reuse, bucket-location
60− -- caching, resource management and simpler error handling
75+ -- caching, resource management and simpler error handling
61− -- functionality. All actions on object storage are performed within
76+ -- functionality. All actions on object storage are performed within
62− -- this Monad.
77+ -- this Monad.
63− , Minio
78+ Minio,
64− , runMinioWith
79+ runMinioWith,
65− , runMinio
80+ runMinio,
66− , runMinioResWith
81+ runMinioResWith,
67− , runMinioRes
82+ runMinioRes,
68−
83+
69− -- * Bucket Operations
84+ -- * Bucket Operations
70−
85+
71− -- ** Creation, removal and querying
86+ -- ** Creation, removal and querying
72− , Bucket
87+ Bucket,
73− , makeBucket
88+ makeBucket,
74− , removeBucket
89+ removeBucket,
75− , bucketExists
90+ bucketExists,
76− , Region
91+ Region,
77− , getLocation
92+ getLocation,
78−
93+
79− -- ** Listing buckets
94+ -- ** Listing buckets
80− , BucketInfo(..)
95+ BucketInfo (..),
81− , listBuckets
96+ listBuckets,
82−
97+
83− -- ** Listing objects
98+ -- ** Listing objects
84− , listObjects
99+ listObjects,
85− , listObjectsV1
100+ listObjectsV1,
86− , ListItem(..)
101+ ListItem (..),
87−
88− , ObjectInfo
102+ ObjectInfo,
89− , oiObject
103+ oiObject,
90− , oiModTime
104+ oiModTime,
91− , oiETag
105+ oiETag,
92− , oiSize
106+ oiSize,
93− , oiUserMetadata
107+ oiUserMetadata,
94− , oiMetadata
108+ oiMetadata,
95−
109+
96− -- ** Listing incomplete uploads
110+ -- ** Listing incomplete uploads
97− , listIncompleteUploads
111+ listIncompleteUploads,
98− , UploadId
112+ UploadId,
99− , UploadInfo(..)
113+ UploadInfo (..),
100− , listIncompleteParts
114+ listIncompleteParts,
101− , ObjectPartInfo(..)
115+ ObjectPartInfo (..),
102−
116+
103− -- ** Bucket Notifications
117+ -- ** Bucket Notifications
104− , getBucketNotification
118+ getBucketNotification,
105− , putBucketNotification
119+ putBucketNotification,
106− , removeAllBucketNotification
120+ removeAllBucketNotification,
107− , Notification(..)
121+ Notification (..),
108− , defaultNotification
122+ defaultNotification,
109− , NotificationConfig(..)
123+ NotificationConfig (..),
110− , Arn
124+ Arn,
111− , Event(..)
125+ Event (..),
112− , Filter(..)
126+ Filter (..),
113− , defaultFilter
127+ defaultFilter,
114− , FilterKey(..)
128+ FilterKey (..),
115− , defaultFilterKey
129+ defaultFilterKey,
116− , FilterRules(..)
130+ FilterRules (..),
117− , defaultFilterRules
131+ defaultFilterRules,
118− , FilterRule(..)
132+ FilterRule (..),
119−
133+
120− -- * Object Operations
134+ -- * Object Operations
121− , Object
135+ Object,
122−
136+
123− -- ** File-based operations
137+ -- ** File-based operations
124− , fGetObject
138+ fGetObject,
125− , fPutObject
139+ fPutObject,
126−
140+
127− -- ** Conduit-based streaming operations
141+ -- ** Conduit-based streaming operations
128− , putObject
142+ putObject,
129− , PutObjectOptions
143+ PutObjectOptions,
130− , defaultPutObjectOptions
144+ defaultPutObjectOptions,
131− , pooContentType
145+ pooContentType,
132− , pooContentEncoding
146+ pooContentEncoding,
133− , pooContentDisposition
147+ pooContentDisposition,
134− , pooContentLanguage
148+ pooContentLanguage,
135− , pooCacheControl
149+ pooCacheControl,
136− , pooStorageClass
150+ pooStorageClass,
137− , pooUserMetadata
151+ pooUserMetadata,
138− , pooNumThreads
152+ pooNumThreads,
139− , pooSSE
153+ pooSSE,
140−
141− , getObject
154+ getObject,
142− , GetObjectOptions
155+ GetObjectOptions,
143− , defaultGetObjectOptions
156+ defaultGetObjectOptions,
144− , gooRange
157+ gooRange,
145− , gooIfMatch
158+ gooIfMatch,
146− , gooIfNoneMatch
159+ gooIfNoneMatch,
147− , gooIfModifiedSince
160+ gooIfModifiedSince,
148− , gooIfUnmodifiedSince
161+ gooIfUnmodifiedSince,
149− , gooSSECKey
162+ gooSSECKey,
150− , GetObjectResponse
163+ GetObjectResponse,
151− , gorObjectInfo
164+ gorObjectInfo,
152− , gorObjectStream
165+ gorObjectStream,
153−
166+
154− -- ** Server-side object copying
167+ -- ** Server-side object copying
155− , copyObject
168+ copyObject,
156− , SourceInfo
169+ SourceInfo,
157− , defaultSourceInfo
170+ defaultSourceInfo,
158− , srcBucket
171+ srcBucket,
159− , srcObject
172+ srcObject,
160− , srcRange
173+ srcRange,
161− , srcIfMatch
174+ srcIfMatch,
162− , srcIfNoneMatch
175+ srcIfNoneMatch,
163− , srcIfModifiedSince
176+ srcIfModifiedSince,
164− , srcIfUnmodifiedSince
177+ srcIfUnmodifiedSince,
165− , DestinationInfo
178+ DestinationInfo,
166− , defaultDestinationInfo
179+ defaultDestinationInfo,
167− , dstBucket
180+ dstBucket,
168− , dstObject
181+ dstObject,
169−
182+
170− -- ** Querying object info
183+ -- ** Querying object info
171− , statObject
184+ statObject,
172−
185+
173− -- ** Object removal operations
186+ -- ** Object removal operations
174− , removeObject
187+ removeObject,
175− , removeIncompleteUpload
188+ removeIncompleteUpload,
176−
189+
177− -- ** Select Object Content with SQL
190+ -- ** Select Object Content with SQL
178− , module Network.Minio.SelectAPI
191+ module Network.Minio.SelectAPI,
179−
192+
180− -- * Server-Side Encryption Helpers
193+ -- * Server-Side Encryption Helpers
181− , mkSSECKey
194+ mkSSECKey,
182− , SSECKey
195+ SSECKey,
183− , SSE(..)
196+ SSE (..),
184−
197+
185− -- * Presigned Operations
198+ -- * Presigned Operations
186− , presignedPutObjectUrl
199+ presignedPutObjectUrl,
187− , presignedGetObjectUrl
200+ presignedGetObjectUrl,
188− , presignedHeadObjectUrl
201+ presignedHeadObjectUrl,
189− , UrlExpiry
202+ UrlExpiry,
190−
203+
191− -- ** POST (browser) upload helpers
204+ -- ** POST (browser) upload helpers
205+
192− -- | Please see
206+ -- | Please see
193− -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
207+ -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
194− -- for detailed information.
208+ -- for detailed information.
195− , newPostPolicy
209+ newPostPolicy,
196− , presignedPostPolicy
210+ presignedPostPolicy,
197− , showPostPolicy
211+ showPostPolicy,
198− , PostPolicy
212+ PostPolicy,
199− , PostPolicyError(..)
213+ PostPolicyError (..),
200−
214+
201− -- *** Post Policy condition helpers
215+ -- *** Post Policy condition helpers
202− , PostPolicyCondition
216+ PostPolicyCondition,
203− , ppCondBucket
217+ ppCondBucket,
204− , ppCondContentLengthRange
218+ ppCondContentLengthRange,
205− , ppCondContentType
219+ ppCondContentType,
206− , ppCondKey
220+ ppCondKey,
207− , ppCondKeyStartsWith
221+ ppCondKeyStartsWith,
208− , ppCondSuccessActionStatus
222+ ppCondSuccessActionStatus,
209−
223+
210− -- * Error handling
224+ -- * Error handling
225+
211− -- | Data types representing various errors that may occur while
226+ -- | Data types representing various errors that may occur while
212− -- working with an object storage service.
227+ -- working with an object storage service.
213− , MinioErr(..)
228+ MinioErr (..),
214− , MErrV(..)
229+ MErrV (..),
215− , ServiceErr(..)
230+ ServiceErr (..),
216−
231+ )
217−) where
232+where
218
233
219{-
234{-
220This module exports the high-level MinIO API for object storage.
235This module exports the high-level MinIO API for object storage.
221-}
236-}
222
237
223−import qualified Data.Conduit as C
238+import qualified Data.Conduit as C
224−import qualified Data.Conduit.Binary as CB
239+import qualified Data.Conduit.Binary as CB
225import qualified Data.Conduit.Combinators as CC
240import qualified Data.Conduit.Combinators as CC
226−
241+import Network.Minio.API
227−import Lib.Prelude
228−
229−import Network.Minio.CopyObject
242+import Network.Minio.CopyObject
243+import Network.Minio.Credentials
230−import Network.Minio.Data
244+import Network.Minio.Data
231−import Network.Minio.Errors
245+import Network.Minio.Errors
232−import Network.Minio.ListOps
246+import Network.Minio.ListOps
233−import Network.Minio.PutObject
247+import Network.Minio.PutObject
234−import Network.Minio.S3API
248+import Network.Minio.S3API
235−import Network.Minio.SelectAPI
249+import Network.Minio.SelectAPI
236−import Network.Minio.Utils
237
250
238-- | Lists buckets.
251-- | Lists buckets.
239listBuckets :: Minio [BucketInfo]
252listBuckets :: Minio [BucketInfo]
@@ -248,8 +261,12 @@ fGetObject bucket object fp opts = do
248 C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
261 C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
249
262
250-- | Upload the given file to the given object.
263-- | Upload the given file to the given object.
251−fPutObject :: Bucket -> Object -> FilePath
264+fPutObject ::
265+ Bucket ->
266+ Object ->
267+ FilePath ->
252− -> PutObjectOptions -> Minio ()
268+ PutObjectOptions ->
269+ Minio ()
253fPutObject bucket object f opts =
270fPutObject bucket object f opts =
254 void $ putObjectInternal bucket object opts $ ODFile f Nothing
271 void $ putObjectInternal bucket object opts $ ODFile f Nothing
255
272
@@ -257,8 +274,13 @@ fPutObject bucket object f opts =
257-- known; this helps the library select optimal part sizes to perform
274-- known; this helps the library select optimal part sizes to perform
258-- a multipart upload. If not specified, it is assumed that the object
275-- a multipart upload. If not specified, it is assumed that the object
259-- can be potentially 5TiB and selects multipart sizes appropriately.
276-- can be potentially 5TiB and selects multipart sizes appropriately.
277+putObject ::
278+ Bucket ->
279+ Object ->
260−putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio ()
280+ C.ConduitM () ByteString Minio () ->
281+ Maybe Int64 ->
261− -> Maybe Int64 -> PutObjectOptions -> Minio ()
282+ PutObjectOptions ->
283+ Minio ()
262putObject bucket object src sizeMay opts =
284putObject bucket object src sizeMay opts =
263 void $ putObjectInternal bucket object opts $ ODStream src sizeMay
285 void $ putObjectInternal bucket object opts $ ODStream src sizeMay
264
286
@@ -268,18 +290,25 @@ putObject bucket object src sizeMay opts =
268-- copy operation if the new object is to be greater than 5GiB in
290-- copy operation if the new object is to be greater than 5GiB in
269-- size.
291-- size.
270copyObject :: DestinationInfo -> SourceInfo -> Minio ()
292copyObject :: DestinationInfo -> SourceInfo -> Minio ()
271−copyObject dstInfo srcInfo = void $ copyObjectInternal (dstBucket dstInfo)
293+copyObject dstInfo srcInfo =
294+ void $
295+ copyObjectInternal
296+ (dstBucket dstInfo)
272− (dstObject dstInfo) srcInfo
297+ (dstObject dstInfo)
298+ srcInfo
273
299
274-- | Remove an object from the object store.
300-- | Remove an object from the object store.
275removeObject :: Bucket -> Object -> Minio ()
301removeObject :: Bucket -> Object -> Minio ()
276removeObject = deleteObject
302removeObject = deleteObject
277
303
278-- | Get an object from the object store.
304-- | Get an object from the object store.
305+getObject ::
306+ Bucket ->
307+ Object ->
279−getObject :: Bucket -> Object -> GetObjectOptions
308+ GetObjectOptions ->
280− -> Minio GetObjectResponse
309+ Minio GetObjectResponse
281getObject bucket object opts =
310getObject bucket object opts =
282− getObject' bucket object [] $ gooToHeaders opts
311+ getObject' bucket object [] $ gooToHeaders opts
283
312
284-- | Get an object's metadata from the object store. It accepts the
313-- | Get an object's metadata from the object store. It accepts the
285-- same options as GetObject.
314-- same options as GetObject.
@@ -309,6 +338,8 @@ bucketExists = headBucket
309-- | Removes an ongoing multipart upload of an object.
338-- | Removes an ongoing multipart upload of an object.
310removeIncompleteUpload :: Bucket -> Object -> Minio ()
339removeIncompleteUpload :: Bucket -> Object -> Minio ()
311removeIncompleteUpload bucket object = do
340removeIncompleteUpload bucket object = do
341+ uploads <-
342+ C.runConduit $
312− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
343+ listIncompleteUploads bucket (Just object) False
313− C..| CC.sinkList
344+ C..| CC.sinkList
314 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
345 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs+259−159
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,169 +15,260 @@
15--
15--
16
16
17module Network.Minio.API
17module Network.Minio.API
18− ( connect
18+ ( connect,
19− , S3ReqInfo(..)
19+ S3ReqInfo (..),
20− , runMinio
20+ runMinio,
21− , executeRequest
21+ executeRequest,
22+ buildRequest,
22− , mkStreamRequest
23+ mkStreamRequest,
23− , getLocation
24+ getLocation,
24−
25− , isValidBucketName
25+ isValidBucketName,
26− , checkBucketNameValidity
26+ checkBucketNameValidity,
27− , isValidObjectName
27+ isValidObjectName,
28− , checkObjectNameValidity
28+ checkObjectNameValidity,
29+ requestSTSCredential,
30+ )
29− ) where
31+where
30−
32+
31−import Control.Retry (fullJitterBackoff,
33+import Control.Retry
34+ ( fullJitterBackoff,
32− limitRetriesByCumulativeDelay,
35+ limitRetriesByCumulativeDelay,
33− retrying)
36+ retrying,
37+ )
34−import qualified Data.ByteString as B
38+import qualified Data.ByteString as B
35−import qualified Data.Char as C
39+import qualified Data.Char as C
36−import qualified Data.Conduit as C
40+import qualified Data.Conduit as C
37−import qualified Data.HashMap.Strict as H
41+import qualified Data.HashMap.Strict as H
38−import qualified Data.Text as T
42+import qualified Data.Text as T
39−import qualified Data.Time.Clock as Time
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
40−import Network.HTTP.Conduit (Response)
47+import Network.HTTP.Conduit (Response)
41−import qualified Network.HTTP.Conduit as NC
48+import qualified Network.HTTP.Conduit as NC
49+import Network.HTTP.Types (simpleQueryToQuery)
42−import qualified Network.HTTP.Types as HT
50+import qualified Network.HTTP.Types as HT
43−import Network.HTTP.Types.Header (hHost)
51+import Network.HTTP.Types.Header (hHost)
44−
45−import Lib.Prelude
46−
47−import Network.Minio.APICommon
52+import Network.Minio.APICommon
53+import Network.Minio.Credentials
48−import Network.Minio.Data
54+import Network.Minio.Data
49−import Network.Minio.Errors
55+import Network.Minio.Errors
50−import Network.Minio.Sign.V4
56+import Network.Minio.Sign.V4
51−import Network.Minio.Utils
57+import Network.Minio.Utils
52−import Network.Minio.XmlParser
58+import Network.Minio.XmlParser
53
59
54-- | Fetch bucket location (region)
60-- | Fetch bucket location (region)
55getLocation :: Bucket -> Minio Region
61getLocation :: Bucket -> Minio Region
56getLocation bucket = do
62getLocation bucket = do
63+ resp <-
64+ executeRequest $
57− resp <- executeRequest $ defaultS3ReqInfo {
65+ defaultS3ReqInfo
58− riBucket = Just bucket
66+ { riBucket = Just bucket,
59− , riQueryParams = [("location", Nothing)]
67+ riQueryParams = [("location", Nothing)],
60− , riNeedsLocation = False
68+ riNeedsLocation = False
61− }
69+ }
62 parseLocation $ NC.responseBody resp
70 parseLocation $ NC.responseBody resp
63
71
64−
65-- | Looks for region in RegionMap and updates it using getLocation if
72-- | Looks for region in RegionMap and updates it using getLocation if
66-- absent.
73-- absent.
67discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
74discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
68discoverRegion ri = runMaybeT $ do
75discoverRegion ri = runMaybeT $ do
69 bucket <- MaybeT $ return $ riBucket ri
76 bucket <- MaybeT $ return $ riBucket ri
70 regionMay <- lift $ lookupRegionCache bucket
77 regionMay <- lift $ lookupRegionCache bucket
71− maybe (do
78+ maybe
79+ ( do
72− l <- lift $ getLocation bucket
80+ l <- lift $ getLocation bucket
73− lift $ addToRegionCache bucket l
81+ lift $ addToRegionCache bucket l
74− return l
82+ return l
83+ )
84+ return
75− ) return regionMay
85+ regionMay
76−
86+
87+-- | Returns the region to be used for the request.
77getRegion :: S3ReqInfo -> Minio (Maybe Region)
88getRegion :: S3ReqInfo -> Minio (Maybe Region)
78getRegion ri = do
89getRegion ri = do
79− ci <- asks mcConnInfo
90+ ci <- asks mcConnInfo
80−
91+
81− -- getService/makeBucket/getLocation -- don't need location
92+ -- getService/makeBucket/getLocation -- don't need location
93+ if
82− if | not $ riNeedsLocation ri ->
94+ | not $ riNeedsLocation ri ->
83− return $ Just $ connectRegion ci
95+ return $ Just $ connectRegion ci
84−
85− -- if autodiscovery of location is disabled by user
96+ -- if autodiscovery of location is disabled by user
86− | not $ connectAutoDiscoverRegion ci ->
97+ | not $ connectAutoDiscoverRegion ci ->
87− return $ Just $ connectRegion ci
98+ return $ Just $ connectRegion ci
88−
89− -- discover the region for the request
99+ -- discover the region for the request
90− | otherwise -> discoverRegion ri
100+ | otherwise -> discoverRegion ri
91
101
92getRegionHost :: Region -> Minio Text
102getRegionHost :: Region -> Minio Text
93getRegionHost r = do
103getRegionHost r = do
94− ci <- asks mcConnInfo
104+ ci <- asks mcConnInfo
95−
105+
96− if "amazonaws.com" `T.isSuffixOf` connectHost ci
106+ if "amazonaws.com" `T.isSuffixOf` connectHost ci
107+ then
108+ maybe
97− then maybe (throwIO $ MErrVRegionNotSupported r)
109+ (throwIO $ MErrVRegionNotSupported r)
110+ return
98− return (H.lookup r awsRegionMap)
111+ (H.lookup r awsRegionMap)
99 else return $ connectHost ci
112 else return $ connectHost ci
100
113
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+
101buildRequest :: S3ReqInfo -> Minio NC.Request
164buildRequest :: S3ReqInfo -> Minio NC.Request
102buildRequest ri = do
165buildRequest ri = do
103− maybe (return ()) checkBucketNameValidity $ riBucket ri
166+ maybe (return ()) checkBucketNameValidity $ riBucket ri
104− maybe (return ()) checkObjectNameValidity $ riObject ri
167+ maybe (return ()) checkObjectNameValidity $ riObject ri
105−
168+
106− ci <- asks mcConnInfo
169+ ci <- asks mcConnInfo
107−
170+
108− regionMay <- getRegion ri
171+ (host, path, regionMay) <- getHostPathRegion ri
109−
172+
110− regionHost <- maybe (return $ connectHost ci) getRegionHost regionMay
111−
112− let ri' = ri { riHeaders = hostHeader : riHeaders ri
113− , riRegion = regionMay
114− }
115− ci' = ci { connectHost = regionHost }
173+ let ci' = ci {connectHost = host}
116− hostHeader = (hHost, getHostAddr ci')
174+ hostHeader = (hHost, getHostAddr ci')
117−
175+ ri' =
176+ ri
177+ { riHeaders = hostHeader : riHeaders ri,
178+ riRegion = regionMay
179+ }
118− -- Does not contain body and auth info.
180+ -- Does not contain body and auth info.
181+ baseRequest =
119− baseRequest = NC.defaultRequest
182+ NC.defaultRequest
120− { NC.method = riMethod ri'
183+ { NC.method = riMethod ri',
121− , NC.secure = connectIsSecure ci'
184+ NC.secure = connectIsSecure ci',
122− , NC.host = encodeUtf8 $ connectHost ci'
185+ NC.host = encodeUtf8 $ connectHost ci',
123− , NC.port = connectPort ci'
186+ NC.port = connectPort ci',
124− , NC.path = getS3Path (riBucket ri') (riObject ri')
187+ NC.path = path,
125− , NC.requestHeaders = riHeaders ri'
188+ NC.requestHeaders = riHeaders ri',
126− , NC.queryString = HT.renderQuery False $ riQueryParams ri'
189+ NC.queryString = HT.renderQuery False $ riQueryParams ri'
127− }
190+ }
128−
191+
129− timeStamp <- liftIO Time.getCurrentTime
192+ timeStamp <- liftIO Time.getCurrentTime
130−
131− let sp = SignParams (connectAccessKey ci') (connectSecretKey ci')
132− timeStamp (riRegion ri') Nothing Nothing
133−
134− -- Cases to handle:
135− --
136− -- 1. Connection is secure: use unsigned payload
137− --
138− -- 2. Insecure connection, streaming signature is enabled via use of
139− -- conduit payload: use streaming signature for request.
140− --
141− -- 3. Insecure connection, non-conduit payload: compute payload
142− -- sha256hash, buffer request in memory and perform request.
143−
144− -- case 2 from above.
145− if | isStreamingPayload (riPayload ri') &&
146− (not $ connectIsSecure ci') -> do
147− (pLen, pSrc) <- case riPayload ri of
148− PayloadC l src -> return (l, src)
149− _ -> throwIO MErrVUnexpectedPayload
150− let reqFn = signV4Stream pLen sp baseRequest
151− return $ reqFn pSrc
152−
153− | otherwise -> do
154− -- case 1 described above.
155− sp' <- if | connectIsSecure ci' -> return sp
156− -- case 3 described above.
157− | otherwise -> do
158− pHash <- getPayloadSHA256Hash $ riPayload ri'
159− return $ sp { spPayloadHash = Just pHash }
160−
161− let signHeaders = signV4 sp' baseRequest
162− return $ baseRequest
163− { NC.requestHeaders =
164− NC.requestHeaders baseRequest ++
165− mkHeaderFromPairs signHeaders
166− , NC.requestBody = getRequestBody (riPayload ri')
167− }
168
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+ }
169
258
170retryAPIRequest :: Minio a -> Minio a
259retryAPIRequest :: Minio a -> Minio a
171retryAPIRequest apiCall = do
260retryAPIRequest apiCall = do
261+ resE <-
172− resE <- retrying retryPolicy (const shouldRetry) $
262+ retrying retryPolicy (const shouldRetry) $
263+ const $
173− const $ try apiCall
264+ try apiCall
174 either throwIO return resE
265 either throwIO return resE
175 where
266 where
176 -- Retry using the full-jitter backoff method for up to 10 mins
267 -- Retry using the full-jitter backoff method for up to 10 mins
177 -- total
268 -- total
269+ retryPolicy =
178− retryPolicy = limitRetriesByCumulativeDelay tenMins
270+ limitRetriesByCumulativeDelay tenMins $
179− $ fullJitterBackoff oneMilliSecond
271+ fullJitterBackoff oneMilliSecond
180−
181 oneMilliSecond = 1000 -- in microseconds
272 oneMilliSecond = 1000 -- in microseconds
182 tenMins = 10 * 60 * 1000000 -- in microseconds
273 tenMins = 10 * 60 * 1000000 -- in microseconds
183 -- retry on connection related failure
274 -- retry on connection related failure
@@ -189,23 +280,23 @@ retryAPIRequest apiCall = do
189 -- API request failed with a retryable exception
280 -- API request failed with a retryable exception
190 Left httpExn@(NC.HttpExceptionRequest _ exn) ->
281 Left httpExn@(NC.HttpExceptionRequest _ exn) ->
191 case (exn :: NC.HttpExceptionContent) of
282 case (exn :: NC.HttpExceptionContent) of
192− NC.ResponseTimeout -> return True
283+ NC.ResponseTimeout -> return True
193− NC.ConnectionTimeout -> return True
284+ NC.ConnectionTimeout -> return True
194 NC.ConnectionFailure _ -> return True
285 NC.ConnectionFailure _ -> return True
195 -- We received an unexpected exception
286 -- We received an unexpected exception
196− _ -> throwIO httpExn
287+ _ -> throwIO httpExn
197 -- We received an unexpected exception
288 -- We received an unexpected exception
198 Left someOtherExn -> throwIO someOtherExn
289 Left someOtherExn -> throwIO someOtherExn
199
290
200−
201executeRequest :: S3ReqInfo -> Minio (Response LByteString)
291executeRequest :: S3ReqInfo -> Minio (Response LByteString)
202executeRequest ri = do
292executeRequest ri = do
203 req <- buildRequest ri
293 req <- buildRequest ri
204 mgr <- asks mcConnManager
294 mgr <- asks mcConnManager
205 retryAPIRequest $ httpLbs req mgr
295 retryAPIRequest $ httpLbs req mgr
206
296
207−mkStreamRequest :: S3ReqInfo
297+mkStreamRequest ::
298+ S3ReqInfo ->
208− -> Minio (Response (C.ConduitM () ByteString Minio ()))
299+ Minio (Response (C.ConduitM () ByteString Minio ()))
209mkStreamRequest ri = do
300mkStreamRequest ri = do
210 req <- buildRequest ri
301 req <- buildRequest ri
211 mgr <- asks mcConnManager
302 mgr <- asks mcConnManager
@@ -214,41 +305,50 @@ mkStreamRequest ri = do
214-- Bucket name validity check according to AWS rules.
305-- Bucket name validity check according to AWS rules.
215isValidBucketName :: Bucket -> Bool
306isValidBucketName :: Bucket -> Bool
216isValidBucketName bucket =
307isValidBucketName bucket =
308+ not
309+ ( or
217− not (or [ len < 3 || len > 63
310+ [ len < 3 || len > 63,
218− , or (map labelCheck labels)
311+ any labelCheck labels,
219− , or (map labelCharsCheck labels)
312+ any labelCharsCheck labels,
220− , isIPCheck
313+ isIPCheck
221− ])
314+ ]
315+ )
222 where
316 where
223 len = T.length bucket
317 len = T.length bucket
224 labels = T.splitOn "." bucket
318 labels = T.splitOn "." bucket
225−
226 -- does label `l` fail basic checks of length and start/end?
319 -- does label `l` fail basic checks of length and start/end?
227 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
320 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
228−
229 -- does label `l` have non-allowed characters?
321 -- does label `l` have non-allowed characters?
230− labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
322+ labelCharsCheck l =
323+ isJust $
324+ T.find
325+ ( \x ->
326+ not
327+ ( C.isAsciiLower x
231− x == '-' ||
328+ || x == '-'
232− C.isDigit x)) l
329+ || C.isDigit x
233−
330+ )
331+ )
332+ l
234 -- does label `l` have non-digit characters?
333 -- does label `l` have non-digit characters?
235 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
334 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
236 labelAsNums = map (not . labelNonDigits) labels
335 labelAsNums = map (not . labelNonDigits) labels
237−
238 -- check if bucket name looks like an IP
336 -- check if bucket name looks like an IP
239 isIPCheck = and labelAsNums && length labelAsNums == 4
337 isIPCheck = and labelAsNums && length labelAsNums == 4
240
338
241-- Throws exception iff bucket name is invalid according to AWS rules.
339-- Throws exception iff bucket name is invalid according to AWS rules.
242−checkBucketNameValidity :: MonadIO m => Bucket -> m ()
340+checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
243checkBucketNameValidity bucket =
341checkBucketNameValidity bucket =
244− when (not $ isValidBucketName bucket) $
342+ unless (isValidBucketName bucket) $
343+ throwIO $
245− throwIO $ MErrVInvalidBucketName bucket
344+ MErrVInvalidBucketName bucket
246
345
247isValidObjectName :: Object -> Bool
346isValidObjectName :: Object -> Bool
248isValidObjectName object =
347isValidObjectName object =
249 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
348 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
250
349
251−checkObjectNameValidity :: MonadIO m => Object -> m ()
350+checkObjectNameValidity :: (MonadIO m) => Object -> m ()
252checkObjectNameValidity object =
351checkObjectNameValidity object =
253− when (not $ isValidObjectName object) $
352+ unless (isValidObjectName object) $
353+ throwIO $
254− throwIO $ MErrVInvalidObjectName object
354+ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hs+36−24
@@ -16,37 +16,39 @@
16
16
17module Network.Minio.APICommon where
17module Network.Minio.APICommon where
18
18
19−import qualified Conduit as C
19+import qualified Conduit as C
20−import qualified Data.ByteString as BS
20+import qualified Data.ByteString as BS
21−import qualified Data.ByteString.Lazy as LB
21+import qualified Data.ByteString.Lazy as LB
22−import Data.Conduit.Binary (sourceHandleRange)
22+import Data.Conduit.Binary (sourceHandleRange)
23+import qualified Data.Text as T
24+import Lib.Prelude
23−import qualified Network.HTTP.Conduit as NC
25+import qualified Network.HTTP.Conduit as NC
24−import qualified Network.HTTP.Types as HT
26+import qualified Network.HTTP.Types as HT
25−
26−import Lib.Prelude
27−
28−import Network.Minio.Data
27+import Network.Minio.Data
29−import Network.Minio.Data.Crypto
28+import Network.Minio.Data.Crypto
30−import Network.Minio.Errors
29+import Network.Minio.Errors
31
30
32sha256Header :: ByteString -> HT.Header
31sha256Header :: ByteString -> HT.Header
33−sha256Header = ("x-amz-content-sha256", )
32+sha256Header = ("x-amz-content-sha256",)
34
33
35-- | This function throws an error if the payload is a conduit (as it
34-- | This function throws an error if the payload is a conduit (as it
36-- will not be possible to re-read the conduit after it is consumed).
35-- will not be possible to re-read the conduit after it is consumed).
37getPayloadSHA256Hash :: Payload -> Minio ByteString
36getPayloadSHA256Hash :: Payload -> Minio ByteString
38getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
37getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
39−getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
38+getPayloadSHA256Hash (PayloadH h off size) =
39+ hashSHA256FromSource $
40− sourceHandleRange h
40+ sourceHandleRange
41+ h
41− (return . fromIntegral $ off)
42+ (return . fromIntegral $ off)
42− (return . fromIntegral $ size)
43+ (return . fromIntegral $ size)
43getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
44getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
44
45
45getRequestBody :: Payload -> NC.RequestBody
46getRequestBody :: Payload -> NC.RequestBody
46getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
47getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
47getRequestBody (PayloadH h off size) =
48getRequestBody (PayloadH h off size) =
48− NC.requestBodySource (fromIntegral size) $
49+ NC.requestBodySource size $
49− sourceHandleRange h
50+ sourceHandleRange
51+ h
50 (return . fromIntegral $ off)
52 (return . fromIntegral $ off)
51 (return . fromIntegral $ size)
53 (return . fromIntegral $ size)
52getRequestBody (PayloadC n src) = NC.requestBodySource n src
54getRequestBody (PayloadC n src) = NC.requestBodySource n src
@@ -55,14 +57,24 @@ mkStreamingPayload :: Payload -> Payload
55mkStreamingPayload payload =
57mkStreamingPayload payload =
56 case payload of
58 case payload of
57 PayloadBS bs ->
59 PayloadBS bs ->
60+ PayloadC
58− PayloadC (fromIntegral $ BS.length bs)
61+ (fromIntegral $ BS.length bs)
59 (C.sourceLazy $ LB.fromStrict bs)
62 (C.sourceLazy $ LB.fromStrict bs)
60 PayloadH h off len ->
63 PayloadH h off len ->
64+ PayloadC len $
61− PayloadC len $ sourceHandleRange h
65+ sourceHandleRange
66+ h
62− (return . fromIntegral $ off)
67+ (return . fromIntegral $ off)
63− (return . fromIntegral $ len)
68+ (return . fromIntegral $ len)
64 _ -> payload
69 _ -> payload
65
70
66isStreamingPayload :: Payload -> Bool
71isStreamingPayload :: Payload -> Bool
67isStreamingPayload (PayloadC _ _) = True
72isStreamingPayload (PayloadC _ _) = True
68−isStreamingPayload _ = False
73+isStreamingPayload _ = False
74+
75+-- | Checks if the connect info is for Amazon S3.
76+isAWSConnectInfo :: ConnectInfo -> Bool
77+isAWSConnectInfo ci = ".amazonaws.com" `T.isSuffixOf` connectHost ci
78+
79+bucketHasPeriods :: Bucket -> Bool
80+bucketHasPeriods b = isJust $ T.find (== '.') b
src/Network/Minio/AdminAPI.hs+543−391
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2018-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -16,270 +16,341 @@
16
16
17module Network.Minio.AdminAPI
17module Network.Minio.AdminAPI
18 ( -- * MinIO Admin API
18 ( -- * MinIO Admin API
19+
19 --------------------
20 --------------------
21+
20 -- | Provides MinIO admin API and related types. It is in
22 -- | Provides MinIO admin API and related types. It is in
21 -- experimental state.
23 -- experimental state.
22− DriveInfo(..)
24+ DriveInfo (..),
23− , ErasureInfo(..)
25+ ErasureInfo (..),
24− , Backend(..)
26+ Backend (..),
25− , ConnStats(..)
27+ ConnStats (..),
26− , HttpStats(..)
28+ HttpStats (..),
27− , ServerProps(..)
29+ ServerProps (..),
28− , CountNAvgTime(..)
30+ CountNAvgTime (..),
29− , StorageClass(..)
31+ StorageClass (..),
30− , StorageInfo(..)
32+ StorageInfo (..),
31− , SIData(..)
33+ SIData (..),
32− , ServerInfo(..)
34+ ServerInfo (..),
33− , getServerInfo
35+ getServerInfo,
34−
35− , HealOpts(..)
36+ HealOpts (..),
36− , HealResultItem(..)
37+ HealResultItem (..),
37− , HealStatus(..)
38+ HealStatus (..),
38− , HealStartResp(..)
39+ HealStartResp (..),
39− , startHeal
40+ startHeal,
40− , forceStartHeal
41+ forceStartHeal,
41− , getHealStatus
42+ getHealStatus,
42−
43− , SetConfigResult(..)
43+ SetConfigResult (..),
44− , NodeSummary(..)
44+ NodeSummary (..),
45− , setConfig
45+ setConfig,
46− , getConfig
46+ getConfig,
47−
48− , ServerVersion(..)
47+ ServerVersion (..),
49− , ServiceStatus(..)
48+ ServiceStatus (..),
50− , serviceStatus
49+ serviceStatus,
51−
52− , ServiceAction(..)
50+ ServiceAction (..),
53− , serviceSendAction
51+ serviceSendAction,
52+ )
54− ) where
53+where
55−
54+
56−import Data.Aeson (FromJSON, ToJSON, Value (Object),
55+import Data.Aeson
56+ ( FromJSON,
57+ ToJSON,
58+ Value (Object),
57− eitherDecode, object, pairs,
59+ eitherDecode,
60+ object,
61+ pairs,
62+ parseJSON,
58− parseJSON, toEncoding, toJSON,
63+ toEncoding,
64+ toJSON,
59− withObject, withText, (.:), (.:?),
65+ withObject,
66+ withText,
67+ (.:),
68+ (.:?),
60− (.=))
69+ (.=),
70+ )
61−import qualified Data.Aeson as A
71+import qualified Data.Aeson as A
62−import Data.Aeson.Types (typeMismatch)
72+import Data.Aeson.Types (typeMismatch)
63−import qualified Data.ByteString as B
73+import qualified Data.ByteString as B
64−import qualified Data.ByteString.Lazy as LBS
74+import qualified Data.ByteString.Lazy as LBS
65−import qualified Data.Text as T
75+import qualified Data.Text as T
66−import Data.Time (NominalDiffTime, getCurrentTime)
76+import Data.Time (NominalDiffTime, getCurrentTime)
77+import Lib.Prelude
67−import Network.HTTP.Conduit (Response)
78+import Network.HTTP.Conduit (Response)
68−import qualified Network.HTTP.Conduit as NC
79+import qualified Network.HTTP.Conduit as NC
69−import qualified Network.HTTP.Types as HT
80+import qualified Network.HTTP.Types as HT
70−import Network.HTTP.Types.Header (hHost)
81+import Network.HTTP.Types.Header (hHost)
71−
72−import Lib.Prelude
73−
74−import Network.Minio.APICommon
82+import Network.Minio.APICommon
83+import Network.Minio.Credentials
75−import Network.Minio.Data
84+import Network.Minio.Data
76−import Network.Minio.Errors
85+import Network.Minio.Errors
77−import Network.Minio.Sign.V4
86+import Network.Minio.Sign.V4
78−import Network.Minio.Utils
87+import Network.Minio.Utils
79
88
80data DriveInfo = DriveInfo
89data DriveInfo = DriveInfo
81− { diUuid :: Text
90+ { diUuid :: Text,
82− , diEndpoint :: Text
91+ diEndpoint :: Text,
83− , diState :: Text
92+ diState :: Text
93+ }
84− } deriving (Eq, Show)
94+ deriving stock (Show, Eq)
85
95
86instance FromJSON DriveInfo where
96instance FromJSON DriveInfo where
87− parseJSON = withObject "DriveInfo" $ \v -> DriveInfo
97+ parseJSON = withObject "DriveInfo" $ \v ->
98+ DriveInfo
99+ <$> v
88− <$> v .: "uuid"
100+ .: "uuid"
101+ <*> v
89− <*> v .: "endpoint"
102+ .: "endpoint"
103+ <*> v
90− <*> v .: "state"
104+ .: "state"
91
105
92data StorageClass = StorageClass
106data StorageClass = StorageClass
93− { scParity :: Int
107+ { scParity :: Int,
94− , scData :: Int
108+ scData :: Int
109+ }
95− } deriving (Eq, Show)
110+ deriving stock (Show, Eq)
96
111
97data ErasureInfo = ErasureInfo
112data ErasureInfo = ErasureInfo
98− { eiOnlineDisks :: Int
113+ { eiOnlineDisks :: Int,
99− , eiOfflineDisks :: Int
114+ eiOfflineDisks :: Int,
100− , eiStandard :: StorageClass
115+ eiStandard :: StorageClass,
101− , eiReducedRedundancy :: StorageClass
116+ eiReducedRedundancy :: StorageClass,
102− , eiSets :: [[DriveInfo]]
117+ eiSets :: [[DriveInfo]]
118+ }
103− } deriving (Eq, Show)
119+ deriving stock (Show, Eq)
104
120
105instance FromJSON ErasureInfo where
121instance FromJSON ErasureInfo where
106− parseJSON = withObject "ErasureInfo" $ \v -> do
122+ parseJSON = withObject "ErasureInfo" $ \v -> do
107− onlineDisks <- v .: "OnlineDisks"
123+ onlineDisks <- v .: "OnlineDisks"
108− offlineDisks <- v .: "OfflineDisks"
124+ offlineDisks <- v .: "OfflineDisks"
125+ stdClass <-
109− stdClass <- StorageClass
126+ StorageClass
127+ <$> v
110− <$> v .: "StandardSCData"
128+ .: "StandardSCData"
129+ <*> v
111− <*> v .: "StandardSCParity"
130+ .: "StandardSCParity"
131+ rrClass <-
112− rrClass <- StorageClass
132+ StorageClass
133+ <$> v
113− <$> v .: "RRSCData"
134+ .: "RRSCData"
135+ <*> v
114− <*> v .: "RRSCParity"
136+ .: "RRSCParity"
115− sets <- v .: "Sets"
137+ sets <- v .: "Sets"
116− return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
138+ return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
117−
139+
118−data Backend = BackendFS
140+data Backend
141+ = BackendFS
119− | BackendErasure ErasureInfo
142+ | BackendErasure ErasureInfo
120− deriving (Eq, Show)
143+ deriving stock (Show, Eq)
121
144
122instance FromJSON Backend where
145instance FromJSON Backend where
123− parseJSON = withObject "Backend" $ \v -> do
146+ parseJSON = withObject "Backend" $ \v -> do
124− typ <- v .: "Type"
147+ typ <- v .: "Type"
125− case typ :: Int of
148+ case typ :: Int of
126− 1 -> return BackendFS
149+ 1 -> return BackendFS
127− 2 -> BackendErasure <$> parseJSON (Object v)
150+ 2 -> BackendErasure <$> parseJSON (Object v)
128− _ -> typeMismatch "BackendType" (Object v)
151+ _ -> typeMismatch "BackendType" (Object v)
129
152
130data ConnStats = ConnStats
153data ConnStats = ConnStats
131− { csTransferred :: Int64
154+ { csTransferred :: Int64,
132− , csReceived :: Int64
155+ csReceived :: Int64
156+ }
133− } deriving (Eq, Show)
157+ deriving stock (Show, Eq)
134
158
135instance FromJSON ConnStats where
159instance FromJSON ConnStats where
136− parseJSON = withObject "ConnStats" $ \v -> ConnStats
160+ parseJSON = withObject "ConnStats" $ \v ->
161+ ConnStats
162+ <$> v
137− <$> v .: "transferred"
163+ .: "transferred"
164+ <*> v
138− <*> v .: "received"
165+ .: "received"
139
166
140data ServerProps = ServerProps
167data ServerProps = ServerProps
141− { spUptime :: NominalDiffTime
168+ { spUptime :: NominalDiffTime,
142− , spVersion :: Text
169+ spVersion :: Text,
143− , spCommitId :: Text
170+ spCommitId :: Text,
144− , spRegion :: Text
171+ spRegion :: Text,
145− , spSqsArns :: [Text]
172+ spSqsArns :: [Text]
173+ }
146− } deriving (Eq, Show)
174+ deriving stock (Show, Eq)
147
175
148instance FromJSON ServerProps where
176instance FromJSON ServerProps where
149− parseJSON = withObject "SIServer" $ \v -> do
177+ parseJSON = withObject "SIServer" $ \v -> do
150− uptimeNs <- v .: "uptime"
178+ uptimeNs <- v .: "uptime"
151− let uptime = uptimeNs / 1e9
179+ let uptime = uptimeNs / 1e9
152− ver <- v .: "version"
180+ ver <- v .: "version"
153− commitId <- v .: "commitID"
181+ commitId <- v .: "commitID"
154− region <- v .: "region"
182+ region <- v .: "region"
155− arn <- v .: "sqsARN"
183+ arn <- v .: "sqsARN"
156− return $ ServerProps uptime ver commitId region arn
184+ return $ ServerProps uptime ver commitId region arn
157
185
158data StorageInfo = StorageInfo
186data StorageInfo = StorageInfo
159− { siUsed :: Int64
187+ { siUsed :: Int64,
160− , siBackend :: Backend
188+ siBackend :: Backend
189+ }
161− } deriving (Eq, Show)
190+ deriving stock (Show, Eq)
162
191
163instance FromJSON StorageInfo where
192instance FromJSON StorageInfo where
164− parseJSON = withObject "StorageInfo" $ \v -> StorageInfo
193+ parseJSON = withObject "StorageInfo" $ \v ->
194+ StorageInfo
195+ <$> v
165− <$> v .: "Used"
196+ .: "Used"
197+ <*> v
166− <*> v .: "Backend"
198+ .: "Backend"
167
199
168data CountNAvgTime = CountNAvgTime
200data CountNAvgTime = CountNAvgTime
169− { caCount :: Int64
201+ { caCount :: Int64,
170− , caAvgDuration :: Text
202+ caAvgDuration :: Text
203+ }
171− } deriving (Eq, Show)
204+ deriving stock (Show, Eq)
172
205
173instance FromJSON CountNAvgTime where
206instance FromJSON CountNAvgTime where
174− parseJSON = withObject "CountNAvgTime" $ \v -> CountNAvgTime
207+ parseJSON = withObject "CountNAvgTime" $ \v ->
208+ CountNAvgTime
209+ <$> v
175− <$> v .: "count"
210+ .: "count"
211+ <*> v
176− <*> v .: "avgDuration"
212+ .: "avgDuration"
177
213
178data HttpStats = HttpStats
214data HttpStats = HttpStats
179− { hsTotalHeads :: CountNAvgTime
215+ { hsTotalHeads :: CountNAvgTime,
180− , hsSuccessHeads :: CountNAvgTime
216+ hsSuccessHeads :: CountNAvgTime,
181− , hsTotalGets :: CountNAvgTime
217+ hsTotalGets :: CountNAvgTime,
182− , hsSuccessGets :: CountNAvgTime
218+ hsSuccessGets :: CountNAvgTime,
183− , hsTotalPuts :: CountNAvgTime
219+ hsTotalPuts :: CountNAvgTime,
184− , hsSuccessPuts :: CountNAvgTime
220+ hsSuccessPuts :: CountNAvgTime,
185− , hsTotalPosts :: CountNAvgTime
221+ hsTotalPosts :: CountNAvgTime,
186− , hsSuccessPosts :: CountNAvgTime
222+ hsSuccessPosts :: CountNAvgTime,
187− , hsTotalDeletes :: CountNAvgTime
223+ hsTotalDeletes :: CountNAvgTime,
188− , hsSuccessDeletes :: CountNAvgTime
224+ hsSuccessDeletes :: CountNAvgTime
225+ }
189− } deriving (Eq, Show)
226+ deriving stock (Show, Eq)
190
227
191instance FromJSON HttpStats where
228instance FromJSON HttpStats where
192− parseJSON = withObject "HttpStats" $ \v -> HttpStats
229+ parseJSON = withObject "HttpStats" $ \v ->
230+ HttpStats
231+ <$> v
193− <$> v .: "totalHEADs"
232+ .: "totalHEADs"
233+ <*> v
194− <*> v .: "successHEADs"
234+ .: "successHEADs"
235+ <*> v
195− <*> v .: "totalGETs"
236+ .: "totalGETs"
237+ <*> v
196− <*> v .: "successGETs"
238+ .: "successGETs"
239+ <*> v
197− <*> v .: "totalPUTs"
240+ .: "totalPUTs"
241+ <*> v
198− <*> v .: "successPUTs"
242+ .: "successPUTs"
243+ <*> v
199− <*> v .: "totalPOSTs"
244+ .: "totalPOSTs"
245+ <*> v
200− <*> v .: "successPOSTs"
246+ .: "successPOSTs"
247+ <*> v
201− <*> v .: "totalDELETEs"
248+ .: "totalDELETEs"
249+ <*> v
202− <*> v .: "successDELETEs"
250+ .: "successDELETEs"
203
251
204data SIData = SIData
252data SIData = SIData
205− { sdStorage :: StorageInfo
253+ { sdStorage :: StorageInfo,
206− , sdConnStats :: ConnStats
254+ sdConnStats :: ConnStats,
207− , sdHttpStats :: HttpStats
255+ sdHttpStats :: HttpStats,
208− , sdProps :: ServerProps
256+ sdProps :: ServerProps
257+ }
209− } deriving (Eq, Show)
258+ deriving stock (Show, Eq)
210
259
211instance FromJSON SIData where
260instance FromJSON SIData where
212− parseJSON = withObject "SIData" $ \v -> SIData
261+ parseJSON = withObject "SIData" $ \v ->
262+ SIData
263+ <$> v
213− <$> v .: "storage"
264+ .: "storage"
265+ <*> v
214− <*> v .: "network"
266+ .: "network"
267+ <*> v
215− <*> v .: "http"
268+ .: "http"
269+ <*> v
216− <*> v .: "server"
270+ .: "server"
217
271
218data ServerInfo = ServerInfo
272data ServerInfo = ServerInfo
219− { siError :: Text
273+ { siError :: Text,
220− , siAddr :: Text
274+ siAddr :: Text,
221− , siData :: SIData
275+ siData :: SIData
276+ }
222− } deriving (Eq, Show)
277+ deriving stock (Show, Eq)
223
278
224instance FromJSON ServerInfo where
279instance FromJSON ServerInfo where
225− parseJSON = withObject "ServerInfo" $ \v -> ServerInfo
280+ parseJSON = withObject "ServerInfo" $ \v ->
281+ ServerInfo
282+ <$> v
226− <$> v .: "error"
283+ .: "error"
284+ <*> v
227− <*> v .: "addr"
285+ .: "addr"
286+ <*> v
228− <*> v .: "data"
287+ .: "data"
229
288
230data ServerVersion = ServerVersion
289data ServerVersion = ServerVersion
231− { svVersion :: Text
290+ { svVersion :: Text,
232− , svCommitId :: Text
291+ svCommitId :: Text
292+ }
233− } deriving (Eq, Show)
293+ deriving stock (Show, Eq)
234
294
235instance FromJSON ServerVersion where
295instance FromJSON ServerVersion where
236− parseJSON = withObject "ServerVersion" $ \v -> ServerVersion
296+ parseJSON = withObject "ServerVersion" $ \v ->
297+ ServerVersion
298+ <$> v
237− <$> v .: "version"
299+ .: "version"
300+ <*> v
238− <*> v .: "commitID"
301+ .: "commitID"
239
302
240data ServiceStatus = ServiceStatus
303data ServiceStatus = ServiceStatus
241− { ssVersion :: ServerVersion
304+ { ssVersion :: ServerVersion,
242− , ssUptime :: NominalDiffTime
305+ ssUptime :: NominalDiffTime
306+ }
243− } deriving (Eq, Show)
307+ deriving stock (Show, Eq)
244
308
245instance FromJSON ServiceStatus where
309instance FromJSON ServiceStatus where
246− parseJSON = withObject "ServiceStatus" $ \v -> do
310+ parseJSON = withObject "ServiceStatus" $ \v -> do
247− serverVersion <- v .: "serverVersion"
311+ serverVersion <- v .: "serverVersion"
248− uptimeNs <- v .: "uptime"
312+ uptimeNs <- v .: "uptime"
249− let uptime = uptimeNs / 1e9
313+ let uptime = uptimeNs / 1e9
250− return $ ServiceStatus serverVersion uptime
314+ return $ ServiceStatus serverVersion uptime
251
315
316+data ServiceAction
252−data ServiceAction = ServiceActionRestart
317+ = ServiceActionRestart
253− | ServiceActionStop
318+ | ServiceActionStop
254− deriving (Eq, Show)
319+ deriving stock (Show, Eq)
255
320
256instance ToJSON ServiceAction where
321instance ToJSON ServiceAction where
257− toJSON a = object [ "action" .= serviceActionToText a ]
322+ toJSON a = object ["action" .= serviceActionToText a]
258
323
259serviceActionToText :: ServiceAction -> Text
324serviceActionToText :: ServiceAction -> Text
260serviceActionToText a = case a of
325serviceActionToText a = case a of
261 ServiceActionRestart -> "restart"
326 ServiceActionRestart -> "restart"
262− ServiceActionStop -> "stop"
327+ ServiceActionStop -> "stop"
263
328
264adminPath :: ByteString
329adminPath :: ByteString
265adminPath = "/minio/admin"
330adminPath = "/minio/admin"
266
331
267data HealStartResp = HealStartResp
332data HealStartResp = HealStartResp
268− { hsrClientToken :: Text
333+ { hsrClientToken :: Text,
269− , hsrClientAddr :: Text
334+ hsrClientAddr :: Text,
270− , hsrStartTime :: UTCTime
335+ hsrStartTime :: UTCTime
336+ }
271− } deriving (Eq, Show)
337+ deriving stock (Show, Eq)
272
338
273instance FromJSON HealStartResp where
339instance FromJSON HealStartResp where
274− parseJSON = withObject "HealStartResp" $ \v -> HealStartResp
340+ parseJSON = withObject "HealStartResp" $ \v ->
341+ HealStartResp
342+ <$> v
275− <$> v .: "clientToken"
343+ .: "clientToken"
344+ <*> v
276− <*> v .: "clientAddress"
345+ .: "clientAddress"
346+ <*> v
277− <*> v .: "startTime"
347+ .: "startTime"
278
348
279data HealOpts = HealOpts
349data HealOpts = HealOpts
280− { hoRecursive :: Bool
350+ { hoRecursive :: Bool,
281− , hoDryRun :: Bool
351+ hoDryRun :: Bool
352+ }
282− } deriving (Eq, Show)
353+ deriving stock (Show, Eq)
283
354
284instance ToJSON HealOpts where
355instance ToJSON HealOpts where
285 toJSON (HealOpts r d) =
356 toJSON (HealOpts r d) =
@@ -288,197 +359,259 @@ instance ToJSON HealOpts where
288 pairs ("recursive" .= r <> "dryRun" .= d)
359 pairs ("recursive" .= r <> "dryRun" .= d)
289
360
290instance FromJSON HealOpts where
361instance FromJSON HealOpts where
291− parseJSON = withObject "HealOpts" $ \v -> HealOpts
362+ parseJSON = withObject "HealOpts" $ \v ->
363+ HealOpts
364+ <$> v
292− <$> v .: "recursive"
365+ .: "recursive"
366+ <*> v
293− <*> v .: "dryRun"
367+ .: "dryRun"
294−
368+
295−data HealItemType = HealItemMetadata
369+data HealItemType
370+ = HealItemMetadata
296− | HealItemBucket
371+ | HealItemBucket
297− | HealItemBucketMetadata
372+ | HealItemBucketMetadata
298− | HealItemObject
373+ | HealItemObject
299− deriving (Eq, Show)
374+ deriving stock (Show, Eq)
300
375
301instance FromJSON HealItemType where
376instance FromJSON HealItemType where
302− parseJSON = withText "HealItemType" $ \v -> case v of
377+ parseJSON = withText "HealItemType" $ \v -> case v of
303− "metadata" -> return HealItemMetadata
378+ "metadata" -> return HealItemMetadata
304− "bucket" -> return HealItemBucket
379+ "bucket" -> return HealItemBucket
305− "object" -> return HealItemObject
380+ "object" -> return HealItemObject
306− "bucket-metadata" -> return HealItemBucketMetadata
381+ "bucket-metadata" -> return HealItemBucketMetadata
307− _ -> typeMismatch "HealItemType" (A.String v)
382+ _ -> typeMismatch "HealItemType" (A.String v)
308
383
309data NodeSummary = NodeSummary
384data NodeSummary = NodeSummary
310− { nsName :: Text
385+ { nsName :: Text,
311− , nsErrSet :: Bool
386+ nsErrSet :: Bool,
312− , nsErrMessage :: Text
387+ nsErrMessage :: Text
388+ }
313− } deriving (Eq, Show)
389+ deriving stock (Show, Eq)
314
390
315instance FromJSON NodeSummary where
391instance FromJSON NodeSummary where
316− parseJSON = withObject "NodeSummary" $ \v -> NodeSummary
392+ parseJSON = withObject "NodeSummary" $ \v ->
393+ NodeSummary
394+ <$> v
317− <$> v .: "name"
395+ .: "name"
396+ <*> v
318− <*> v .: "errSet"
397+ .: "errSet"
398+ <*> v
319− <*> v .: "errMsg"
399+ .: "errMsg"
320
400
321data SetConfigResult = SetConfigResult
401data SetConfigResult = SetConfigResult
322− { scrStatus :: Bool
402+ { scrStatus :: Bool,
323− , scrNodeSummary :: [NodeSummary]
403+ scrNodeSummary :: [NodeSummary]
404+ }
324− } deriving (Eq, Show)
405+ deriving stock (Show, Eq)
325
406
326instance FromJSON SetConfigResult where
407instance FromJSON SetConfigResult where
327− parseJSON = withObject "SetConfigResult" $ \v -> SetConfigResult
408+ parseJSON = withObject "SetConfigResult" $ \v ->
409+ SetConfigResult
410+ <$> v
328− <$> v .: "status"
411+ .: "status"
412+ <*> v
329− <*> v .: "nodeResults"
413+ .: "nodeResults"
330
414
331data HealResultItem = HealResultItem
415data HealResultItem = HealResultItem
332− { hriResultIdx :: Int
416+ { hriResultIdx :: Int,
333− , hriType :: HealItemType
417+ hriType :: HealItemType,
334− , hriBucket :: Bucket
418+ hriBucket :: Bucket,
335− , hriObject :: Object
419+ hriObject :: Object,
336− , hriDetail :: Text
420+ hriDetail :: Text,
337− , hriParityBlocks :: Maybe Int
421+ hriParityBlocks :: Maybe Int,
338− , hriDataBlocks :: Maybe Int
422+ hriDataBlocks :: Maybe Int,
339− , hriDiskCount :: Int
423+ hriDiskCount :: Int,
340− , hriSetCount :: Int
424+ hriSetCount :: Int,
341− , hriObjectSize :: Int
425+ hriObjectSize :: Int,
342− , hriBefore :: [DriveInfo]
426+ hriBefore :: [DriveInfo],
343− , hriAfter :: [DriveInfo]
427+ hriAfter :: [DriveInfo]
428+ }
344− } deriving (Eq, Show)
429+ deriving stock (Show, Eq)
345
430
346instance FromJSON HealResultItem where
431instance FromJSON HealResultItem where
347− parseJSON = withObject "HealResultItem" $ \v -> HealResultItem
432+ parseJSON = withObject "HealResultItem" $ \v ->
433+ HealResultItem
434+ <$> v
348− <$> v .: "resultId"
435+ .: "resultId"
436+ <*> v
349− <*> v .: "type"
437+ .: "type"
438+ <*> v
350− <*> v .: "bucket"
439+ .: "bucket"
440+ <*> v
351− <*> v .: "object"
441+ .: "object"
442+ <*> v
352− <*> v .: "detail"
443+ .: "detail"
444+ <*> v
353− <*> v .:? "parityBlocks"
445+ .:? "parityBlocks"
446+ <*> v
354− <*> v .:? "dataBlocks"
447+ .:? "dataBlocks"
448+ <*> v
355− <*> v .: "diskCount"
449+ .: "diskCount"
450+ <*> v
356− <*> v .: "setCount"
451+ .: "setCount"
452+ <*> v
357− <*> v .: "objectSize"
453+ .: "objectSize"
454+ <*> ( do
358− <*> (do before <- v .: "before"
455+ before <- v .: "before"
359− before .: "drives")
456+ before .: "drives"
457+ )
458+ <*> ( do
360− <*> (do after <- v .: "after"
459+ after <- v .: "after"
361− after .: "drives")
460+ after .: "drives"
461+ )
362
462
363data HealStatus = HealStatus
463data HealStatus = HealStatus
364− { hsSummary :: Text
464+ { hsSummary :: Text,
365− , hsStartTime :: UTCTime
465+ hsStartTime :: UTCTime,
366− , hsSettings :: HealOpts
466+ hsSettings :: HealOpts,
367− , hsNumDisks :: Int
467+ hsNumDisks :: Int,
368− , hsFailureDetail :: Maybe Text
468+ hsFailureDetail :: Maybe Text,
369− , hsItems :: Maybe [HealResultItem]
469+ hsItems :: Maybe [HealResultItem]
470+ }
370− } deriving (Eq, Show)
471+ deriving stock (Show, Eq)
371
472
372instance FromJSON HealStatus where
473instance FromJSON HealStatus where
373− parseJSON = withObject "HealStatus" $ \v -> HealStatus
474+ parseJSON = withObject "HealStatus" $ \v ->
475+ HealStatus
476+ <$> v
374− <$> v .: "Summary"
477+ .: "Summary"
478+ <*> v
375− <*> v .: "StartTime"
479+ .: "StartTime"
480+ <*> v
376− <*> v .: "Settings"
481+ .: "Settings"
482+ <*> v
377− <*> v .: "NumDisks"
483+ .: "NumDisks"
484+ <*> v
378− <*> v .:? "Detail"
485+ .:? "Detail"
486+ <*> v
379− <*> v .: "Items"
487+ .: "Items"
380
488
381healPath :: Maybe Bucket -> Maybe Text -> ByteString
489healPath :: Maybe Bucket -> Maybe Text -> ByteString
382healPath bucket prefix = do
490healPath bucket prefix = do
383− if (isJust bucket)
491+ if isJust bucket
492+ then
493+ encodeUtf8 $
494+ "v1/heal/"
384− then encodeUtf8 $ "v1/heal/" <> fromMaybe "" bucket <> "/"
495+ <> fromMaybe "" bucket
496+ <> "/"
385− <> fromMaybe "" prefix
497+ <> fromMaybe "" prefix
386− else encodeUtf8 $ "v1/heal/"
498+ else encodeUtf8 ("v1/heal/" :: Text)
387
499
388-- | Get server version and uptime.
500-- | Get server version and uptime.
389serviceStatus :: Minio ServiceStatus
501serviceStatus :: Minio ServiceStatus
390serviceStatus = do
502serviceStatus = do
503+ rsp <-
504+ executeAdminRequest
505+ AdminReqInfo
391− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
506+ { ariMethod = HT.methodGet,
392− , ariPayload = PayloadBS B.empty
507+ ariPayload = PayloadBS B.empty,
393− , ariPayloadHash = Nothing
508+ ariPayloadHash = Nothing,
394− , ariPath = "v1/service"
509+ ariPath = "v1/service",
395− , ariHeaders = []
510+ ariHeaders = [],
396− , ariQueryParams = []
511+ ariQueryParams = []
397− }
512+ }
398−
513+
399− let rspBS = NC.responseBody rsp
514+ let rspBS = NC.responseBody rsp
400− case eitherDecode rspBS of
515+ case eitherDecode rspBS of
401− Right ss -> return ss
516+ Right ss -> return ss
402− Left err -> throwIO $ MErrVJsonParse $ T.pack err
517+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
403
518
404-- | Send service restart or stop action to MinIO server.
519-- | Send service restart or stop action to MinIO server.
405serviceSendAction :: ServiceAction -> Minio ()
520serviceSendAction :: ServiceAction -> Minio ()
406serviceSendAction action = do
521serviceSendAction action = do
407− let payload = PayloadBS $ LBS.toStrict $ A.encode action
522+ let payload = PayloadBS $ LBS.toStrict $ A.encode action
523+ void $
524+ executeAdminRequest
525+ AdminReqInfo
408− void $ executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
526+ { ariMethod = HT.methodPost,
409− , ariPayload = payload
527+ ariPayload = payload,
410− , ariPayloadHash = Nothing
528+ ariPayloadHash = Nothing,
411− , ariPath = "v1/service"
529+ ariPath = "v1/service",
412− , ariHeaders = []
530+ ariHeaders = [],
413− , ariQueryParams = []
531+ ariQueryParams = []
414− }
532+ }
415
533
416-- | Get the current config file from server.
534-- | Get the current config file from server.
417getConfig :: Minio ByteString
535getConfig :: Minio ByteString
418getConfig = do
536getConfig = do
537+ rsp <-
538+ executeAdminRequest
539+ AdminReqInfo
419− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
540+ { ariMethod = HT.methodGet,
420− , ariPayload = PayloadBS B.empty
541+ ariPayload = PayloadBS B.empty,
421− , ariPayloadHash = Nothing
542+ ariPayloadHash = Nothing,
422− , ariPath = "v1/config"
543+ ariPath = "v1/config",
423− , ariHeaders = []
544+ ariHeaders = [],
424− , ariQueryParams = []
545+ ariQueryParams = []
425− }
546+ }
426− return $ LBS.toStrict $ NC.responseBody rsp
547+ return $ LBS.toStrict $ NC.responseBody rsp
427
548
428-- | Set a new config to the server.
549-- | Set a new config to the server.
429setConfig :: ByteString -> Minio SetConfigResult
550setConfig :: ByteString -> Minio SetConfigResult
430setConfig config = do
551setConfig config = do
552+ rsp <-
553+ executeAdminRequest
554+ AdminReqInfo
431− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPut
555+ { ariMethod = HT.methodPut,
432− , ariPayload = PayloadBS config
556+ ariPayload = PayloadBS config,
433− , ariPayloadHash = Nothing
557+ ariPayloadHash = Nothing,
434− , ariPath = "v1/config"
558+ ariPath = "v1/config",
435− , ariHeaders = []
559+ ariHeaders = [],
436− , ariQueryParams = []
560+ ariQueryParams = []
437− }
561+ }
438−
562+
439− let rspBS = NC.responseBody rsp
563+ let rspBS = NC.responseBody rsp
440− case eitherDecode rspBS of
564+ case eitherDecode rspBS of
441− Right scr -> return scr
565+ Right scr -> return scr
442− Left err -> throwIO $ MErrVJsonParse $ T.pack err
566+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
443
567
444-- | Get the progress of currently running heal task, this API should be
568-- | Get the progress of currently running heal task, this API should be
445-- invoked right after `startHeal`. `token` is obtained after `startHeal`
569-- invoked right after `startHeal`. `token` is obtained after `startHeal`
446-- which should be used to get the heal status.
570-- which should be used to get the heal status.
447getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
571getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
448getHealStatus bucket prefix token = do
572getHealStatus bucket prefix token = do
449− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
573+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
450− let qparams = HT.queryTextToQuery [("clientToken", Just token)]
574+ let qparams = HT.queryTextToQuery [("clientToken", Just token)]
575+ rsp <-
576+ executeAdminRequest
577+ AdminReqInfo
451− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
578+ { ariMethod = HT.methodPost,
452− , ariPayload = PayloadBS B.empty
579+ ariPayload = PayloadBS B.empty,
453− , ariPayloadHash = Nothing
580+ ariPayloadHash = Nothing,
454− , ariPath = healPath bucket prefix
581+ ariPath = healPath bucket prefix,
455− , ariHeaders = []
582+ ariHeaders = [],
456− , ariQueryParams = qparams
583+ ariQueryParams = qparams
457− }
584+ }
458− let rspBS = NC.responseBody rsp
585+ let rspBS = NC.responseBody rsp
459− case eitherDecode rspBS of
586+ case eitherDecode rspBS of
460− Right hs -> return hs
587+ Right hs -> return hs
461− Left err -> throwIO $ MErrVJsonParse $ T.pack err
588+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
462
589
463doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
590doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
464doHeal bucket prefix opts forceStart = do
591doHeal bucket prefix opts forceStart = do
465− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
592+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
466− let payload = PayloadBS $ LBS.toStrict $ A.encode opts
593+ let payload = PayloadBS $ LBS.toStrict $ A.encode opts
594+ let qparams =
595+ bool
596+ []
467− let qparams = bool [] (HT.queryTextToQuery [("forceStart", Just "true")])
597+ (HT.queryTextToQuery [("forceStart", Just "true")])
468− forceStart
598+ forceStart
469−
599+
600+ rsp <-
601+ executeAdminRequest
602+ AdminReqInfo
470− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
603+ { ariMethod = HT.methodPost,
471− , ariPayload = payload
604+ ariPayload = payload,
472− , ariPayloadHash = Nothing
605+ ariPayloadHash = Nothing,
473− , ariPath = healPath bucket prefix
606+ ariPath = healPath bucket prefix,
474− , ariHeaders = []
607+ ariHeaders = [],
475− , ariQueryParams = qparams
608+ ariQueryParams = qparams
476− }
609+ }
477−
610+
478− let rspBS = NC.responseBody rsp
611+ let rspBS = NC.responseBody rsp
479− case eitherDecode rspBS of
612+ case eitherDecode rspBS of
480− Right hsr -> return hsr
613+ Right hsr -> return hsr
481− Left err -> throwIO $ MErrVJsonParse $ T.pack err
614+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
482
615
483-- | Start a heal sequence that scans data under given (possible empty)
616-- | Start a heal sequence that scans data under given (possible empty)
484-- `bucket` and `prefix`. The `recursive` bool turns on recursive
617-- `bucket` and `prefix`. The `recursive` bool turns on recursive
@@ -500,60 +633,79 @@ forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
500-- properties, storage information, network statistics, etc.
633-- properties, storage information, network statistics, etc.
501getServerInfo :: Minio [ServerInfo]
634getServerInfo :: Minio [ServerInfo]
502getServerInfo = do
635getServerInfo = do
636+ rsp <-
637+ executeAdminRequest
638+ AdminReqInfo
503− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
639+ { ariMethod = HT.methodGet,
504− , ariPayload = PayloadBS B.empty
640+ ariPayload = PayloadBS B.empty,
505− , ariPayloadHash = Nothing
641+ ariPayloadHash = Nothing,
506− , ariPath = "v1/info"
642+ ariPath = "v1/info",
507− , ariHeaders = []
643+ ariHeaders = [],
508− , ariQueryParams = []
644+ ariQueryParams = []
509− }
645+ }
510− let rspBS = NC.responseBody rsp
646+ let rspBS = NC.responseBody rsp
511− case eitherDecode rspBS of
647+ case eitherDecode rspBS of
512− Right si -> return si
648+ Right si -> return si
513− Left err -> throwIO $ MErrVJsonParse $ T.pack err
649+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
514
650
515executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
651executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
516executeAdminRequest ari = do
652executeAdminRequest ari = do
517− req <- buildAdminRequest ari
653+ req <- buildAdminRequest ari
518− mgr <- asks mcConnManager
654+ mgr <- asks mcConnManager
519− httpLbs req mgr
655+ httpLbs req mgr
520
656
521buildAdminRequest :: AdminReqInfo -> Minio NC.Request
657buildAdminRequest :: AdminReqInfo -> Minio NC.Request
522buildAdminRequest areq = do
658buildAdminRequest areq = do
523− ci <- asks mcConnInfo
659+ ci <- asks mcConnInfo
660+ sha256Hash <-
524− sha256Hash <- if | connectIsSecure ci ->
661+ if connectIsSecure ci
525− -- if secure connection
662+ then -- if secure connection
526− return "UNSIGNED-PAYLOAD"
663+ return "UNSIGNED-PAYLOAD"
527−
528− -- otherwise compute sha256
664+ else -- otherwise compute sha256
529− | otherwise -> getPayloadSHA256Hash (ariPayload areq)
665+ getPayloadSHA256Hash (ariPayload areq)
530−
666+
531− timeStamp <- liftIO getCurrentTime
667+ timeStamp <- liftIO getCurrentTime
532−
668+
669+ mgr <- asks mcConnManager
670+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671+
533− let hostHeader = (hHost, getHostAddr ci)
672+ let hostHeader = (hHost, getHostAddr ci)
673+ newAreq =
674+ areq
534− newAreq = areq { ariPayloadHash = Just sha256Hash
675+ { ariPayloadHash = Just sha256Hash,
535− , ariHeaders = hostHeader
676+ ariHeaders =
677+ hostHeader
536− : sha256Header sha256Hash
678+ : sha256Header sha256Hash
537− : ariHeaders areq
679+ : ariHeaders areq
538− }
680+ }
539− signReq = toRequest ci newAreq
681+ signReq = toRequest ci newAreq
540− sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
682+ sp =
683+ SignParams
684+ (coerce $ cvAccessKey cv)
685+ (coerce $ cvSecretKey cv)
686+ (coerce $ cvSessionToken cv)
687+ ServiceS3
688+ timeStamp
689+ Nothing
690+ Nothing
541− timeStamp Nothing Nothing (ariPayloadHash newAreq)
691+ (ariPayloadHash newAreq)
542− signHeaders = signV4 sp signReq
692+ signHeaders = signV4 sp signReq
543−
693+
544− -- Update signReq with Authorization header containing v4 signature
694+ -- Update signReq with Authorization header containing v4 signature
695+ return
545− return signReq {
696+ signReq
546− NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
697+ { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
547− }
698+ }
548 where
699 where
549 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
700 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
701+ toRequest ci aReq =
550− toRequest ci aReq = NC.defaultRequest
702+ NC.defaultRequest
551− { NC.method = ariMethod aReq
703+ { NC.method = ariMethod aReq,
552− , NC.secure = connectIsSecure ci
704+ NC.secure = connectIsSecure ci,
553− , NC.host = encodeUtf8 $ connectHost ci
705+ NC.host = encodeUtf8 $ connectHost ci,
554− , NC.port = connectPort ci
706+ NC.port = connectPort ci,
555− , NC.path = B.intercalate "/" [adminPath, ariPath aReq]
707+ NC.path = B.intercalate "/" [adminPath, ariPath aReq],
556− , NC.requestHeaders = ariHeaders aReq
708+ NC.requestHeaders = ariHeaders aReq,
557− , NC.queryString = HT.renderQuery False $ ariQueryParams aReq
709+ NC.queryString = HT.renderQuery False $ ariQueryParams aReq,
558− , NC.requestBody = getRequestBody (ariPayload aReq)
710+ NC.requestBody = getRequestBody (ariPayload aReq)
559 }
711 }
src/Network/Minio/CopyObject.hs+48−34
@@ -16,19 +16,19 @@
16
16
17module Network.Minio.CopyObject where
17module Network.Minio.CopyObject where
18
18
19−import qualified Data.List as List
19+import qualified Data.List as List
20−
21−import Lib.Prelude
20+import Lib.Prelude
22−
23−import Network.Minio.Data
21+import Network.Minio.Data
24−import Network.Minio.Errors
22+import Network.Minio.Errors
25−import Network.Minio.S3API
23+import Network.Minio.S3API
26−import Network.Minio.Utils
24+import Network.Minio.Utils
27−
28
25
29-- | Copy an object using single or multipart copy strategy.
26-- | Copy an object using single or multipart copy strategy.
30−copyObjectInternal :: Bucket -> Object -> SourceInfo
27+copyObjectInternal ::
28+ Bucket ->
29+ Object ->
30+ SourceInfo ->
31− -> Minio ETag
31+ Minio ETag
32copyObjectInternal b' o srcInfo = do
32copyObjectInternal b' o srcInfo = do
33 let sBucket = srcBucket srcInfo
33 let sBucket = srcBucket srcInfo
34 sObject = srcObject srcInfo
34 sObject = srcObject srcInfo
@@ -43,27 +43,33 @@ copyObjectInternal b' o srcInfo = do
43 startOffset = fst range
43 startOffset = fst range
44 endOffset = snd range
44 endOffset = snd range
45
45
46+ when
46− when (isJust rangeMay &&
47+ ( isJust rangeMay
48+ && ( (startOffset < 0)
47− or [startOffset < 0, endOffset < startOffset,
49+ || (endOffset < startOffset)
48− endOffset >= fromIntegral srcSize]) $
50+ || (endOffset >= srcSize)
51+ )
52+ )
53+ $ throwIO
49− throwIO $ MErrVInvalidSrcObjByteRange range
54+ $ MErrVInvalidSrcObjByteRange range
50
55
51 -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
56 -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
52 -- 2. If startOffset /= 0 use multipart copy
57 -- 2. If startOffset /= 0 use multipart copy
53− let destSize = (\(a, b) -> b - a + 1 ) $
58+ let destSize =
59+ (\(a, b) -> b - a + 1) $
54− maybe (0, srcSize - 1) identity rangeMay
60+ maybe (0, srcSize - 1) identity rangeMay
55
61
56 if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
62 if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
57 then multiPartCopyObject b' o srcInfo srcSize
63 then multiPartCopyObject b' o srcInfo srcSize
58−
59− else fst <$> copyObjectSingle b' o srcInfo{srcRange = Nothing} []
64+ else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} []
60
65
61-- | Given the input byte range of the source object, compute the
66-- | Given the input byte range of the source object, compute the
62-- splits for a multipart copy object procedure. Minimum part size
67-- splits for a multipart copy object procedure. Minimum part size
63-- used is minPartSize.
68-- used is minPartSize.
64selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
69selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
65−selectCopyRanges (st, end) = zip pns $
70+selectCopyRanges (st, end) =
71+ zip pns $
66− map (\(x, y) -> (st + x, st + x + y - 1)) $ zip startOffsets partSizes
72+ zipWith (\x y -> (st + x, st + x + y - 1)) startOffsets partSizes
67 where
73 where
68 size = end - st + 1
74 size = end - st + 1
69 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
75 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
@@ -71,22 +77,30 @@ selectCopyRanges (st, end) = zip pns $
71-- | Perform a multipart copy object action. Since we cannot verify
77-- | Perform a multipart copy object action. Since we cannot verify
72-- existing parts based on the source object, there is no resuming
78-- existing parts based on the source object, there is no resuming
73-- copy action support.
79-- copy action support.
74−multiPartCopyObject :: Bucket -> Object -> SourceInfo -> Int64
80+multiPartCopyObject ::
81+ Bucket ->
82+ Object ->
83+ SourceInfo ->
84+ Int64 ->
75− -> Minio ETag
85+ Minio ETag
76multiPartCopyObject b o cps srcSize = do
86multiPartCopyObject b o cps srcSize = do
77 uid <- newMultipartUpload b o []
87 uid <- newMultipartUpload b o []
78
88
79− let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $ srcRange cps
89+ let byteRange = maybe (0, srcSize - 1) identity $ srcRange cps
80 partRanges = selectCopyRanges byteRange
90 partRanges = selectCopyRanges byteRange
91+ partSources =
92+ map
81− partSources = map (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end) }))
93+ (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end)}))
82− partRanges
94+ partRanges
83− dstInfo = defaultDestinationInfo { dstBucket = b, dstObject = o}
95+ dstInfo = defaultDestinationInfo {dstBucket = b, dstObject = o}
84−
96+
97+ copiedParts <-
85− copiedParts <- limitedMapConcurrently 10
98+ limitedMapConcurrently
99+ 10
86− (\(pn, cps') -> do
100+ ( \(pn, cps') -> do
87− (etag, _) <- copyObjectPart dstInfo cps' uid pn []
101+ (etag, _) <- copyObjectPart dstInfo cps' uid pn []
88− return (pn, etag)
102+ return (pn, etag)
89− )
103+ )
90− partSources
104+ partSources
91
105
92 completeMultipartUpload b o uid copiedParts
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+697−559
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,45 +13,64 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
17−{-# LANGUAGE CPP #-}
16+{-# LANGUAGE CPP #-}
18−{-# LANGUAGE GADTs #-}
17+{-# LANGUAGE GADTs #-}
19{-# LANGUAGE GeneralizedNewtypeDeriving #-}
18{-# LANGUAGE GeneralizedNewtypeDeriving #-}
19+{-# LANGUAGE StrictData #-}
20−{-# LANGUAGE TypeFamilies #-}
20+{-# LANGUAGE TypeFamilies #-}
21+
21module Network.Minio.Data where
22module Network.Minio.Data where
22
23
23−import qualified Conduit as C
24+import qualified Conduit as C
24−import qualified Control.Concurrent.MVar as M
25+import qualified Control.Concurrent.MVar as M
25−import Control.Monad.IO.Unlift (UnliftIO (..), askUnliftIO,
26+import Control.Monad.Trans.Except (throwE)
26− withUnliftIO)
27−import Control.Monad.Trans.Resource
27+import Control.Monad.Trans.Resource
28+ ( MonadResource,
29+ MonadThrow (..),
30+ MonadUnliftIO,
31+ ResourceT,
32+ runResourceT,
33+ )
28−import qualified Data.Aeson as A
34+import qualified Data.Aeson as A
29−import qualified Data.ByteArray as BA
35+import qualified Data.ByteArray as BA
30−import qualified Data.ByteString as B
36+import qualified Data.ByteString as B
31−import Data.CaseInsensitive (mk)
37+import qualified Data.ByteString.Lazy as LB
38+import Data.Default.Class (def)
32−import qualified Data.HashMap.Strict as H
39+import qualified Data.HashMap.Strict as H
33−import qualified Data.Ini as Ini
40+import qualified Data.Ini as Ini
34−import Data.String (IsString (..))
41+import qualified Data.List as List
35−import qualified Data.Text as T
42+import qualified Data.Text as T
36−import qualified Data.Text.Encoding as TE
43+import qualified Data.Text.Encoding as TE
37−import Data.Time (defaultTimeLocale, formatTime)
44+import Data.Time (defaultTimeLocale, formatTime)
38−import GHC.Show (Show (show))
45+import Lib.Prelude (UTCTime, throwIO)
39−import qualified Network.Connection as Conn
46+import qualified Network.Connection as Conn
40−import Network.HTTP.Client (defaultManagerSettings)
47+import Network.HTTP.Client (defaultManagerSettings)
41−import qualified Network.HTTP.Client.TLS as TLS
48+import qualified Network.HTTP.Client.TLS as TLS
42−import qualified Network.HTTP.Conduit as NC
49+import qualified Network.HTTP.Conduit as NC
43−import Network.HTTP.Types (ByteRange, Header, Method, Query,
50+import Network.HTTP.Types
51+ ( ByteRange,
52+ Header,
53+ Method,
54+ Query,
44− hRange)
55+ hRange,
56+ )
45−import qualified Network.HTTP.Types as HT
57+import qualified Network.HTTP.Types as HT
58+import Network.Minio.Credentials
59+import Network.Minio.Data.Crypto
60+ ( encodeToBase64,
61+ hashMD5ToBase64,
62+ )
63+import Network.Minio.Data.Time (UrlExpiry)
46−import Network.Minio.Errors
64+import Network.Minio.Errors
65+ ( MErrV (MErrVInvalidEncryptionKeyLength, MErrVMissingCredentials),
66+ MinioErr (..),
67+ )
68+import Network.Minio.Utils
47−import System.Directory (doesFileExist, getHomeDirectory)
69+import System.Directory (doesFileExist, getHomeDirectory)
48−import qualified System.Environment as Env
70+import qualified System.Environment as Env
49−import System.FilePath.Posix (combine)
71+import System.FilePath.Posix (combine)
50−import Text.XML
51−import qualified UnliftIO as U
72+import qualified UnliftIO as U
52−
73+import qualified UnliftIO.MVar as UM
53−import Lib.Prelude
54−import Network.Minio.Data.Crypto
55
74
56-- | max obj size is 5TiB
75-- | max obj size is 5TiB
57maxObjectSize :: Int64
76maxObjectSize :: Int64
@@ -71,125 +90,157 @@ maxMultipartParts = 10000
71-- TODO: Add a type which provides typed constants for region. this
90-- TODO: Add a type which provides typed constants for region. this
72-- type should have a IsString instance to infer the appropriate
91-- type should have a IsString instance to infer the appropriate
73-- constant.
92-- constant.
93+
74-- | awsRegionMap - library constant
94-- | awsRegionMap - library constant
75awsRegionMap :: H.HashMap Text Text
95awsRegionMap :: H.HashMap Text Text
76−awsRegionMap = H.fromList [
96+awsRegionMap =
97+ H.fromList
77− ("us-east-1", "s3.amazonaws.com")
98+ [ ("us-east-1", "s3.us-east-1.amazonaws.com"),
78− , ("us-east-2", "s3-us-east-2.amazonaws.com")
99+ ("us-east-2", "s3.us-east-2.amazonaws.com"),
79− , ("us-west-1", "s3-us-west-1.amazonaws.com")
100+ ("us-west-1", "s3.us-west-1.amazonaws.com"),
80− , ("us-west-2", "s3-us-west-2.amazonaws.com")
101+ ("us-west-2", "s3.us-west-2.amazonaws.com"),
81− , ("ca-central-1", "s3-ca-central-1.amazonaws.com")
102+ ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
82− , ("ap-south-1", "s3-ap-south-1.amazonaws.com")
103+ ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
104+ ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
83− , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
105+ ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
84− , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
106+ ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
107+ ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
85− , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
108+ ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
86− , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
109+ ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
110+ ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
87− , ("eu-west-1", "s3-eu-west-1.amazonaws.com")
111+ ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
88− , ("eu-west-2", "s3-eu-west-2.amazonaws.com")
112+ ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
113+ ("eu-west-3", "s3.eu-west-3.amazonaws.com"),
89− , ("eu-central-1", "s3-eu-central-1.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"),
90− , ("sa-east-1", "s3-sa-east-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")
91− ]
128+ ]
92
129
93-- | Connection Info data type. To create a 'ConnectInfo' value,
130-- | Connection Info data type. To create a 'ConnectInfo' value,
94-- enable the @OverloadedStrings@ language extension and use the
131-- enable the @OverloadedStrings@ language extension and use the
95-- `IsString` instance to provide a URL, for example:
132-- `IsString` instance to provide a URL, for example:
96--
133--
97-- > let c :: ConnectInfo = "https://play.min.io"
134-- > let c :: ConnectInfo = "https://play.min.io"
98−data ConnectInfo =
135+data ConnectInfo = ConnectInfo
99− ConnectInfo { connectHost :: Text
136+ { connectHost :: Text,
100− , connectPort :: Int
137+ connectPort :: Int,
101− , connectAccessKey :: Text
138+ connectCreds :: Creds,
102− , connectSecretKey :: Text
139+ connectIsSecure :: Bool,
103− , connectIsSecure :: Bool
104− , connectRegion :: Region
140+ connectRegion :: Region,
105− , connectAutoDiscoverRegion :: Bool
141+ connectAutoDiscoverRegion :: Bool,
106− , connectDisableTLSCertValidation :: Bool
142+ connectDisableTLSCertValidation :: Bool
107− } deriving (Eq, Show)
143+ }
144+
145+getEndpoint :: ConnectInfo -> Endpoint
146+getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
108
147
109instance IsString ConnectInfo where
148instance IsString ConnectInfo where
110− fromString str =
149+ fromString str =
111− let req = NC.parseRequest_ str
150+ let req = NC.parseRequest_ str
112− in ConnectInfo
151+ in ConnectInfo
113− { connectHost = TE.decodeUtf8 $ NC.host req
152+ { connectHost = TE.decodeUtf8 $ NC.host req,
114− , connectPort = NC.port req
153+ connectPort = NC.port req,
115− , connectAccessKey = ""
154+ connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
116− , connectSecretKey = ""
117− , connectIsSecure = NC.secure req
155+ connectIsSecure = NC.secure req,
118− , connectRegion = ""
156+ connectRegion = "",
119− , connectAutoDiscoverRegion = True
157+ connectAutoDiscoverRegion = True,
120− , connectDisableTLSCertValidation = False
158+ connectDisableTLSCertValidation = False
121− }
159+ }
122−
160+
123−-- | Contains access key and secret key to access object storage.
124−data Credentials = Credentials { cAccessKey :: Text
125− , cSecretKey :: Text
126− } deriving (Eq, Show)
127−
128−-- | A Provider is an action that may return Credentials. Providers
161+-- | A 'CredentialLoader' is an action that may return a 'CredentialValue'.
129−-- may be chained together using 'findFirst'.
162+-- Loaders may be chained together using 'findFirst'.
130−type Provider = IO (Maybe Credentials)
163+--
131−
164+-- @since 1.7.0
165+type CredentialLoader = IO (Maybe CredentialValue)
166+
132−-- | Combines the given list of providers, by calling each one in
167+-- | Combines the given list of loaders, by calling each one in
133−-- order until Credentials are found.
168+-- order until a 'CredentialValue' is returned.
134−findFirst :: [Provider] -> Provider
169+findFirst :: [CredentialLoader] -> IO (Maybe CredentialValue)
135findFirst [] = return Nothing
170findFirst [] = return Nothing
136−findFirst (f:fs) = do c <- f
171+findFirst (f : fs) = do
172+ c <- f
137− maybe (findFirst fs) (return . Just) c
173+ maybe (findFirst fs) (return . Just) c
138
174
175+-- | This action returns a 'CredentialValue' populated from
139−-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
176+-- @~\/.aws\/credentials@
140−fromAWSConfigFile :: Provider
177+fromAWSConfigFile :: CredentialLoader
141fromAWSConfigFile = do
178fromAWSConfigFile = do
142− credsE <- runExceptT $ do
179+ credsE <- runExceptT $ do
143− homeDir <- lift $ getHomeDirectory
180+ homeDir <- lift getHomeDirectory
144− let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
181+ let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
145− fileExists <- lift $ doesFileExist awsCredsFile
182+ fileExists <- lift $ doesFileExist awsCredsFile
146− bool (throwE "FileNotFound") (return ()) fileExists
183+ bool (throwE "FileNotFound") (return ()) fileExists
147− ini <- ExceptT $ Ini.readIniFile awsCredsFile
184+ ini <- ExceptT $ Ini.readIniFile awsCredsFile
185+ akey <-
148− akey <- ExceptT $ return
186+ ExceptT $
187+ return $
149− $ Ini.lookupValue "default" "aws_access_key_id" ini
188+ Ini.lookupValue "default" "aws_access_key_id" ini
189+ skey <-
150− skey <- ExceptT $ return
190+ ExceptT $
191+ return $
151− $ Ini.lookupValue "default" "aws_secret_access_key" ini
192+ Ini.lookupValue "default" "aws_secret_access_key" ini
152− return $ Credentials akey skey
193+ return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
153− return $ hush credsE
194+ return $ either (const Nothing) Just credsE
154−
195+
155−-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
196+-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
156−-- @AWS_SECRET_ACCESS_KEY@ environment variables.
197+-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
157−fromAWSEnv :: Provider
198+fromAWSEnv :: CredentialLoader
158fromAWSEnv = runMaybeT $ do
199fromAWSEnv = runMaybeT $ do
159− akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
200+ akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
160− skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
201+ skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
161− return $ Credentials (T.pack akey) (T.pack skey)
202+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
162
203
163−-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
204+-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
164−-- @MINIO_SECRET_KEY@ environment variables.
205+-- and @MINIO_SECRET_KEY@ environment variables.
165−fromMinioEnv :: Provider
206+fromMinioEnv :: CredentialLoader
166fromMinioEnv = runMaybeT $ do
207fromMinioEnv = runMaybeT $ do
167− akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
208+ akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
168− skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
209+ skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
169− return $ Credentials (T.pack akey) (T.pack skey)
210+ return $ CredentialValue (fromString akey) (fromString skey) Nothing
170−
211+
171−-- | setCredsFrom retrieves access credentials from the first
212+-- | setCredsFrom retrieves access credentials from the first action in the
172−-- `Provider` form the given list that succeeds and sets it in the
213+-- given list that succeeds and sets it in the 'ConnectInfo'.
173−-- `ConnectInfo`.
174−setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo
214+setCredsFrom :: [CredentialLoader] -> ConnectInfo -> IO ConnectInfo
175−setCredsFrom ps ci = do pMay <- findFirst ps
215+setCredsFrom ps ci = do
216+ pMay <- findFirst ps
176− maybe
217+ maybe
177− (throwIO MErrVMissingCredentials)
218+ (throwIO MErrVMissingCredentials)
178− (return . (flip setCreds ci))
219+ (return . (`setCreds` ci))
179− pMay
220+ pMay
180−
221+
181−-- | setCreds sets the given `Credentials` in the `ConnectInfo`.
222+-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
182−setCreds :: Credentials -> ConnectInfo -> ConnectInfo
223+setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
183−setCreds (Credentials accessKey secretKey) connInfo =
224+setCreds cv connInfo =
184− connInfo { connectAccessKey = accessKey
225+ connInfo
185− , connectSecretKey = secretKey
226+ { connectCreds = CredsStatic cv
186− }
227+ }
228+
229+-- | 'setSTSCredential' configures `ConnectInfo` to retrieve temporary
230+-- credentials via the STS API on demand. It is automatically refreshed on
231+-- expiry.
232+setSTSCredential :: (STSCredentialProvider p) => p -> ConnectInfo -> IO ConnectInfo
233+setSTSCredential p ci = do
234+ store <- initSTSCredential p
235+ return ci {connectCreds = CredsSTS store}
187
236
188-- | Set the S3 region parameter in the `ConnectInfo`
237-- | Set the S3 region parameter in the `ConnectInfo`
189setRegion :: Region -> ConnectInfo -> ConnectInfo
238setRegion :: Region -> ConnectInfo -> ConnectInfo
190−setRegion r connInfo = connInfo { connectRegion = r
239+setRegion r connInfo =
240+ connInfo
241+ { connectRegion = r,
191− , connectAutoDiscoverRegion = False
242+ connectAutoDiscoverRegion = False
192− }
243+ }
193
244
194-- | Check if the connection to object storage server is secure
245-- | Check if the connection to object storage server is secure
195-- (i.e. uses TLS)
246-- (i.e. uses TLS)
@@ -201,16 +252,10 @@ isConnectInfoSecure = connectIsSecure
201-- certificates. Note that this option has no effect, if you provide
252-- certificates. Note that this option has no effect, if you provide
202-- your own Manager in `mkMinioConn`.
253-- your own Manager in `mkMinioConn`.
203disableTLSCertValidation :: ConnectInfo -> ConnectInfo
254disableTLSCertValidation :: ConnectInfo -> ConnectInfo
204−disableTLSCertValidation c = c { connectDisableTLSCertValidation = True }
255+disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
205
256
206getHostAddr :: ConnectInfo -> ByteString
257getHostAddr :: ConnectInfo -> ByteString
207−getHostAddr ci = if | port == 80 || port == 443 -> toS host
258+getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
208− | otherwise -> toS $
209− T.concat [ host, ":" , Lib.Prelude.show port]
210− where
211− port = connectPort ci
212− host = connectHost ci
213−
214
259
215-- | Default Google Compute Storage ConnectInfo. Works only for
260-- | Default Google Compute Storage ConnectInfo. Works only for
216-- "Simple Migration" use-case with interoperability mode enabled on
261-- "Simple Migration" use-case with interoperability mode enabled on
@@ -219,23 +264,25 @@ getHostAddr ci = if | port == 80 || port == 443 -> toS host
219--
264--
220-- Credentials should be supplied before use.
265-- Credentials should be supplied before use.
221gcsCI :: ConnectInfo
266gcsCI :: ConnectInfo
267+gcsCI =
222−gcsCI = setRegion "us"
268+ setRegion
269+ "us"
223− "https://storage.googleapis.com"
270+ "https://storage.googleapis.com"
224−
225
271
226-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
272-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
227-- should be supplied before use.
273-- should be supplied before use.
228awsCI :: ConnectInfo
274awsCI :: ConnectInfo
229awsCI = "https://s3.amazonaws.com"
275awsCI = "https://s3.amazonaws.com"
230
276
231−
232-- | <https://play.min.io MinIO Play Server>
277-- | <https://play.min.io MinIO Play Server>
233-- ConnectInfo. Credentials are already filled in.
278-- ConnectInfo. Credentials are already filled in.
234minioPlayCI :: ConnectInfo
279minioPlayCI :: ConnectInfo
280+minioPlayCI =
235−minioPlayCI = let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
281+ let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
236− in setCreds playCreds
282+ in setCreds playCreds $
237− $ setRegion "us-east-1"
283+ setRegion
284+ "us-east-1"
238− "https://play.min.io"
285+ "https://play.min.io"
239
286
240-- |
287-- |
241-- Represents a bucket in the object store
288-- Represents a bucket in the object store
@@ -254,135 +301,147 @@ type ETag = Text
254-- | Data type to represent an object encryption key. Create one using
301-- | Data type to represent an object encryption key. Create one using
255-- the `mkSSECKey` function.
302-- the `mkSSECKey` function.
256newtype SSECKey = SSECKey BA.ScrubbedBytes
303newtype SSECKey = SSECKey BA.ScrubbedBytes
257− deriving (Eq, Show)
304+ deriving stock (Eq, Show)
258
305
259-- | Validates that the given ByteString is 32 bytes long and creates
306-- | Validates that the given ByteString is 32 bytes long and creates
260-- an encryption key.
307-- an encryption key.
261−mkSSECKey :: MonadThrow m => ByteString -> m SSECKey
308+mkSSECKey :: (MonadThrow m) => ByteString -> m SSECKey
262−mkSSECKey keyBytes | B.length keyBytes /= 32 =
309+mkSSECKey keyBytes
310+ | B.length keyBytes /= 32 =
263− throwM MErrVInvalidEncryptionKeyLength
311+ throwM MErrVInvalidEncryptionKeyLength
264− | otherwise =
312+ | otherwise =
265− return $ SSECKey $ BA.convert keyBytes
313+ return $ SSECKey $ BA.convert keyBytes
266
314
267-- | Data type to represent Server-Side-Encryption settings
315-- | Data type to represent Server-Side-Encryption settings
268data SSE where
316data SSE where
269− -- | Specifies SSE S3 encryption - server manages encryption keys
317+ -- | Specifies SSE S3 encryption - server manages encryption keys
270− SSE :: SSE
318+ SSE :: SSE
271− -- | Specifies that KMS service should be used. The first argument
319+ -- | Specifies that KMS service should be used. The first argument
272− -- to the constructor is the Key Id to be used by the server (if
320+ -- to the constructor is the Key Id to be used by the server (if
273− -- not specified, the default KMS key id is used). The second
321+ -- not specified, the default KMS key id is used). The second
274− -- argument is the optional KMS context that must have a
322+ -- argument is the optional KMS context that must have a
275− -- `A.ToJSON` instance - please refer to the AWS S3 documentation
323+ -- `A.ToJSON` instance - please refer to the AWS S3 documentation
276− -- for detailed information.
324+ -- for detailed information.
277− SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
325+ SSEKMS :: (A.ToJSON a) => Maybe ByteString -> Maybe a -> SSE
278− -- | Specifies server-side encryption with customer provided
326+ -- | Specifies server-side encryption with customer provided
279− -- key. The argument is the encryption key to be used.
327+ -- key. The argument is the encryption key to be used.
280− SSEC :: SSECKey -> SSE
328+ SSEC :: SSECKey -> SSE
281
329
282toPutObjectHeaders :: SSE -> [HT.Header]
330toPutObjectHeaders :: SSE -> [HT.Header]
283toPutObjectHeaders sseArg =
331toPutObjectHeaders sseArg =
284− let sseHeader = "x-amz-server-side-encryption"
332+ let sseHeader = "x-amz-server-side-encryption"
285− sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
333+ sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
286− sseKmsContextHeader = sseHeader <> "-context"
334+ sseKmsContextHeader = sseHeader <> "-context"
287− ssecAlgo = sseHeader <> "-customer-algorithm"
335+ ssecAlgo = sseHeader <> "-customer-algorithm"
288− ssecKey = sseHeader <> "-customer-key"
336+ ssecKey = sseHeader <> "-customer-key"
289− ssecKeyMD5 = ssecKey <> "-MD5"
337+ ssecKeyMD5 = ssecKey <> "-MD5"
290−
291− in case sseArg of
338+ in case sseArg of
292− SSE -> [(sseHeader, "AES256")]
339+ SSE -> [(sseHeader, "AES256")]
293−
294− SSEKMS keyIdMay ctxMay ->
340+ SSEKMS keyIdMay ctxMay ->
295− [(sseHeader, "aws:kms")] ++
341+ [(sseHeader, "aws:kms")]
296− maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay ++
342+ ++ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay
297− maybe [] (\k -> [(sseKmsContextHeader, toS $ A.encode k)]) ctxMay
343+ ++ maybe [] (\k -> [(sseKmsContextHeader, LB.toStrict $ A.encode k)]) ctxMay
298−
299− SSEC (SSECKey sb) ->
344+ SSEC (SSECKey sb) ->
300− [(ssecAlgo, "AES256"),
345+ [ (ssecAlgo, "AES256"),
301− (ssecKey, encodeToBase64 sb),
346+ (ssecKey, encodeToBase64 sb),
302− (ssecKeyMD5, hashMD5ToBase64 sb)]
347+ (ssecKeyMD5, hashMD5ToBase64 sb)
348+ ]
303
349
304-- | Data type for options in PutObject call. Start with the empty
350-- | Data type for options in PutObject call. Start with the empty
305-- `defaultPutObjectOptions` and use various the various poo*
351-- `defaultPutObjectOptions` and use various the various poo*
306-- accessors.
352-- accessors.
307−data PutObjectOptions = PutObjectOptions {
353+data PutObjectOptions = PutObjectOptions
308− -- | Set a standard MIME type describing the format of the object.
354+ { -- | Set a standard MIME type describing the format of the object.
309− pooContentType :: Maybe Text
355+ pooContentType :: Maybe Text,
310− -- | Set what content encodings have been applied to the object and thus
356+ -- | Set what content encodings have been applied to the object and thus
311− -- what decoding mechanisms must be applied to obtain the media-type
357+ -- what decoding mechanisms must be applied to obtain the media-type
312− -- referenced by the Content-Type header field.
358+ -- referenced by the Content-Type header field.
313− , pooContentEncoding :: Maybe Text
359+ pooContentEncoding :: Maybe Text,
314− -- | Set presentational information for the object.
360+ -- | Set presentational information for the object.
315− , pooContentDisposition :: Maybe Text
361+ pooContentDisposition :: Maybe Text,
316− -- | Set to specify caching behavior for the object along the
362+ -- | Set to specify caching behavior for the object along the
317− -- request/reply chain.
363+ -- request/reply chain.
318− , pooCacheControl :: Maybe Text
364+ pooCacheControl :: Maybe Text,
319− -- | Set to describe the language(s) intended for the audience.
365+ -- | Set to describe the language(s) intended for the audience.
320− , pooContentLanguage :: Maybe Text
366+ pooContentLanguage :: Maybe Text,
321− -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
367+ -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
322− -- performance needs, storage class is @STANDARD@ by default (i.e
368+ -- performance needs, storage class is @STANDARD@ by default (i.e
323− -- when Nothing is passed).
369+ -- when Nothing is passed).
324− , pooStorageClass :: Maybe Text
370+ pooStorageClass :: Maybe Text,
325− -- | Set user defined metadata to store with the object.
371+ -- | Set user defined metadata to store with the object.
326− , pooUserMetadata :: [(Text, Text)]
372+ pooUserMetadata :: [(Text, Text)],
327− -- | Set number of worker threads used to upload an object.
373+ -- | Set number of worker threads used to upload an object.
328− , pooNumThreads :: Maybe Word
374+ pooNumThreads :: Maybe Word,
329− -- | Set object encryption parameters for the request.
375+ -- | Set object encryption parameters for the request.
330− , pooSSE :: Maybe SSE
376+ pooSSE :: Maybe SSE
331 }
377 }
332
378
333-- | Provide default `PutObjectOptions`.
379-- | Provide default `PutObjectOptions`.
334defaultPutObjectOptions :: PutObjectOptions
380defaultPutObjectOptions :: PutObjectOptions
335defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
381defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
336
382
337−-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
338−-- stripped and a Just is returned.
339−userMetadataHeaderNameMaybe :: Text -> Maybe Text
340−userMetadataHeaderNameMaybe k =
341− let prefix = T.toCaseFold "X-Amz-Meta-"
342− n = T.length prefix
343− in if T.toCaseFold (T.take n k) == prefix
344− then Just (T.drop n k)
345− else Nothing
346−
347−addXAmzMetaPrefix :: Text -> Text
348−addXAmzMetaPrefix s | isJust (userMetadataHeaderNameMaybe s) = s
349− | otherwise = "X-Amz-Meta-" <> s
350−
351−mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
352−mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix $ x, encodeUtf8 y))
353−
354pooToHeaders :: PutObjectOptions -> [HT.Header]
383pooToHeaders :: PutObjectOptions -> [HT.Header]
355−pooToHeaders poo = userMetadata
384+pooToHeaders poo =
385+ userMetadata
356− ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
386+ ++ mapMaybe tupToMaybe (zip names values)
357− ++ maybe [] toPutObjectHeaders (pooSSE poo)
387+ ++ maybe [] toPutObjectHeaders (pooSSE poo)
358 where
388 where
359− tupToMaybe (k, Just v) = Just (k, v)
389+ tupToMaybe (k, Just v) = Just (k, v)
360 tupToMaybe (_, Nothing) = Nothing
390 tupToMaybe (_, Nothing) = Nothing
361−
362 userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
391 userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
363−
392+ names =
364− names = ["content-type",
393+ [ "content-type",
365− "content-encoding",
394+ "content-encoding",
366− "content-disposition",
395+ "content-disposition",
367− "content-language",
396+ "content-language",
368− "cache-control",
397+ "cache-control",
369− "x-amz-storage-class"]
398+ "x-amz-storage-class"
399+ ]
400+ values =
401+ map
370− values = map (fmap encodeUtf8 . (poo &))
402+ (fmap encodeUtf8 . (poo &))
403+ [ pooContentType,
371− [pooContentType, pooContentEncoding,
404+ pooContentEncoding,
372− pooContentDisposition, pooContentLanguage,
405+ pooContentDisposition,
406+ pooContentLanguage,
373− pooCacheControl, pooStorageClass]
407+ pooCacheControl,
374−
408+ pooStorageClass
409+ ]
375
410
376-- |
411-- |
377-- BucketInfo returned for list buckets call
412-- BucketInfo returned for list buckets call
378−data BucketInfo = BucketInfo {
413+data BucketInfo = BucketInfo
379− biName :: Bucket
414+ { biName :: Bucket,
380− , biCreationDate :: UTCTime
415+ biCreationDate :: UTCTime
416+ }
381− } deriving (Show, Eq)
417+ deriving stock (Show, Eq)
382
418
383-- | A type alias to represent a part-number for multipart upload
419-- | A type alias to represent a part-number for multipart upload
384type PartNumber = Int16
420type PartNumber = Int16
385
421
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+
386-- | A type alias to represent an upload-id for multipart upload
445-- | A type alias to represent an upload-id for multipart upload
387type UploadId = Text
446type UploadId = Text
388
447
@@ -391,95 +450,116 @@ type PartTuple = (PartNumber, ETag)
391
450
392-- | Represents result from a listing of object parts of an ongoing
451-- | Represents result from a listing of object parts of an ongoing
393-- multipart upload.
452-- multipart upload.
394−data ListPartsResult = ListPartsResult {
453+data ListPartsResult = ListPartsResult
395− lprHasMore :: Bool
454+ { lprHasMore :: Bool,
396− , lprNextPart :: Maybe Int
455+ lprNextPart :: Maybe Int,
397− , lprParts :: [ObjectPartInfo]
456+ lprParts :: [ObjectPartInfo]
457+ }
398− } deriving (Show, Eq)
458+ deriving stock (Show, Eq)
399
459
400-- | Represents information about an object part in an ongoing
460-- | Represents information about an object part in an ongoing
401-- multipart upload.
461-- multipart upload.
402−data ObjectPartInfo = ObjectPartInfo {
462+data ObjectPartInfo = ObjectPartInfo
403− opiNumber :: PartNumber
463+ { opiNumber :: PartNumber,
404− , opiETag :: ETag
464+ opiETag :: ETag,
405− , opiSize :: Int64
465+ opiSize :: Int64,
406− , opiModTime :: UTCTime
466+ opiModTime :: UTCTime
467+ }
407− } deriving (Show, Eq)
468+ deriving stock (Show, Eq)
408
469
409-- | Represents result from a listing of incomplete uploads to a
470-- | Represents result from a listing of incomplete uploads to a
410-- bucket.
471-- bucket.
411−data ListUploadsResult = ListUploadsResult {
472+data ListUploadsResult = ListUploadsResult
412− lurHasMore :: Bool
473+ { lurHasMore :: Bool,
413− , lurNextKey :: Maybe Text
474+ lurNextKey :: Maybe Text,
414− , lurNextUpload :: Maybe Text
475+ lurNextUpload :: Maybe Text,
415− , lurUploads :: [(Object, UploadId, UTCTime)]
476+ lurUploads :: [(Object, UploadId, UTCTime)],
416− , lurCPrefixes :: [Text]
477+ lurCPrefixes :: [Text]
478+ }
417− } deriving (Show, Eq)
479+ deriving stock (Show, Eq)
418
480
419-- | Represents information about a multipart upload.
481-- | Represents information about a multipart upload.
420−data UploadInfo = UploadInfo {
482+data UploadInfo = UploadInfo
421− uiKey :: Object
483+ { uiKey :: Object,
422− , uiUploadId :: UploadId
484+ uiUploadId :: UploadId,
423− , uiInitTime :: UTCTime
485+ uiInitTime :: UTCTime,
424− , uiSize :: Int64
486+ uiSize :: Int64
487+ }
425− } deriving (Show, Eq)
488+ deriving stock (Show, Eq)
426
489
427-- | Represents result from a listing of objects in a bucket.
490-- | Represents result from a listing of objects in a bucket.
428−data ListObjectsResult = ListObjectsResult {
491+data ListObjectsResult = ListObjectsResult
429− lorHasMore :: Bool
492+ { lorHasMore :: Bool,
430− , lorNextToken :: Maybe Text
493+ lorNextToken :: Maybe Text,
431− , lorObjects :: [ObjectInfo]
494+ lorObjects :: [ObjectInfo],
432− , lorCPrefixes :: [Text]
495+ lorCPrefixes :: [Text]
496+ }
433− } deriving (Show, Eq)
497+ deriving stock (Show, Eq)
434
498
435-- | Represents result from a listing of objects version 1 in a bucket.
499-- | Represents result from a listing of objects version 1 in a bucket.
436−data ListObjectsV1Result = ListObjectsV1Result {
500+data ListObjectsV1Result = ListObjectsV1Result
437− lorHasMore' :: Bool
501+ { lorHasMore' :: Bool,
438− , lorNextMarker :: Maybe Text
502+ lorNextMarker :: Maybe Text,
439− , lorObjects' :: [ObjectInfo]
503+ lorObjects' :: [ObjectInfo],
440− , lorCPrefixes' :: [Text]
504+ lorCPrefixes' :: [Text]
505+ }
441− } deriving (Show, Eq)
506+ deriving stock (Show, Eq)
442
507
443-- | Represents information about an object.
508-- | Represents information about an object.
444data ObjectInfo = ObjectInfo
509data ObjectInfo = ObjectInfo
510+ { -- | Object key
445− { oiObject :: Object -- ^ Object key
511+ oiObject :: Object,
512+ -- | Modification time of the object
446− , oiModTime :: UTCTime -- ^ Modification time of the object
513+ oiModTime :: UTCTime,
514+ -- | ETag of the object
447− , oiETag :: ETag -- ^ ETag of the object
515+ oiETag :: ETag,
448− , oiSize :: Int64 -- ^ Size of the object in bytes
516+ -- | Size of the object in bytes
517+ oiSize :: Int64,
449− , oiUserMetadata :: H.HashMap Text Text -- ^ A map of user-metadata
518+ -- | A map of user-metadata
450− -- pairs stored with an
519+ -- pairs stored with an
451− -- object (keys will not
520+ -- object (keys will not
452− -- have the @X-Amz-Meta-@
521+ -- have the @X-Amz-Meta-@
453− -- prefix).
522+ -- prefix).
523+ oiUserMetadata :: H.HashMap Text Text,
454− , oiMetadata :: H.HashMap Text Text -- ^ A map of metadata
524+ -- | A map of metadata
455− -- key-value pairs (not
525+ -- key-value pairs (not
456− -- including the
526+ -- including the
457− -- user-metadata pairs)
527+ -- user-metadata pairs)
528+ oiMetadata :: H.HashMap Text Text
529+ }
458− } deriving (Show, Eq)
530+ deriving stock (Show, Eq)
459
531
460-- | Represents source object in server-side copy object
532-- | Represents source object in server-side copy object
461data SourceInfo = SourceInfo
533data SourceInfo = SourceInfo
462− { srcBucket :: Text -- ^ Bucket containing the source object
534+ { -- | Bucket containing the source object
535+ srcBucket :: Text,
463− , srcObject :: Text -- ^ Source object key
536+ -- | Source object key
537+ srcObject :: Text,
464− , srcRange :: Maybe (Int64, Int64) -- ^ Source object
538+ -- | Source object
465− -- byte-range
539+ -- byte-range
466− -- (inclusive)
540+ -- (inclusive)
541+ srcRange :: Maybe (Int64, Int64),
467− , srcIfMatch :: Maybe Text -- ^ ETag condition on source -
542+ -- | ETag condition on source -
468− -- object is copied only if the
543+ -- object is copied only if the
469− -- source object's ETag matches
544+ -- source object's ETag matches
470− -- this value.
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.
471− , srcIfNoneMatch :: Maybe Text -- ^ ETag not match condition
551+ srcIfNoneMatch :: Maybe Text,
472− -- on source - object is copied
552+ -- | Copy source object only
473− -- if ETag does not match this
553+ -- if the source has been
474− -- value.
554+ -- modified since this time.
475− , srcIfModifiedSince :: Maybe UTCTime -- ^ Copy source object only
555+ srcIfModifiedSince :: Maybe UTCTime,
476− -- if the source has been
477− -- modified since this time.
478− , srcIfUnmodifiedSince :: Maybe UTCTime -- ^ Copy source object only
556+ -- | Copy source object only
479− -- if the source has been
557+ -- if the source has been
480− -- un-modified since this
558+ -- un-modified since this
481− -- given time.
559+ -- given time.
560+ srcIfUnmodifiedSince :: Maybe UTCTime
561+ }
482− } deriving (Show, Eq)
562+ deriving stock (Show, Eq)
483
563
484-- | Provide a default for `SourceInfo`
564-- | Provide a default for `SourceInfo`
485defaultSourceInfo :: SourceInfo
565defaultSourceInfo :: SourceInfo
@@ -487,9 +567,12 @@ defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing
487
567
488-- | Represents destination object in server-side copy object
568-- | Represents destination object in server-side copy object
489data DestinationInfo = DestinationInfo
569data DestinationInfo = DestinationInfo
490− { dstBucket :: Text -- ^ Destination bucket
570+ { -- | Destination bucket
571+ dstBucket :: Text,
491− , dstObject :: Text -- ^ Destination object key
572+ -- | Destination object key
573+ dstObject :: Text
574+ }
492− } deriving (Show, Eq)
575+ deriving stock (Show, Eq)
493
576
494-- | Provide a default for `DestinationInfo`
577-- | Provide a default for `DestinationInfo`
495defaultDestinationInfo :: DestinationInfo
578defaultDestinationInfo :: DestinationInfo
@@ -498,96 +581,106 @@ defaultDestinationInfo = DestinationInfo "" ""
498-- | Data type for options when getting an object from the
581-- | Data type for options when getting an object from the
499-- service. Start with the empty `defaultGetObjectOptions` and modify
582-- service. Start with the empty `defaultGetObjectOptions` and modify
500-- it using the goo* functions.
583-- it using the goo* functions.
501−data GetObjectOptions = GetObjectOptions {
584+data GetObjectOptions = GetObjectOptions
502− -- | Set object's data of given offset begin and end,
585+ { -- | Set object's data of given offset begin and end,
503 -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
586 -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
504− gooRange :: Maybe ByteRange
587+ gooRange :: Maybe ByteRange,
505 -- | Set matching ETag condition, GetObject which matches the following
588 -- | Set matching ETag condition, GetObject which matches the following
506 -- ETag.
589 -- ETag.
507− , gooIfMatch :: Maybe ETag
590+ gooIfMatch :: Maybe ETag,
508 -- | Set matching ETag none condition, GetObject which does not match
591 -- | Set matching ETag none condition, GetObject which does not match
509 -- the following ETag.
592 -- the following ETag.
510− , gooIfNoneMatch :: Maybe ETag
593+ gooIfNoneMatch :: Maybe ETag,
511 -- | Set object unmodified condition, GetObject unmodified since given time.
594 -- | Set object unmodified condition, GetObject unmodified since given time.
512− , gooIfUnmodifiedSince :: Maybe UTCTime
595+ gooIfUnmodifiedSince :: Maybe UTCTime,
513 -- | Set object modified condition, GetObject modified since given time.
596 -- | Set object modified condition, GetObject modified since given time.
514− , gooIfModifiedSince :: Maybe UTCTime
597+ gooIfModifiedSince :: Maybe UTCTime,
515 -- | Specify SSE-C key
598 -- | Specify SSE-C key
516− , gooSSECKey :: Maybe SSECKey
599+ gooSSECKey :: Maybe SSECKey
517 }
600 }
518
601
519-- | Provide default `GetObjectOptions`.
602-- | Provide default `GetObjectOptions`.
520defaultGetObjectOptions :: GetObjectOptions
603defaultGetObjectOptions :: GetObjectOptions
521defaultGetObjectOptions =
604defaultGetObjectOptions =
522− GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
605+ GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
523
606
524gooToHeaders :: GetObjectOptions -> [HT.Header]
607gooToHeaders :: GetObjectOptions -> [HT.Header]
525−gooToHeaders goo = rangeHdr ++ zip names values
608+gooToHeaders goo =
609+ rangeHdr
610+ ++ zip names values
526− ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
611+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
527 where
612 where
613+ names =
528− names = ["If-Match",
614+ [ "If-Match",
529− "If-None-Match",
615+ "If-None-Match",
530− "If-Unmodified-Since",
616+ "If-Unmodified-Since",
531− "If-Modified-Since"]
617+ "If-Modified-Since"
618+ ]
619+ values =
620+ mapMaybe
532− values = mapMaybe (fmap encodeUtf8 . (goo &))
621+ (fmap encodeUtf8 . (goo &))
622+ [ gooIfMatch,
533− [gooIfMatch, gooIfNoneMatch,
623+ gooIfNoneMatch,
534− fmap formatRFC1123 . gooIfUnmodifiedSince,
624+ fmap formatRFC1123 . gooIfUnmodifiedSince,
535− fmap formatRFC1123 . gooIfModifiedSince]
625+ fmap formatRFC1123 . gooIfModifiedSince
626+ ]
627+ rangeHdr =
536− rangeHdr = maybe [] (\a -> [(hRange, HT.renderByteRanges [a])])
628+ maybe [] (\a -> [(hRange, HT.renderByteRanges [a])]) $
537− $ gooRange goo
629+ gooRange goo
538
630
539-- | Data type returned by 'getObject' representing the object being
631-- | Data type returned by 'getObject' representing the object being
540-- retrieved. Use the @gor*@ functions to access its contents.
632-- retrieved. Use the @gor*@ functions to access its contents.
541−data GetObjectResponse = GetObjectResponse {
633+data GetObjectResponse = GetObjectResponse
542− -- | ObjectInfo of the object being retrieved.
634+ { -- | ObjectInfo of the object being retrieved.
543− gorObjectInfo :: ObjectInfo
635+ gorObjectInfo :: ObjectInfo,
544− -- | A conduit of the bytes of the object.
636+ -- | A conduit of the bytes of the object.
545− , gorObjectStream :: C.ConduitM () ByteString Minio ()
637+ gorObjectStream :: C.ConduitM () ByteString Minio ()
546− }
638+ }
547
639
548-- | A data-type for events that can occur in the object storage
640-- | A data-type for events that can occur in the object storage
549-- server. Reference:
641-- server. Reference:
550-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
642-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
643+data Event
551−data Event = ObjectCreated
644+ = ObjectCreated
552− | ObjectCreatedPut
645+ | ObjectCreatedPut
553− | ObjectCreatedPost
646+ | ObjectCreatedPost
554− | ObjectCreatedCopy
647+ | ObjectCreatedCopy
555− | ObjectCreatedMultipartUpload
648+ | ObjectCreatedMultipartUpload
556− | ObjectRemoved
649+ | ObjectRemoved
557− | ObjectRemovedDelete
650+ | ObjectRemovedDelete
558− | ObjectRemovedDeleteMarkerCreated
651+ | ObjectRemovedDeleteMarkerCreated
559− | ReducedRedundancyLostObject
652+ | ReducedRedundancyLostObject
560− deriving (Eq)
653+ deriving stock (Eq, Show)
561−
654+
562−instance Show Event where
655+instance ToText Event where
563− show ObjectCreated = "s3:ObjectCreated:*"
656+ toText ObjectCreated = "s3:ObjectCreated:*"
564− show ObjectCreatedPut = "s3:ObjectCreated:Put"
657+ toText ObjectCreatedPut = "s3:ObjectCreated:Put"
565− show ObjectCreatedPost = "s3:ObjectCreated:Post"
658+ toText ObjectCreatedPost = "s3:ObjectCreated:Post"
566− show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
659+ toText ObjectCreatedCopy = "s3:ObjectCreated:Copy"
567− show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
660+ toText ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
568− show ObjectRemoved = "s3:ObjectRemoved:*"
661+ toText ObjectRemoved = "s3:ObjectRemoved:*"
569− show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
662+ toText ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
570− show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
663+ toText ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
571− show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
664+ toText ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
572
665
573textToEvent :: Text -> Maybe Event
666textToEvent :: Text -> Maybe Event
574textToEvent t = case t of
667textToEvent t = case t of
575− "s3:ObjectCreated:*" -> Just ObjectCreated
668+ "s3:ObjectCreated:*" -> Just ObjectCreated
576− "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
669+ "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
577− "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
670+ "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
578− "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
671+ "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
579− "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
672+ "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
580− "s3:ObjectRemoved:*" -> Just ObjectRemoved
673+ "s3:ObjectRemoved:*" -> Just ObjectRemoved
581− "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
674+ "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
582 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
675 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
583− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
676+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
584− _ -> Nothing
677+ _ -> Nothing
585−
586
678
587-- | Filter data type - part of notification configuration
679-- | Filter data type - part of notification configuration
588−data Filter = Filter
680+newtype Filter = Filter
589 { fFilter :: FilterKey
681 { fFilter :: FilterKey
682+ }
590− } deriving (Show, Eq)
683+ deriving stock (Show, Eq)
591
684
592-- | defaultFilter is empty, used to create a notification
685-- | defaultFilter is empty, used to create a notification
593-- configuration.
686-- configuration.
@@ -595,9 +688,10 @@ defaultFilter :: Filter
595defaultFilter = Filter defaultFilterKey
688defaultFilter = Filter defaultFilterKey
596
689
597-- | FilterKey contains FilterRules, and is part of a Filter.
690-- | FilterKey contains FilterRules, and is part of a Filter.
598−data FilterKey = FilterKey
691+newtype FilterKey = FilterKey
599 { fkKey :: FilterRules
692 { fkKey :: FilterRules
693+ }
600− } deriving (Show, Eq)
694+ deriving stock (Show, Eq)
601
695
602-- | defaultFilterKey is empty, used to create notification
696-- | defaultFilterKey is empty, used to create notification
603-- configuration.
697-- configuration.
@@ -605,16 +699,16 @@ defaultFilterKey :: FilterKey
605defaultFilterKey = FilterKey defaultFilterRules
699defaultFilterKey = FilterKey defaultFilterRules
606
700
607-- | FilterRules represents a collection of `FilterRule`s.
701-- | FilterRules represents a collection of `FilterRule`s.
608−data FilterRules = FilterRules
702+newtype FilterRules = FilterRules
609 { frFilterRules :: [FilterRule]
703 { frFilterRules :: [FilterRule]
704+ }
610− } deriving (Show, Eq)
705+ deriving stock (Show, Eq)
611
706
612-- | defaultFilterRules is empty, used to create notification
707-- | defaultFilterRules is empty, used to create notification
613-- configuration.
708-- configuration.
614defaultFilterRules :: FilterRules
709defaultFilterRules :: FilterRules
615defaultFilterRules = FilterRules []
710defaultFilterRules = FilterRules []
616
711
617−
618-- | A filter rule that can act based on the suffix or prefix of an
712-- | A filter rule that can act based on the suffix or prefix of an
619-- object. As an example, let's create two filter rules:
713-- object. As an example, let's create two filter rules:
620--
714--
@@ -625,9 +719,10 @@ defaultFilterRules = FilterRules []
625-- for objects having a suffix of ".jpg", and the @prefixRule@
719-- for objects having a suffix of ".jpg", and the @prefixRule@
626-- restricts it to objects having a prefix of "images/".
720-- restricts it to objects having a prefix of "images/".
627data FilterRule = FilterRule
721data FilterRule = FilterRule
628− { frName :: Text
722+ { frName :: Text,
629− , frValue :: Text
723+ frValue :: Text
724+ }
630− } deriving (Show, Eq)
725+ deriving stock (Show, Eq)
631
726
632-- | Arn is an alias of Text
727-- | Arn is an alias of Text
633type Arn = Text
728type Arn = Text
@@ -636,11 +731,12 @@ type Arn = Text
636-- notification system. It could represent a Queue, Topic or Lambda
731-- notification system. It could represent a Queue, Topic or Lambda
637-- Function configuration.
732-- Function configuration.
638data NotificationConfig = NotificationConfig
733data NotificationConfig = NotificationConfig
639− { ncId :: Text
734+ { ncId :: Text,
640− , ncArn :: Arn
735+ ncArn :: Arn,
641− , ncEvents :: [Event]
736+ ncEvents :: [Event],
642− , ncFilter :: Filter
737+ ncFilter :: Filter
738+ }
643− } deriving (Show, Eq)
739+ deriving stock (Show, Eq)
644
740
645-- | A data-type to represent bucket notification configuration. It is
741-- | A data-type to represent bucket notification configuration. It is
646-- a collection of queue, topic or lambda function configurations. The
742-- a collection of queue, topic or lambda function configurations. The
@@ -648,16 +744,16 @@ data NotificationConfig = NotificationConfig
648-- described at
744-- described at
649-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
745-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
650data Notification = Notification
746data Notification = Notification
651− { nQueueConfigurations :: [NotificationConfig]
747+ { nQueueConfigurations :: [NotificationConfig],
652− , nTopicConfigurations :: [NotificationConfig]
748+ nTopicConfigurations :: [NotificationConfig],
653− , nCloudFunctionConfigurations :: [NotificationConfig]
749+ nCloudFunctionConfigurations :: [NotificationConfig]
750+ }
654− } deriving (Eq, Show)
751+ deriving stock (Show, Eq)
655
752
656-- | The default notification configuration is empty.
753-- | The default notification configuration is empty.
657defaultNotification :: Notification
754defaultNotification :: Notification
658defaultNotification = Notification [] [] []
755defaultNotification = Notification [] [] []
659
756
660−
661--------------------------------------------------------------------------
757--------------------------------------------------------------------------
662-- Select API Related Types
758-- Select API Related Types
663--------------------------------------------------------------------------
759--------------------------------------------------------------------------
@@ -665,38 +761,42 @@ defaultNotification = Notification [] [] []
665-- | SelectRequest represents the Select API call. Use the
761-- | SelectRequest represents the Select API call. Use the
666-- `selectRequest` function to create a value of this type.
762-- `selectRequest` function to create a value of this type.
667data SelectRequest = SelectRequest
763data SelectRequest = SelectRequest
668− { srExpression :: Text
764+ { srExpression :: Text,
669− , srExpressionType :: ExpressionType
765+ srExpressionType :: ExpressionType,
670− , srInputSerialization :: InputSerialization
766+ srInputSerialization :: InputSerialization,
671− , srOutputSerialization :: OutputSerialization
767+ srOutputSerialization :: OutputSerialization,
672− , srRequestProgressEnabled :: Maybe Bool
768+ srRequestProgressEnabled :: Maybe Bool
769+ }
673− } deriving (Eq, Show)
770+ deriving stock (Show, Eq)
674
771
675data ExpressionType = SQL
772data ExpressionType = SQL
676− deriving (Eq, Show)
773+ deriving stock (Show, Eq)
677
774
678-- | InputSerialization represents format information of the input
775-- | InputSerialization represents format information of the input
679-- object being queried. Use one of the smart constructors such as
776-- object being queried. Use one of the smart constructors such as
680-- `defaultCsvInput` as a starting value, and add compression info
777-- `defaultCsvInput` as a starting value, and add compression info
681-- using `setInputCompressionType`
778-- using `setInputCompressionType`
682data InputSerialization = InputSerialization
779data InputSerialization = InputSerialization
683− { isCompressionType :: Maybe CompressionType
780+ { isCompressionType :: Maybe CompressionType,
684− , isFormatInfo :: InputFormatInfo
781+ isFormatInfo :: InputFormatInfo
782+ }
685− } deriving (Eq, Show)
783+ deriving stock (Show, Eq)
686
784
687-- | Data type representing the compression setting in a Select
785-- | Data type representing the compression setting in a Select
688-- request.
786-- request.
689−data CompressionType = CompressionTypeNone
787+data CompressionType
788+ = CompressionTypeNone
690− | CompressionTypeGzip
789+ | CompressionTypeGzip
691− | CompressionTypeBzip2
790+ | CompressionTypeBzip2
692− deriving (Eq, Show)
791+ deriving stock (Show, Eq)
693
792
694-- | Data type representing input object format information in a
793-- | Data type representing input object format information in a
695-- Select request.
794-- Select request.
795+data InputFormatInfo
696−data InputFormatInfo = InputFormatCSV CSVInputProp
796+ = InputFormatCSV CSVInputProp
697− | InputFormatJSON JSONInputProp
797+ | InputFormatJSON JSONInputProp
698− | InputFormatParquet
798+ | InputFormatParquet
699− deriving (Eq, Show)
799+ deriving stock (Show, Eq)
700
800
701-- | defaultCsvInput returns InputSerialization with default CSV
801-- | defaultCsvInput returns InputSerialization with default CSV
702-- format, and without any compression setting.
802-- format, and without any compression setting.
@@ -706,14 +806,18 @@ defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
706-- | linesJsonInput returns InputSerialization with JSON line based
806-- | linesJsonInput returns InputSerialization with JSON line based
707-- format with no compression setting.
807-- format with no compression setting.
708linesJsonInput :: InputSerialization
808linesJsonInput :: InputSerialization
809+linesJsonInput =
709−linesJsonInput = InputSerialization Nothing
810+ InputSerialization
811+ Nothing
710− (InputFormatJSON $ JSONInputProp JSONTypeLines)
812+ (InputFormatJSON $ JSONInputProp JSONTypeLines)
711
813
712-- | documentJsonInput returns InputSerialization with JSON document
814-- | documentJsonInput returns InputSerialization with JSON document
713-- based format with no compression setting.
815-- based format with no compression setting.
714documentJsonInput :: InputSerialization
816documentJsonInput :: InputSerialization
817+documentJsonInput =
715−documentJsonInput = InputSerialization Nothing
818+ InputSerialization
819+ Nothing
716− (InputFormatJSON $ JSONInputProp JSONTypeDocument)
820+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)
717
821
718-- | defaultParquetInput returns InputSerialization with Parquet
822-- | defaultParquetInput returns InputSerialization with Parquet
719-- format, and no compression setting.
823-- format, and no compression setting.
@@ -722,12 +826,14 @@ defaultParquetInput = InputSerialization Nothing InputFormatParquet
722
826
723-- | setInputCompressionType sets the compression type for the input
827-- | setInputCompressionType sets the compression type for the input
724-- of the SelectRequest
828-- of the SelectRequest
725−setInputCompressionType :: CompressionType -> SelectRequest
829+setInputCompressionType ::
830+ CompressionType ->
831+ SelectRequest ->
726− -> SelectRequest
832+ SelectRequest
727setInputCompressionType c i =
833setInputCompressionType c i =
728− let is = srInputSerialization i
834+ let is = srInputSerialization i
729− is' = is { isCompressionType = Just c }
835+ is' = is {isCompressionType = Just c}
730− in i { srInputSerialization = is' }
836+ in i {srInputSerialization = is'}
731
837
732-- | defaultCsvOutput returns OutputSerialization with default CSV
838-- | defaultCsvOutput returns OutputSerialization with default CSV
733-- format.
839-- format.
@@ -744,40 +850,42 @@ defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
744-- SelectRequest with the SQL query text given by @query@, the input
850-- SelectRequest with the SQL query text given by @query@, the input
745-- serialization settings (compression format and format information)
851-- serialization settings (compression format and format information)
746-- @inputSer@ and the output serialization settings @outputSer@.
852-- @inputSer@ and the output serialization settings @outputSer@.
853+selectRequest ::
854+ Text ->
855+ InputSerialization ->
747−selectRequest :: Text -> InputSerialization -> OutputSerialization
856+ OutputSerialization ->
748− -> SelectRequest
857+ SelectRequest
749selectRequest sqlQuery inputSer outputSer =
858selectRequest sqlQuery inputSer outputSer =
859+ SelectRequest
750− SelectRequest { srExpression = sqlQuery
860+ { srExpression = sqlQuery,
751− , srExpressionType = SQL
861+ srExpressionType = SQL,
752− , srInputSerialization = inputSer
862+ srInputSerialization = inputSer,
753− , srOutputSerialization = outputSer
863+ srOutputSerialization = outputSer,
754− , srRequestProgressEnabled = Nothing
864+ srRequestProgressEnabled = Nothing
755− }
865+ }
756
866
757-- | setRequestProgressEnabled sets the flag for turning on progress
867-- | setRequestProgressEnabled sets the flag for turning on progress
758-- messages when the Select response is being streamed back to the
868-- messages when the Select response is being streamed back to the
759-- client.
869-- client.
760setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
870setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
761setRequestProgressEnabled enabled sr =
871setRequestProgressEnabled enabled sr =
762− sr { srRequestProgressEnabled = Just enabled }
872+ sr {srRequestProgressEnabled = Just enabled}
763
873
764type CSVInputProp = CSVProp
874type CSVInputProp = CSVProp
765
875
766-- | CSVProp represents CSV format properties. It is built up using
876-- | CSVProp represents CSV format properties. It is built up using
767-- the Monoid instance.
877-- the Monoid instance.
768−data CSVProp = CSVProp (H.HashMap Text Text)
878+newtype CSVProp = CSVProp (H.HashMap Text Text)
769− deriving (Eq, Show)
879+ deriving stock (Show, Eq)
770
880
771−#if (__GLASGOW_HASKELL__ >= 804)
772instance Semigroup CSVProp where
881instance Semigroup CSVProp where
773− (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
882+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
774−#endif
775
883
776instance Monoid CSVProp where
884instance Monoid CSVProp where
777− mempty = CSVProp mempty
885+ mempty = CSVProp mempty
778−#if (__GLASGOW_HASKELL__ < 804)
886+
779− mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
887+csvPropsList :: CSVProp -> [(Text, Text)]
780−#endif
888+csvPropsList (CSVProp h) = sort $ H.toList h
781
889
782defaultCSVProp :: CSVProp
890defaultCSVProp :: CSVProp
783defaultCSVProp = mempty
891defaultCSVProp = mempty
@@ -801,18 +909,21 @@ quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
801-- | FileHeaderInfo specifies information about column headers for CSV
909-- | FileHeaderInfo specifies information about column headers for CSV
802-- format.
910-- format.
803data FileHeaderInfo
911data FileHeaderInfo
804− = FileHeaderNone -- ^ No column headers are present
912+ = -- | No column headers are present
913+ FileHeaderNone
805− | FileHeaderUse -- ^ Headers are present and they should be used
914+ | -- | Headers are present and they should be used
915+ FileHeaderUse
806− | FileHeaderIgnore -- ^ Header are present, but should be ignored
916+ | -- | Header are present, but should be ignored
917+ FileHeaderIgnore
807− deriving (Eq, Show)
918+ deriving stock (Show, Eq)
808
919
809-- | Specify the CSV file header info property.
920-- | Specify the CSV file header info property.
810fileHeaderInfo :: FileHeaderInfo -> CSVProp
921fileHeaderInfo :: FileHeaderInfo -> CSVProp
811−fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString
922+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toStr
812 where
923 where
813− toString FileHeaderNone = "NONE"
924+ toStr FileHeaderNone = "NONE"
814− toString FileHeaderUse = "USE"
925+ toStr FileHeaderUse = "USE"
815− toString FileHeaderIgnore = "IGNORE"
926+ toStr FileHeaderIgnore = "IGNORE"
816
927
817-- | Specify the CSV comment character property. Lines starting with
928-- | Specify the CSV comment character property. Lines starting with
818-- this character are ignored by the server.
929-- this character are ignored by the server.
@@ -825,64 +936,69 @@ allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter"
825
936
826-- | Set the CSV format properties in the InputSerialization.
937-- | Set the CSV format properties in the InputSerialization.
827setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
938setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
828−setInputCSVProps p is = is { isFormatInfo = InputFormatCSV p }
939+setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
829
940
830-- | Set the CSV format properties in the OutputSerialization.
941-- | Set the CSV format properties in the OutputSerialization.
831outputCSVFromProps :: CSVProp -> OutputSerialization
942outputCSVFromProps :: CSVProp -> OutputSerialization
832−outputCSVFromProps p = OutputSerializationCSV p
943+outputCSVFromProps = OutputSerializationCSV
833
944
834−data JSONInputProp = JSONInputProp { jsonipType :: JSONType }
945+newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
835− deriving (Eq, Show)
946+ deriving stock (Show, Eq)
836
947
837data JSONType = JSONTypeDocument | JSONTypeLines
948data JSONType = JSONTypeDocument | JSONTypeLines
838− deriving (Eq, Show)
949+ deriving stock (Show, Eq)
839−
840
950
841-- | OutputSerialization represents output serialization settings for
951-- | OutputSerialization represents output serialization settings for
842-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
952-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
843-- a starting point.
953-- a starting point.
954+data OutputSerialization
844−data OutputSerialization = OutputSerializationJSON JSONOutputProp
955+ = OutputSerializationJSON JSONOutputProp
845− | OutputSerializationCSV CSVOutputProp
956+ | OutputSerializationCSV CSVOutputProp
846− deriving (Eq, Show)
957+ deriving stock (Show, Eq)
847
958
848type CSVOutputProp = CSVProp
959type CSVOutputProp = CSVProp
849
960
850-- | quoteFields is an output serialization parameter
961-- | quoteFields is an output serialization parameter
851quoteFields :: QuoteFields -> CSVProp
962quoteFields :: QuoteFields -> CSVProp
963+quoteFields q = CSVProp $
852−quoteFields q = CSVProp $ H.singleton "QuoteFields" $
964+ H.singleton "QuoteFields" $
853− case q of
965+ case q of
854− QuoteFieldsAsNeeded -> "ASNEEDED"
966+ QuoteFieldsAsNeeded -> "ASNEEDED"
855− QuoteFieldsAlways -> "ALWAYS"
967+ QuoteFieldsAlways -> "ALWAYS"
856
968
857-- | Represent the QuoteField setting.
969-- | Represent the QuoteField setting.
858data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
970data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
859− deriving (Eq, Show)
971+ deriving stock (Show, Eq)
860
972
861−data JSONOutputProp = JSONOutputProp { jsonopRecordDelimiter :: Maybe Text }
973+newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
862− deriving (Eq, Show)
974+ deriving stock (Show, Eq)
863
975
864-- | Set the output record delimiter for JSON format
976-- | Set the output record delimiter for JSON format
865outputJSONFromRecordDelimiter :: Text -> OutputSerialization
977outputJSONFromRecordDelimiter :: Text -> OutputSerialization
866outputJSONFromRecordDelimiter t =
978outputJSONFromRecordDelimiter t =
867− OutputSerializationJSON (JSONOutputProp $ Just t)
979+ OutputSerializationJSON (JSONOutputProp $ Just t)
868
980
869-- Response related types
981-- Response related types
870
982
871-- | An EventMessage represents each kind of message received from the server.
983-- | An EventMessage represents each kind of message received from the server.
984+data EventMessage
872−data EventMessage = ProgressEventMessage { emProgress :: Progress }
985+ = ProgressEventMessage Progress
873− | StatsEventMessage { emStats :: Stats }
986+ | StatsEventMessage Stats
874− | RequestLevelErrorMessage { emErrorCode :: Text
987+ | RequestLevelErrorMessage
875− , emErrorMessage :: Text
988+ Text
876− }
989+ -- ^ Error code
990+ Text
991+ -- ^ Error message
877− | RecordPayloadEventMessage { emPayloadBytes :: ByteString }
992+ | RecordPayloadEventMessage ByteString
878− deriving (Eq, Show)
993+ deriving stock (Show, Eq)
879−
994+
880−data MsgHeaderName = MessageType
995+data MsgHeaderName
996+ = MessageType
881− | EventType
997+ | EventType
882− | ContentType
998+ | ContentType
883− | ErrorCode
999+ | ErrorCode
884− | ErrorMessage
1000+ | ErrorMessage
885− deriving (Eq, Show)
1001+ deriving stock (Show, Eq)
886
1002
887msgHeaderValueType :: Word8
1003msgHeaderValueType :: Word8
888msgHeaderValueType = 7
1004msgHeaderValueType = 7
@@ -890,11 +1006,12 @@ msgHeaderValueType = 7
890type MessageHeader = (MsgHeaderName, Text)
1006type MessageHeader = (MsgHeaderName, Text)
891
1007
892-- | Represent the progress event returned in the Select response.
1008-- | Represent the progress event returned in the Select response.
893−data Progress = Progress { pBytesScanned :: Int64
1009+data Progress = Progress
1010+ { pBytesScanned :: Int64,
894− , pBytesProcessed :: Int64
1011+ pBytesProcessed :: Int64,
895− , pBytesReturned :: Int64
1012+ pBytesReturned :: Int64
896− }
1013+ }
897− deriving (Eq, Show)
1014+ deriving stock (Show, Eq)
898
1015
899-- | Represent the stats event returned at the end of the Select
1016-- | Represent the stats event returned at the end of the Select
900-- response.
1017-- response.
@@ -907,86 +1024,89 @@ type Stats = Progress
907-- | Represents different kinds of payload that are used with S3 API
1024-- | Represents different kinds of payload that are used with S3 API
908-- requests.
1025-- requests.
909data Payload
1026data Payload
910− = PayloadBS ByteString
1027+ = PayloadBS ByteString
911− | PayloadH Handle Int64 Int64 -- file handle, offset and length
1028+ | PayloadH Handle Int64 Int64 -- file handle, offset and length
912− | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
1029+ | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
913
1030
914defaultPayload :: Payload
1031defaultPayload :: Payload
915defaultPayload = PayloadBS ""
1032defaultPayload = PayloadBS ""
916
1033
917−data AdminReqInfo = AdminReqInfo {
1034+data AdminReqInfo = AdminReqInfo
918− ariMethod :: Method
1035+ { ariMethod :: Method,
919− , ariPayloadHash :: Maybe ByteString
1036+ ariPayloadHash :: Maybe ByteString,
920− , ariPayload :: Payload
1037+ ariPayload :: Payload,
921− , ariPath :: ByteString
1038+ ariPath :: ByteString,
922− , ariHeaders :: [Header]
1039+ ariHeaders :: [Header],
923− , ariQueryParams :: Query
1040+ ariQueryParams :: Query
924 }
1041 }
925
1042
926data S3ReqInfo = S3ReqInfo
1043data S3ReqInfo = S3ReqInfo
927− { riMethod :: Method
1044+ { riMethod :: Method,
928− , riBucket :: Maybe Bucket
1045+ riBucket :: Maybe Bucket,
929− , riObject :: Maybe Object
1046+ riObject :: Maybe Object,
930− , riQueryParams :: Query
1047+ riQueryParams :: Query,
931− , riHeaders :: [Header]
1048+ riHeaders :: [Header],
932− , riPayload :: Payload
1049+ riPayload :: Payload,
933− , riPayloadHash :: Maybe ByteString
1050+ riPayloadHash :: Maybe ByteString,
934− , riRegion :: Maybe Region
1051+ riRegion :: Maybe Region,
935− , riNeedsLocation :: Bool
1052+ riNeedsLocation :: Bool,
1053+ riPresignExpirySecs :: Maybe UrlExpiry
936 }
1054 }
937
1055
938defaultS3ReqInfo :: S3ReqInfo
1056defaultS3ReqInfo :: S3ReqInfo
939−defaultS3ReqInfo = S3ReqInfo HT.methodGet Nothing Nothing
1057+defaultS3ReqInfo =
1058+ S3ReqInfo
1059+ HT.methodGet
1060+ Nothing
1061+ Nothing
1062+ []
1063+ []
940− [] [] defaultPayload Nothing Nothing True
1064+ defaultPayload
1065+ Nothing
1066+ Nothing
1067+ True
1068+ Nothing
941
1069
942getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
1070getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
943getS3Path b o =
1071getS3Path b o =
944− let segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
1072+ let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
945− in
946− B.concat ["/", B.intercalate "/" segments]
1073+ in B.concat ["/", B.intercalate "/" segments]
947−
948−-- | Time to expire for a presigned URL. It interpreted as a number of
949−-- seconds. The maximum duration that can be specified is 7 days.
950−type UrlExpiry = Int
951
1074
952type RegionMap = H.HashMap Bucket Region
1075type RegionMap = H.HashMap Bucket Region
953
1076
954-- | The Minio Monad - all computations accessing object storage
1077-- | The Minio Monad - all computations accessing object storage
955-- happens in it.
1078-- happens in it.
956−newtype Minio a = Minio {
1079+newtype Minio a = Minio
957− unMinio :: ReaderT MinioConn (ResourceT IO) a
1080+ { unMinio :: ReaderT MinioConn (ResourceT IO) a
958 }
1081 }
959− deriving (
1082+ deriving newtype
960− Functor
1083+ ( Functor,
961− , Applicative
1084+ Applicative,
962− , Monad
1085+ Monad,
963− , MonadIO
1086+ MonadIO,
964− , MonadReader MinioConn
1087+ MonadReader MinioConn,
965− , MonadResource
1088+ MonadResource,
1089+ MonadUnliftIO
966 )
1090 )
967
1091
968−instance MonadUnliftIO Minio where
969− askUnliftIO = Minio $ ReaderT $ \r ->
970− withUnliftIO $ \u ->
971− return (UnliftIO (unliftIO u . flip runReaderT r . unMinio))
972−
973-- | MinioConn holds connection info and a connection pool to allow
1092-- | MinioConn holds connection info and a connection pool to allow
974-- for efficient resource re-use.
1093-- for efficient resource re-use.
975data MinioConn = MinioConn
1094data MinioConn = MinioConn
976− { mcConnInfo :: ConnectInfo
1095+ { mcConnInfo :: ConnectInfo,
977− , mcConnManager :: NC.Manager
1096+ mcConnManager :: NC.Manager,
978− , mcRegionMap :: MVar RegionMap
1097+ mcRegionMap :: MVar RegionMap
979 }
1098 }
980
1099
981class HasSvcNamespace env where
1100class HasSvcNamespace env where
982 getSvcNamespace :: env -> Text
1101 getSvcNamespace :: env -> Text
983
1102
984instance HasSvcNamespace MinioConn where
1103instance HasSvcNamespace MinioConn where
1104+ getSvcNamespace env =
985− getSvcNamespace env = let host = connectHost $ mcConnInfo env
1105+ let host = connectHost $ mcConnInfo env
986− in if | host == "storage.googleapis.com" ->
1106+ in ( if host == "storage.googleapis.com"
987− "http://doc.s3.amazonaws.com/2006-03-01"
1107+ then "http://doc.s3.amazonaws.com/2006-03-01"
988− | otherwise ->
989− "http://s3.amazonaws.com/doc/2006-03-01/"
1108+ else "http://s3.amazonaws.com/doc/2006-03-01/"
1109+ )
990
1110
991-- | Takes connection information and returns a connection object to
1111-- | Takes connection information and returns a connection object to
992-- be passed to 'runMinio'. The returned value can be kept in the
1112-- be passed to 'runMinio'. The returned value can be kept in the
@@ -994,11 +1114,16 @@ instance HasSvcNamespace MinioConn where
994-- object storage is accessed.
1114-- object storage is accessed.
995connect :: ConnectInfo -> IO MinioConn
1115connect :: ConnectInfo -> IO MinioConn
996connect ci = do
1116connect ci = do
1117+ let settings
997− let settings | connectIsSecure ci && connectDisableTLSCertValidation ci =
1118+ | connectIsSecure ci && connectDisableTLSCertValidation ci =
1119+ let badTlsSettings =
1120+ case def of
998− let badTlsSettings = Conn.TLSSettingsSimple True False False
1121+ tlsSettings@Conn.TLSSettingsSimple {} ->
1122+ tlsSettings {Conn.settingDisableCertificateValidation = True}
1123+ _ -> def
999− in TLS.mkManagerSettings badTlsSettings Nothing
1124+ in TLS.mkManagerSettings badTlsSettings Nothing
1000− | connectIsSecure ci = NC.tlsManagerSettings
1125+ | connectIsSecure ci = NC.tlsManagerSettings
1001− | otherwise = defaultManagerSettings
1126+ | otherwise = defaultManagerSettings
1002 mgr <- NC.newManager settings
1127 mgr <- NC.newManager settings
1003 mkMinioConn ci mgr
1128 mkMinioConn ci mgr
1004
1129
@@ -1012,8 +1137,8 @@ runMinioWith conn m = runResourceT $ runMinioResWith conn m
1012-- `MinioConn`.
1137-- `MinioConn`.
1013mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
1138mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
1014mkMinioConn ci mgr = do
1139mkMinioConn ci mgr = do
1015− rMapMVar <- M.newMVar H.empty
1140+ rMapMVar <- M.newMVar H.empty
1016− return $ MinioConn ci mgr rMapMVar
1141+ return $ MinioConn ci mgr rMapMVar
1017
1142
1018-- | Run the Minio action and return the result or an error.
1143-- | Run the Minio action and return the result or an error.
1019runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
1144runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
@@ -1026,12 +1151,12 @@ runMinio ci m = do
1026runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)
1151runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)
1027runMinioResWith conn m =
1152runMinioResWith conn m =
1028 flip runReaderT conn . unMinio $
1153 flip runReaderT conn . unMinio $
1029− fmap Right m `U.catches`
1154+ fmap Right m
1030− [ U.Handler handlerServiceErr
1155+ `U.catches` [ U.Handler handlerServiceErr,
1031− , U.Handler handlerHE
1156+ U.Handler handlerHE,
1032− , U.Handler handlerFE
1157+ U.Handler handlerFE,
1033− , U.Handler handlerValidation
1158+ U.Handler handlerValidation
1034− ]
1159+ ]
1035 where
1160 where
1036 handlerServiceErr = return . Left . MErrService
1161 handlerServiceErr = return . Left . MErrService
1037 handlerHE = return . Left . MErrHTTP
1162 handlerHE = return . Left . MErrHTTP
@@ -1045,9 +1170,22 @@ runMinioRes ci m = do
1045 conn <- liftIO $ connect ci
1170 conn <- liftIO $ connect ci
1046 runMinioResWith conn m
1171 runMinioResWith conn m
1047
1172
1048−s3Name :: Text -> Text -> Name
1049−s3Name ns s = Name s (Just ns) Nothing
1050−
1051-- | Format as per RFC 1123.
1173-- | Format as per RFC 1123.
1052formatRFC1123 :: UTCTime -> T.Text
1174formatRFC1123 :: UTCTime -> T.Text
1053formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1175formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1176+
1177+lookupRegionCache :: Bucket -> Minio (Maybe Region)
1178+lookupRegionCache b = do
1179+ rMVar <- asks mcRegionMap
1180+ rMap <- UM.readMVar rMVar
1181+ return $ H.lookup b rMap
1182+
1183+addToRegionCache :: Bucket -> Region -> Minio ()
1184+addToRegionCache b region = do
1185+ rMVar <- asks mcRegionMap
1186+ UM.modifyMVar_ rMVar $ return . H.insert b region
1187+
1188+deleteFromRegionCache :: Bucket -> Minio ()
1189+deleteFromRegionCache b = do
1190+ rMVar <- asks mcRegionMap
1191+ UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/Data/ByteString.hs+19−18
@@ -13,23 +13,21 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
17{-# LANGUAGE FlexibleInstances #-}
16{-# LANGUAGE FlexibleInstances #-}
17+
18module Network.Minio.Data.ByteString
18module Network.Minio.Data.ByteString
19− (
20− stripBS
19+ ( stripBS,
21− , UriEncodable(..)
20+ UriEncodable (..),
21+ )
22− ) where
22+where
23
23
24import qualified Data.ByteString as B
24import qualified Data.ByteString as B
25import qualified Data.ByteString.Builder as BB
25import qualified Data.ByteString.Builder as BB
26import qualified Data.ByteString.Char8 as BC8
26import qualified Data.ByteString.Char8 as BC8
27import qualified Data.ByteString.Lazy as LB
27import qualified Data.ByteString.Lazy as LB
28−import Data.Char (isSpace, toUpper, isAsciiUpper, isAsciiLower, isDigit)
28+import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace, toUpper)
29import qualified Data.Text as T
29import qualified Data.Text as T
30−import Numeric (showHex)
30+import Numeric (showHex)
31−
32−import Lib.Prelude
33
31
34stripBS :: ByteString -> ByteString
32stripBS :: ByteString -> ByteString
35stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
33stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
@@ -39,8 +37,10 @@ class UriEncodable s where
39
37
40instance UriEncodable [Char] where
38instance UriEncodable [Char] where
41 uriEncode encodeSlash payload =
39 uriEncode encodeSlash payload =
40+ LB.toStrict $
42− LB.toStrict $ BB.toLazyByteString $ mconcat $
41+ BB.toLazyByteString $
42+ mconcat $
43− map (`uriEncodeChar` encodeSlash) payload
43+ map (`uriEncodeChar` encodeSlash) payload
44
44
45instance UriEncodable ByteString where
45instance UriEncodable ByteString where
46 -- assumes that uriEncode is passed ASCII encoded strings.
46 -- assumes that uriEncode is passed ASCII encoded strings.
@@ -59,16 +59,17 @@ uriEncodeChar '/' True = BB.byteString "%2F"
59uriEncodeChar '/' False = BB.char7 '/'
59uriEncodeChar '/' False = BB.char7 '/'
60uriEncodeChar ch _
60uriEncodeChar ch _
61 | isAsciiUpper ch
61 | isAsciiUpper ch
62− || isAsciiLower ch
62+ || isAsciiLower ch
63− || isDigit ch
63+ || isDigit ch
64− || (ch == '_')
64+ || (ch == '_')
65− || (ch == '-')
65+ || (ch == '-')
66− || (ch == '.')
66+ || (ch == '.')
67+ || (ch == '~') =
67− || (ch == '~') = BB.char7 ch
68+ BB.char7 ch
68 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
69 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
69 where
70 where
70 f :: Word8 -> BB.Builder
71 f :: Word8 -> BB.Builder
71 f n = BB.char7 '%' <> BB.string7 hexStr
72 f n = BB.char7 '%' <> BB.string7 hexStr
72 where
73 where
73 hexStr = map toUpper $ showHex q $ showHex r ""
74 hexStr = map toUpper $ showHex q $ showHex r ""
74− (q, r) = divMod (fromIntegral n) (16::Word8)
75+ (q, r) = divMod n (16 :: Word8)
src/Network/Minio/Data/Crypto.hs+32−33
@@ -15,55 +15,54 @@
15--
15--
16
16
17module Network.Minio.Data.Crypto
17module Network.Minio.Data.Crypto
18− (
19− hashSHA256
18+ ( hashSHA256,
20− , hashSHA256FromSource
19+ hashSHA256FromSource,
21−
22− , hashMD5
20+ hashMD5,
23− , hashMD5ToBase64
21+ hashMD5ToBase64,
24− , hashMD5FromSource
22+ hashMD5FromSource,
25−
26− , hmacSHA256
23+ hmacSHA256,
27− , hmacSHA256RawBS
24+ hmacSHA256RawBS,
28− , digestToBS
25+ digestToBS,
29− , digestToBase16
26+ digestToBase16,
30−
31− , encodeToBase64
27+ encodeToBase64,
28+ )
32− ) where
29+where
33−
30+
34−import Crypto.Hash (Digest, MD5 (..), SHA256 (..),
31+import Crypto.Hash
32+ ( Digest,
33+ MD5 (..),
34+ SHA256 (..),
35− hashWith)
35+ hashWith,
36+ )
36−import Crypto.Hash.Conduit (sinkHash)
37+import Crypto.Hash.Conduit (sinkHash)
37−import Crypto.MAC.HMAC (HMAC, hmac)
38+import Crypto.MAC.HMAC (HMAC, hmac)
38−import Data.ByteArray (ByteArrayAccess, convert)
39+import Data.ByteArray (ByteArrayAccess, convert)
39−import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
40+import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
40−import qualified Data.Conduit as C
41+import qualified Data.Conduit as C
41−
42−import Lib.Prelude
43
42
44hashSHA256 :: ByteString -> ByteString
43hashSHA256 :: ByteString -> ByteString
45hashSHA256 = digestToBase16 . hashWith SHA256
44hashSHA256 = digestToBase16 . hashWith SHA256
46
45
47−hashSHA256FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
46+hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
48hashSHA256FromSource src = do
47hashSHA256FromSource src = do
49 digest <- C.connect src sinkSHA256Hash
48 digest <- C.connect src sinkSHA256Hash
50 return $ digestToBase16 digest
49 return $ digestToBase16 digest
51 where
50 where
52 -- To help with type inference
51 -- To help with type inference
53− sinkSHA256Hash :: Monad m => C.ConduitM ByteString Void m (Digest SHA256)
52+ sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
54 sinkSHA256Hash = sinkHash
53 sinkSHA256Hash = sinkHash
55
54
56-- Returns MD5 hash hex encoded.
55-- Returns MD5 hash hex encoded.
57hashMD5 :: ByteString -> ByteString
56hashMD5 :: ByteString -> ByteString
58hashMD5 = digestToBase16 . hashWith MD5
57hashMD5 = digestToBase16 . hashWith MD5
59
58
60−hashMD5FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
59+hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
61hashMD5FromSource src = do
60hashMD5FromSource src = do
62 digest <- C.connect src sinkMD5Hash
61 digest <- C.connect src sinkMD5Hash
63 return $ digestToBase16 digest
62 return $ digestToBase16 digest
64 where
63 where
65 -- To help with type inference
64 -- To help with type inference
66− sinkMD5Hash :: Monad m => C.ConduitM ByteString Void m (Digest MD5)
65+ sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
67 sinkMD5Hash = sinkHash
66 sinkMD5Hash = sinkHash
68
67
69hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
68hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -72,15 +71,15 @@ hmacSHA256 message key = hmac key message
72hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
71hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
73hmacSHA256RawBS message key = convert $ hmacSHA256 message key
72hmacSHA256RawBS message key = convert $ hmacSHA256 message key
74
73
75−digestToBS :: ByteArrayAccess a => a -> ByteString
74+digestToBS :: (ByteArrayAccess a) => a -> ByteString
76digestToBS = convert
75digestToBS = convert
77
76
78−digestToBase16 :: ByteArrayAccess a => a -> ByteString
77+digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
79digestToBase16 = convertToBase Base16
78digestToBase16 = convertToBase Base16
80
79
81-- Returns MD5 hash base 64 encoded.
80-- Returns MD5 hash base 64 encoded.
82−hashMD5ToBase64 :: ByteArrayAccess a => a -> ByteString
81+hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
83hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
82hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
84
83
85−encodeToBase64 :: ByteArrayAccess a => a -> ByteString
84+encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
86encodeToBase64 = convertToBase Base64
85encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+17−13
@@ -15,20 +15,24 @@
15--
15--
16
16
17module Network.Minio.Data.Time
17module Network.Minio.Data.Time
18− (
19− awsTimeFormat
18+ ( awsTimeFormat,
20− , awsTimeFormatBS
19+ awsTimeFormatBS,
21− , awsDateFormat
20+ awsDateFormat,
22− , awsDateFormatBS
21+ awsDateFormatBS,
23− , awsParseTime
22+ awsParseTime,
24− , iso8601TimeFormat
23+ iso8601TimeFormat,
24+ UrlExpiry,
25+ )
25− ) where
26+where
26−
27+
27−
28−import Data.ByteString.Char8 (pack)
28+import Data.ByteString.Char8 (pack)
29import qualified Data.Time as Time
29import qualified Data.Time as Time
30+import Data.Time.Format.ISO8601 (iso8601Show)
31+import Lib.Prelude
30
32
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
32
36
33awsTimeFormat :: UTCTime -> [Char]
37awsTimeFormat :: UTCTime -> [Char]
34awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
38awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
@@ -46,4 +50,4 @@ awsParseTime :: [Char] -> Maybe UTCTime
46awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
50awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
47
51
48iso8601TimeFormat :: UTCTime -> [Char]
52iso8601TimeFormat :: UTCTime -> [Char]
49−iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
53+iso8601TimeFormat = iso8601Show
src/Network/Minio/Errors.hs+59−52
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,76 +14,83 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17−module Network.Minio.Errors where
17+module Network.Minio.Errors
18+ ( MErrV (..),
19+ ServiceErr (..),
20+ MinioErr (..),
21+ toServiceErr,
22+ )
23+where
18
24
19−import Control.Exception
25+import Control.Exception (IOException)
20import qualified Network.HTTP.Conduit as NC
26import qualified Network.HTTP.Conduit as NC
21
27
22−import Lib.Prelude
23−
24−
25---------------------------------
28---------------------------------
26-- Errors
29-- Errors
27---------------------------------
30---------------------------------
31+
28-- | Various validation errors
32-- | Various validation errors
33+data MErrV
29−data MErrV = MErrVSinglePUTSizeExceeded Int64
34+ = MErrVSinglePUTSizeExceeded Int64
30− | MErrVPutSizeExceeded Int64
35+ | MErrVPutSizeExceeded Int64
31− | MErrVETagHeaderNotFound
36+ | MErrVETagHeaderNotFound
32− | MErrVInvalidObjectInfoResponse
37+ | MErrVInvalidObjectInfoResponse
33− | MErrVInvalidSrcObjSpec Text
38+ | MErrVInvalidSrcObjSpec Text
34− | MErrVInvalidSrcObjByteRange (Int64, Int64)
39+ | MErrVInvalidSrcObjByteRange (Int64, Int64)
35− | MErrVCopyObjSingleNoRangeAccepted
40+ | MErrVCopyObjSingleNoRangeAccepted
36− | MErrVRegionNotSupported Text
41+ | MErrVRegionNotSupported Text
37− | MErrVXmlParse Text
42+ | MErrVXmlParse Text
38− | MErrVInvalidBucketName Text
43+ | MErrVInvalidBucketName Text
39− | MErrVInvalidObjectName Text
44+ | MErrVInvalidObjectName Text
40− | MErrVInvalidUrlExpiry Int
45+ | MErrVInvalidUrlExpiry Int
41− | MErrVJsonParse Text
46+ | MErrVJsonParse Text
42− | MErrVInvalidHealPath
47+ | MErrVInvalidHealPath
43− | MErrVMissingCredentials
48+ | MErrVMissingCredentials
44− | MErrVInvalidEncryptionKeyLength
49+ | MErrVInvalidEncryptionKeyLength
45− | MErrVStreamingBodyUnexpectedEOF
50+ | MErrVStreamingBodyUnexpectedEOF
46− | MErrVUnexpectedPayload
51+ | MErrVUnexpectedPayload
52+ | MErrVSTSEndpointNotFound
47− deriving (Show, Eq)
53+ deriving stock (Show, Eq)
48
54
49instance Exception MErrV
55instance Exception MErrV
50
56
51-- | Errors returned by S3 compatible service
57-- | Errors returned by S3 compatible service
58+data ServiceErr
52−data ServiceErr = BucketAlreadyExists
59+ = BucketAlreadyExists
53− | BucketAlreadyOwnedByYou
60+ | BucketAlreadyOwnedByYou
54− | NoSuchBucket
61+ | NoSuchBucket
55− | InvalidBucketName
62+ | InvalidBucketName
56− | NoSuchKey
63+ | NoSuchKey
57− | SelectErr Text Text
64+ | SelectErr Text Text
58− | ServiceErr Text Text
65+ | ServiceErr Text Text
59− deriving (Show, Eq)
66+ deriving stock (Show, Eq)
60
67
61instance Exception ServiceErr
68instance Exception ServiceErr
62
69
63toServiceErr :: Text -> Text -> ServiceErr
70toServiceErr :: Text -> Text -> ServiceErr
64−toServiceErr "NoSuchKey" _ = NoSuchKey
71+toServiceErr "NoSuchKey" _ = NoSuchKey
65−toServiceErr "NoSuchBucket" _ = NoSuchBucket
72+toServiceErr "NoSuchBucket" _ = NoSuchBucket
66−toServiceErr "InvalidBucketName" _ = InvalidBucketName
73+toServiceErr "InvalidBucketName" _ = InvalidBucketName
67toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
74toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
68−toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
75+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
69−toServiceErr code message = ServiceErr code message
76+toServiceErr code message = ServiceErr code message
70−
71
77
72-- | Errors thrown by the library
78-- | Errors thrown by the library
79+data MinioErr
73−data MinioErr = MErrHTTP NC.HttpException
80+ = MErrHTTP NC.HttpException
74− | MErrIO IOException
81+ | MErrIO IOException
75− | MErrService ServiceErr
82+ | MErrService ServiceErr
76− | MErrValidation MErrV
83+ | MErrValidation MErrV
77− deriving (Show)
84+ deriving stock (Show)
78
85
79instance Eq MinioErr where
86instance Eq MinioErr where
80− MErrHTTP _ == MErrHTTP _ = True
87+ MErrHTTP _ == MErrHTTP _ = True
81− MErrHTTP _ == _ = False
88+ MErrHTTP _ == _ = False
82− MErrIO _ == MErrIO _ = True
89+ MErrIO _ == MErrIO _ = True
83− MErrIO _ == _ = False
90+ MErrIO _ == _ = False
84− MErrService a == MErrService b = a == b
91+ MErrService a == MErrService b = a == b
85− MErrService _ == _ = False
92+ MErrService _ == _ = False
86− MErrValidation a == MErrValidation b = a == b
93+ MErrValidation a == MErrValidation b = a == b
87− MErrValidation _ == _ = False
94+ MErrValidation _ == _ = False
88
95
89instance Exception MinioErr
96instance Exception MinioErr
src/Network/Minio/JsonParser.hs+23−16
@@ -15,28 +15,35 @@
15--
15--
16
16
17module Network.Minio.JsonParser
17module Network.Minio.JsonParser
18− (
19− parseErrResponseJSON
18+ ( parseErrResponseJSON,
19+ )
20− ) where
20+where
21
21
22−import Data.Aeson (FromJSON, eitherDecode, parseJSON,
22+import Data.Aeson
23+ ( FromJSON,
24+ eitherDecode,
25+ parseJSON,
23− withObject, (.:))
26+ withObject,
27+ (.:),
28+ )
24−import qualified Data.Text as T
29+import qualified Data.Text as T
30+import Lib.Prelude
31+import Network.Minio.Errors
25
32
26−import Lib.Prelude
33+data AdminErrJSON = AdminErrJSON
34+ { aeCode :: Text,
35+ aeMessage :: Text
36+ }
37+ deriving stock (Eq, Show)
27
38
28−import Network.Minio.Errors
29−
30−data AdminErrJSON = AdminErrJSON { aeCode :: Text
31− , aeMessage :: Text
32− } deriving (Eq, Show)
33instance FromJSON AdminErrJSON where
39instance FromJSON AdminErrJSON where
34− parseJSON = withObject "AdminErrJSON" $ \v -> AdminErrJSON
40+ parseJSON = withObject "AdminErrJSON" $ \v ->
41+ AdminErrJSON
35− <$> v .: "Code"
42+ <$> v .: "Code"
36− <*> v .: "Message"
43+ <*> v .: "Message"
37
44
38parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
45parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
39parseErrResponseJSON jsondata =
46parseErrResponseJSON jsondata =
40 case eitherDecode jsondata of
47 case eitherDecode jsondata of
41 Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
48 Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
42− Left err -> throwIO $ MErrVJsonParse $ T.pack err
49+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs+98−38
@@ -16,20 +16,50 @@
16
16
17module Network.Minio.ListOps where
17module Network.Minio.ListOps where
18
18
19−import qualified Data.Conduit as C
19+import qualified Data.Conduit as C
20import qualified Data.Conduit.Combinators as CC
20import qualified Data.Conduit.Combinators as CC
21−import qualified Data.Conduit.List as CL
21+import qualified Data.Conduit.List as CL
22−
23−import Lib.Prelude
24−
25−import Network.Minio.Data
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+ )
26−import Network.Minio.S3API
50+import Network.Minio.S3API
51+ ( listIncompleteParts',
52+ listIncompleteUploads',
53+ listObjects',
54+ listObjectsV1',
55+ )
27
56
28-- | Represents a list output item - either an object or an object
57-- | Represents a list output item - either an object or an object
29-- prefix (i.e. a directory).
58-- prefix (i.e. a directory).
59+data ListItem
30−data ListItem = ListItemObject ObjectInfo
60+ = ListItemObject ObjectInfo
31− | ListItemPrefix Text
61+ | ListItemPrefix Text
32− deriving (Show, Eq)
62+ deriving stock (Show, Eq)
33
63
34-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
64-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
35-- similar to a file system tree traversal.
65-- similar to a file system tree traversal.
@@ -48,73 +78,103 @@ listObjects bucket prefix recurse = loop Nothing
48 where
78 where
49 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
79 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
50 loop nextToken = do
80 loop nextToken = do
51− let
52− delimiter = bool (Just "/") Nothing recurse
81+ let delimiter = bool (Just "/") Nothing recurse
53
82
54 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
83 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
55 CL.sourceList $ map ListItemObject $ lorObjects res
84 CL.sourceList $ map ListItemObject $ lorObjects res
56 unless recurse $
85 unless recurse $
86+ CL.sourceList $
57− CL.sourceList $ map ListItemPrefix $ lorCPrefixes res
87+ map ListItemPrefix $
88+ lorCPrefixes res
58 when (lorHasMore res) $
89 when (lorHasMore res) $
59 loop (lorNextToken res)
90 loop (lorNextToken res)
60
91
61-- | Lists objects - similar to @listObjects@, however uses the older
92-- | Lists objects - similar to @listObjects@, however uses the older
62-- V1 AWS S3 API. Prefer @listObjects@ to this.
93-- V1 AWS S3 API. Prefer @listObjects@ to this.
63−listObjectsV1 :: Bucket -> Maybe Text -> Bool
94+listObjectsV1 ::
95+ Bucket ->
96+ Maybe Text ->
97+ Bool ->
64− -> C.ConduitM () ListItem Minio ()
98+ C.ConduitM () ListItem Minio ()
65listObjectsV1 bucket prefix recurse = loop Nothing
99listObjectsV1 bucket prefix recurse = loop Nothing
66 where
100 where
67 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
101 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
68 loop nextMarker = do
102 loop nextMarker = do
69− let
70− delimiter = bool (Just "/") Nothing recurse
103+ let delimiter = bool (Just "/") Nothing recurse
71
104
72 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
105 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
73 CL.sourceList $ map ListItemObject $ lorObjects' res
106 CL.sourceList $ map ListItemObject $ lorObjects' res
74 unless recurse $
107 unless recurse $
108+ CL.sourceList $
75− CL.sourceList $ map ListItemPrefix $ lorCPrefixes' res
109+ map ListItemPrefix $
110+ lorCPrefixes' res
76 when (lorHasMore' res) $
111 when (lorHasMore' res) $
77 loop (lorNextMarker res)
112 loop (lorNextMarker res)
78
113
79-- | List incomplete uploads in a bucket matching the given prefix. If
114-- | List incomplete uploads in a bucket matching the given prefix. If
80-- recurse is set to True incomplete uploads for the given prefix are
115-- recurse is set to True incomplete uploads for the given prefix are
81-- recursively listed.
116-- recursively listed.
82−listIncompleteUploads :: Bucket -> Maybe Text -> Bool
117+listIncompleteUploads ::
118+ Bucket ->
119+ Maybe Text ->
120+ Bool ->
83− -> C.ConduitM () UploadInfo Minio ()
121+ C.ConduitM () UploadInfo Minio ()
84listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
122listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
85 where
123 where
86 loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
124 loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
87 loop nextKeyMarker nextUploadIdMarker = do
125 loop nextKeyMarker nextUploadIdMarker = do
88− let
89− delimiter = bool (Just "/") Nothing recurse
126+ let delimiter = bool (Just "/") Nothing recurse
90−
127+
128+ res <-
129+ lift $
91− res <- lift $ listIncompleteUploads' bucket prefix delimiter
130+ listIncompleteUploads'
131+ bucket
132+ prefix
133+ delimiter
134+ nextKeyMarker
92− nextKeyMarker nextUploadIdMarker Nothing
135+ nextUploadIdMarker
93−
136+ Nothing
137+
138+ aggrSizes <- lift $
94− aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do
139+ forM (lurUploads res) $ \(uKey, uId, _) -> do
140+ partInfos <-
141+ C.runConduit $
95− partInfos <- C.runConduit $ listIncompleteParts bucket uKey uId
142+ listIncompleteParts bucket uKey uId
96− C..| CC.sinkList
143+ C..| CC.sinkList
97− return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
144+ return $ foldl' (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
98
145
99 CL.sourceList $
146 CL.sourceList $
147+ zipWith
148+ ( curry
100− map (\((uKey, uId, uInitTime), size) ->
149+ ( \((uKey, uId, uInitTime), size) ->
101− UploadInfo uKey uId uInitTime size
150+ UploadInfo uKey uId uInitTime size
151+ )
152+ )
102− ) $ zip (lurUploads res) aggrSizes
153+ (lurUploads res)
154+ aggrSizes
103
155
104 when (lurHasMore res) $
156 when (lurHasMore res) $
105 loop (lurNextKey res) (lurNextUpload res)
157 loop (lurNextKey res) (lurNextUpload res)
106
158
107−
108-- | List object parts of an ongoing multipart upload for given
159-- | List object parts of an ongoing multipart upload for given
109-- bucket, object and uploadId.
160-- bucket, object and uploadId.
110−listIncompleteParts :: Bucket -> Object -> UploadId
161+listIncompleteParts ::
162+ Bucket ->
163+ Object ->
164+ UploadId ->
111− -> C.ConduitM () ObjectPartInfo Minio ()
165+ C.ConduitM () ObjectPartInfo Minio ()
112listIncompleteParts bucket object uploadId = loop Nothing
166listIncompleteParts bucket object uploadId = loop Nothing
113 where
167 where
114 loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
168 loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
115 loop nextPartMarker = do
169 loop nextPartMarker = do
170+ res <-
171+ lift $
116− res <- lift $ listIncompleteParts' bucket object uploadId Nothing
172+ listIncompleteParts'
173+ bucket
174+ object
175+ uploadId
176+ Nothing
117− nextPartMarker
177+ nextPartMarker
118 CL.sourceList $ lprParts res
178 CL.sourceList $ lprParts res
119 when (lprHasMore res) $
179 when (lprHasMore res) $
120 loop (show <$> lprNextPart res)
180 loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs+215−152
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,45 +13,51 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16+{-# LANGUAGE CPP #-}
16
17
17module Network.Minio.PresignedOperations
18module Network.Minio.PresignedOperations
18− ( UrlExpiry
19+ ( UrlExpiry,
19− , makePresignedUrl
20+ makePresignedUrl,
20− , presignedPutObjectUrl
21+ presignedPutObjectUrl,
21− , presignedGetObjectUrl
22+ presignedGetObjectUrl,
22− , presignedHeadObjectUrl
23+ presignedHeadObjectUrl,
23−
24− , PostPolicyCondition(..)
24+ PostPolicyCondition (..),
25− , ppCondBucket
25+ ppCondBucket,
26− , ppCondContentLengthRange
26+ ppCondContentLengthRange,
27− , ppCondContentType
27+ ppCondContentType,
28− , ppCondKey
28+ ppCondKey,
29− , ppCondKeyStartsWith
29+ ppCondKeyStartsWith,
30− , ppCondSuccessActionStatus
30+ ppCondSuccessActionStatus,
31−
32− , PostPolicy(..)
31+ PostPolicy (..),
33− , PostPolicyError(..)
32+ PostPolicyError (..),
34− , newPostPolicy
33+ newPostPolicy,
35− , showPostPolicy
34+ showPostPolicy,
36− , presignedPostPolicy
35+ presignedPostPolicy,
36+ )
37− ) where
37+where
38−
38+
39−import Data.Aeson ((.=))
39+import Data.Aeson ((.=))
40−import qualified Data.Aeson as Json
40+import qualified Data.Aeson as Json
41−import Data.ByteString.Builder (byteString, toLazyByteString)
41+import Data.ByteString.Builder (byteString, toLazyByteString)
42−import qualified Data.HashMap.Strict as H
42+import qualified Data.HashMap.Strict as H
43−import qualified Data.Text as T
43+import qualified Data.Text as T
44−import qualified Data.Time as Time
44+import qualified Data.Time as Time
45−import qualified Network.HTTP.Conduit as NC
46−import qualified Network.HTTP.Types as HT
47−import Network.HTTP.Types.Header (hHost)
48−
49−import Lib.Prelude
45+import Lib.Prelude
50−
46+import qualified Network.HTTP.Client as NClient
51−import Network.Minio.Data
47+import qualified Network.HTTP.Types as HT
48+import Network.Minio.API (buildRequest)
49+import Network.Minio.Credentials
50+import Network.Minio.Data
52−import Network.Minio.Data.Time
51+import Network.Minio.Data.Time
53−import Network.Minio.Errors
52+import Network.Minio.Errors
54−import Network.Minio.Sign.V4
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 -}
55
61
56-- | Generate a presigned URL. This function allows for advanced usage
62-- | Generate a presigned URL. This function allows for advanced usage
57-- - for simple cases prefer the `presigned*Url` functions.
63-- - for simple cases prefer the `presigned*Url` functions.
@@ -61,42 +67,36 @@ import Network.Minio.Sign.V4
61--
67--
62-- All extra query parameters or headers are signed, and therefore are
68-- All extra query parameters or headers are signed, and therefore are
63-- required to be sent when the generated URL is actually used.
69-- required to be sent when the generated URL is actually used.
64−makePresignedUrl :: UrlExpiry -> HT.Method -> Maybe Bucket -> Maybe Object
70+makePresignedUrl ::
71+ UrlExpiry ->
72+ HT.Method ->
73+ Maybe Bucket ->
74+ Maybe Object ->
75+ Maybe Region ->
76+ HT.Query ->
65− -> Maybe Region -> HT.Query -> HT.RequestHeaders
77+ HT.RequestHeaders ->
66− -> Minio ByteString
78+ Minio ByteString
67makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
79makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
68− when (expiry > 7*24*3600 || expiry < 0) $
80+ when (expiry > 7 * 24 * 3600 || expiry < 0) $
81+ throwIO $
69− throwIO $ MErrVInvalidUrlExpiry expiry
82+ MErrVInvalidUrlExpiry expiry
70−
83+
71− ci <- asks mcConnInfo
72−
73− let
84+ let s3ri =
74− hostHeader = (hHost, getHostAddr ci)
85+ defaultS3ReqInfo
75− req = NC.defaultRequest {
86+ { riPresignExpirySecs = Just expiry,
76− NC.method = method
87+ riMethod = method,
77− , NC.secure = connectIsSecure ci
88+ riBucket = bucket,
78− , NC.host = encodeUtf8 $ connectHost ci
89+ riObject = object,
79− , NC.port = connectPort ci
90+ riRegion = region,
80− , NC.path = getS3Path bucket object
91+ riQueryParams = extraQuery,
81− , NC.requestHeaders = hostHeader : extraHeaders
92+ riHeaders = extraHeaders
82− , NC.queryString = HT.renderQuery True extraQuery
83− }
93+ }
84− ts <- liftIO Time.getCurrentTime
94+
85−
95+ req <- buildRequest s3ri
86− let sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
96+ let uri = NClient.getUri req
87− ts region (Just expiry) Nothing
97+ uriString = uriToString identity uri ""
88−
98+
89− signPairs = signV4 sp req
99+ return $ encodeUtf8 uriString
90−
91− qpToAdd = (fmap . fmap) Just signPairs
92− queryStr = HT.renderQueryBuilder True
93− ((HT.parseQuery $ NC.queryString req) ++ qpToAdd)
94− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
95−
96− return $ toS $ toLazyByteString $ scheme
97− <> byteString (getHostAddr ci)
98− <> byteString (getS3Path bucket object)
99− <> queryStr
100
100
101-- | Generate a URL with authentication signature to PUT (upload) an
101-- | Generate a URL with authentication signature to PUT (upload) an
102-- object. Any extra headers if passed, are signed, and so they are
102-- object. Any extra headers if passed, are signed, and so they are
@@ -105,12 +105,22 @@ makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
105--
105--
106-- For a list of possible headers to pass, please refer to the PUT
106-- For a list of possible headers to pass, please refer to the PUT
107-- object REST API AWS S3 documentation.
107-- object REST API AWS S3 documentation.
108−presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders
108+presignedPutObjectUrl ::
109+ Bucket ->
110+ Object ->
111+ UrlExpiry ->
112+ HT.RequestHeaders ->
109− -> Minio ByteString
113+ Minio ByteString
110presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
114presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
111 region <- asks (Just . connectRegion . mcConnInfo)
115 region <- asks (Just . connectRegion . mcConnInfo)
112− makePresignedUrl expirySeconds HT.methodPut
116+ makePresignedUrl
117+ expirySeconds
118+ HT.methodPut
119+ (Just bucket)
120+ (Just object)
121+ region
122+ []
113− (Just bucket) (Just object) region [] extraHeaders
123+ extraHeaders
114
124
115-- | Generate a URL with authentication signature to GET (download) an
125-- | Generate a URL with authentication signature to GET (download) an
116-- object. All extra query parameters and headers passed here will be
126-- object. All extra query parameters and headers passed here will be
@@ -121,12 +131,23 @@ presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
121--
131--
122-- For a list of possible request parameters and headers, please refer
132-- For a list of possible request parameters and headers, please refer
123-- to the GET object REST API AWS S3 documentation.
133-- to the GET object REST API AWS S3 documentation.
124−presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query
134+presignedGetObjectUrl ::
135+ Bucket ->
136+ Object ->
137+ UrlExpiry ->
138+ HT.Query ->
125− -> HT.RequestHeaders -> Minio ByteString
139+ HT.RequestHeaders ->
140+ Minio ByteString
126presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
141presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
127 region <- asks (Just . connectRegion . mcConnInfo)
142 region <- asks (Just . connectRegion . mcConnInfo)
128− makePresignedUrl expirySeconds HT.methodGet
143+ makePresignedUrl
144+ expirySeconds
145+ HT.methodGet
146+ (Just bucket)
147+ (Just object)
148+ region
149+ extraQuery
129− (Just bucket) (Just object) region extraQuery extraHeaders
150+ extraHeaders
130
151
131-- | Generate a URL with authentication signature to make a HEAD
152-- | Generate a URL with authentication signature to make a HEAD
132-- request on an object. This is used to fetch metadata about an
153-- request on an object. This is used to fetch metadata about an
@@ -135,50 +156,74 @@ presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
135--
156--
136-- For a list of possible headers to pass, please refer to the HEAD
157-- For a list of possible headers to pass, please refer to the HEAD
137-- object REST API AWS S3 documentation.
158-- object REST API AWS S3 documentation.
138−presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry
159+presignedHeadObjectUrl ::
160+ Bucket ->
161+ Object ->
162+ UrlExpiry ->
139− -> HT.RequestHeaders -> Minio ByteString
163+ HT.RequestHeaders ->
164+ Minio ByteString
140presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
165presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
141 region <- asks (Just . connectRegion . mcConnInfo)
166 region <- asks (Just . connectRegion . mcConnInfo)
142− makePresignedUrl expirySeconds HT.methodHead
167+ makePresignedUrl
168+ expirySeconds
169+ HT.methodHead
170+ (Just bucket)
171+ (Just object)
172+ region
173+ []
143− (Just bucket) (Just object) region [] extraHeaders
174+ extraHeaders
144
175
145-- | Represents individual conditions in a Post Policy document.
176-- | Represents individual conditions in a Post Policy document.
146−data PostPolicyCondition = PPCStartsWith Text Text
177+data PostPolicyCondition
178+ = PPCStartsWith Text Text
147− | PPCEquals Text Text
179+ | PPCEquals Text Text
148− | PPCRange Text Int64 Int64
180+ | PPCRange Text Int64 Int64
149− deriving (Show, Eq)
181+ deriving stock (Show, Eq)
150
182
183+{- ORMOLU_DISABLE -}
151instance Json.ToJSON PostPolicyCondition where
184instance Json.ToJSON PostPolicyCondition where
152 toJSON (PPCStartsWith k v) = Json.toJSON ["starts-with", k, v]
185 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
153 toJSON (PPCEquals k v) = Json.object [k .= v]
189 toJSON (PPCEquals k v) = Json.object [k .= v]
190+#endif
154 toJSON (PPCRange k minVal maxVal) =
191 toJSON (PPCRange k minVal maxVal) =
155 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
192 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
156
193
157 toEncoding (PPCStartsWith k v) = Json.foldable ["starts-with", k, v]
194 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
158 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
198 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
199+#endif
159 toEncoding (PPCRange k minVal maxVal) =
200 toEncoding (PPCRange k minVal maxVal) =
160 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
201 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202+{- ORMOLU_ENABLE -}
161
203
162-- | A PostPolicy is required to perform uploads via browser forms.
204-- | A PostPolicy is required to perform uploads via browser forms.
163−data PostPolicy = PostPolicy {
205+data PostPolicy = PostPolicy
164− expiration :: UTCTime
206+ { expiration :: UTCTime,
165− , conditions :: [PostPolicyCondition]
207+ conditions :: [PostPolicyCondition]
208+ }
166− } deriving (Show, Eq)
209+ deriving stock (Show, Eq)
167
210
168instance Json.ToJSON PostPolicy where
211instance Json.ToJSON PostPolicy where
169 toJSON (PostPolicy e c) =
212 toJSON (PostPolicy e c) =
213+ Json.object
170− Json.object $ [ "expiration" .= iso8601TimeFormat e
214+ [ "expiration" .= iso8601TimeFormat e,
171− , "conditions" .= c
215+ "conditions" .= c
172− ]
216+ ]
173 toEncoding (PostPolicy e c) =
217 toEncoding (PostPolicy e c) =
174 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
218 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
175
219
176-- | Possible validation errors when creating a PostPolicy.
220-- | Possible validation errors when creating a PostPolicy.
177−data PostPolicyError = PPEKeyNotSpecified
221+data PostPolicyError
222+ = PPEKeyNotSpecified
178− | PPEBucketNotSpecified
223+ | PPEBucketNotSpecified
179− | PPEConditionKeyEmpty
224+ | PPEConditionKeyEmpty
180− | PPERangeInvalid
225+ | PPERangeInvalid
181− deriving (Eq, Show)
226+ deriving stock (Show, Eq)
182
227
183-- | Set the bucket name that the upload should use.
228-- | Set the bucket name that the upload should use.
184ppCondBucket :: Bucket -> PostPolicyCondition
229ppCondBucket :: Bucket -> PostPolicyCondition
@@ -186,8 +231,10 @@ ppCondBucket = PPCEquals "bucket"
186
231
187-- | Set the content length range constraint with minimum and maximum
232-- | Set the content length range constraint with minimum and maximum
188-- byte count values.
233-- byte count values.
189−ppCondContentLengthRange :: Int64 -> Int64
234+ppCondContentLengthRange ::
235+ Int64 ->
236+ Int64 ->
190− -> PostPolicyCondition
237+ PostPolicyCondition
191ppCondContentLengthRange = PPCRange "content-length-range"
238ppCondContentLengthRange = PPCRange "content-length-range"
192
239
193-- | Set the content-type header for the upload.
240-- | Set the content-type header for the upload.
@@ -210,83 +257,99 @@ ppCondSuccessActionStatus n =
210
257
211-- | This function creates a PostPolicy after validating its
258-- | This function creates a PostPolicy after validating its
212-- arguments.
259-- arguments.
260+newPostPolicy ::
261+ UTCTime ->
213−newPostPolicy :: UTCTime -> [PostPolicyCondition]
262+ [PostPolicyCondition] ->
214− -> Either PostPolicyError PostPolicy
263+ Either PostPolicyError PostPolicy
215newPostPolicy expirationTime conds
264newPostPolicy expirationTime conds
216 -- object name condition must be present
265 -- object name condition must be present
217 | not $ any (keyEquals "key") conds =
266 | not $ any (keyEquals "key") conds =
218 Left PPEKeyNotSpecified
267 Left PPEKeyNotSpecified
219−
220 -- bucket name condition must be present
268 -- bucket name condition must be present
221 | not $ any (keyEquals "bucket") conds =
269 | not $ any (keyEquals "bucket") conds =
222 Left PPEBucketNotSpecified
270 Left PPEBucketNotSpecified
223−
224 -- a condition with an empty key is invalid
271 -- a condition with an empty key is invalid
225 | any (keyEquals "") conds || any isEmptyRangeKey conds =
272 | any (keyEquals "") conds || any isEmptyRangeKey conds =
226 Left PPEConditionKeyEmpty
273 Left PPEConditionKeyEmpty
227−
228 -- invalid range check
274 -- invalid range check
229 | any isInvalidRange conds =
275 | any isInvalidRange conds =
230 Left PPERangeInvalid
276 Left PPERangeInvalid
231−
232 -- all good!
277 -- all good!
233 | otherwise =
278 | otherwise =
234 return $ PostPolicy expirationTime conds
279 return $ PostPolicy expirationTime conds
235−
236 where
280 where
237 keyEquals k' (PPCStartsWith k _) = k == k'
281 keyEquals k' (PPCStartsWith k _) = k == k'
238− keyEquals k' (PPCEquals k _) = k == k'
282+ keyEquals k' (PPCEquals k _) = k == k'
239− keyEquals _ _ = False
283+ keyEquals _ _ = False
240−
241 isEmptyRangeKey (PPCRange k _ _) = k == ""
284 isEmptyRangeKey (PPCRange k _ _) = k == ""
242− isEmptyRangeKey _ = False
285+ isEmptyRangeKey _ = False
243−
244 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
286 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
245− isInvalidRange _ = False
287+ isInvalidRange _ = False
246
288
247-- | Convert Post Policy to a string (e.g. for printing).
289-- | Convert Post Policy to a string (e.g. for printing).
248showPostPolicy :: PostPolicy -> ByteString
290showPostPolicy :: PostPolicy -> ByteString
249−showPostPolicy = toS . Json.encode
291+showPostPolicy = toStrictBS . Json.encode
250
292
251-- | Generate a presigned URL and POST policy to upload files via a
293-- | Generate a presigned URL and POST policy to upload files via a
252-- browser. On success, this function returns a URL and POST
294-- browser. On success, this function returns a URL and POST
253-- form-data.
295-- form-data.
254−presignedPostPolicy :: PostPolicy
296+presignedPostPolicy ::
297+ PostPolicy ->
255− -> Minio (ByteString, H.HashMap Text ByteString)
298+ Minio (ByteString, H.HashMap Text ByteString)
256presignedPostPolicy p = do
299presignedPostPolicy p = do
257 ci <- asks mcConnInfo
300 ci <- asks mcConnInfo
258− signTime <- liftIO $ Time.getCurrentTime
301+ signTime <- liftIO Time.getCurrentTime
259−
302+ mgr <- asks mcConnManager
260− let
303+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
304+
261− extraConditions =
305+ let extraConditions signParams =
262− [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime)
306+ [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
263− , PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256"
307+ PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
308+ PPCEquals
264− , PPCEquals "x-amz-credential"
309+ "x-amz-credential"
265− (T.intercalate "/" [connectAccessKey ci,
310+ ( T.intercalate
266− decodeUtf8 $ mkScope signTime region])
311+ "/"
312+ [ coerce $ cvAccessKey cv,
313+ decodeUtf8 $ credentialScope signParams
314+ ]
315+ )
267− ]
316+ ]
268− ppWithCreds = p {
317+ ppWithCreds signParams =
318+ p
269− conditions = conditions p ++ extraConditions
319+ { conditions = conditions p ++ extraConditions signParams
270− }
320+ }
271− sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
321+ sp =
322+ SignParams
323+ (coerce $ cvAccessKey cv)
324+ (coerce $ cvSecretKey cv)
325+ (coerce $ cvSessionToken cv)
326+ ServiceS3
327+ signTime
272− signTime (Just $ connectRegion ci) Nothing Nothing
328+ (Just $ connectRegion ci)
329+ Nothing
330+ Nothing
273− signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
331+ signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
274−
275−
276− -- compute form-data
332+ -- compute form-data
277− mkPair (PPCStartsWith k v) = Just (k, v)
333+ mkPair (PPCStartsWith k v) = Just (k, v)
278− mkPair (PPCEquals k v) = Just (k, v)
334+ mkPair (PPCEquals k v) = Just (k, v)
279− mkPair _ = Nothing
335+ mkPair _ = Nothing
280− formFromPolicy = H.map toS $ H.fromList $ catMaybes $
336+ formFromPolicy =
337+ H.map encodeUtf8 $
338+ H.fromList $
339+ mapMaybe
340+ mkPair
281− mkPair <$> conditions ppWithCreds
341+ (conditions $ ppWithCreds sp)
282− formData = formFromPolicy `H.union` signData
342+ formData = formFromPolicy `H.union` signData
283−
284− -- compute POST upload URL
343+ -- compute POST upload URL
285− bucket = H.lookupDefault "" "bucket" formData
344+ bucket = H.lookupDefault "" "bucket" formData
286− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
345+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
287− region = connectRegion ci
346+ url =
288−
347+ toStrictBS $
348+ toLazyByteString $
349+ scheme
289− url = toS $ toLazyByteString $ scheme <> byteString (getHostAddr ci) <>
350+ <> byteString (getHostAddr ci)
351+ <> byteString "/"
290− byteString "/" <> byteString (toS bucket) <> byteString "/"
352+ <> byteString bucket
353+ <> byteString "/"
291
354
292 return (url, formData)
355 return (url, formData)
src/Network/Minio/PutObject.hs+85−69
@@ -15,29 +15,24 @@
15--
15--
16
16
17module Network.Minio.PutObject
17module Network.Minio.PutObject
18− (
19− putObjectInternal
18+ ( putObjectInternal,
20− , ObjectData(..)
19+ ObjectData (..),
21− , selectPartSizes
20+ selectPartSizes,
21+ )
22− ) where
22+where
23−
23+
24−
25−import Conduit (takeC)
24+import Conduit (takeC)
26−import qualified Conduit as C
25+import qualified Conduit as C
27−import qualified Data.ByteString.Lazy as LBS
26+import qualified Data.ByteString.Lazy as LBS
28−import qualified Data.Conduit.Binary as CB
27+import qualified Data.Conduit.Binary as CB
29import qualified Data.Conduit.Combinators as CC
28import qualified Data.Conduit.Combinators as CC
30−import qualified Data.Conduit.List as CL
29+import qualified Data.Conduit.List as CL
31−import qualified Data.List as List
30+import qualified Data.List as List
32−
33−
34−import Lib.Prelude
31+import Lib.Prelude
35−
36−import Network.Minio.Data
32+import Network.Minio.Data
37−import Network.Minio.Errors
33+import Network.Minio.Errors
38−import Network.Minio.S3API
34+import Network.Minio.S3API
39−import Network.Minio.Utils
35+import Network.Minio.Utils
40−
41
36
42-- | A data-type to represent the source data for an object. A
37-- | A data-type to represent the source data for an object. A
43-- file-path or a producer-conduit may be provided.
38-- file-path or a producer-conduit may be provided.
@@ -50,37 +45,45 @@ import Network.Minio.Utils
50-- the input - if it is not provided, upload will continue until the
45-- the input - if it is not provided, upload will continue until the
51-- stream ends or the object reaches `maxObjectSize` size.
46-- stream ends or the object reaches `maxObjectSize` size.
52data ObjectData m
47data ObjectData m
53− = ODFile FilePath (Maybe Int64) -- ^ Takes filepath and optional
48+ = -- | Takes filepath and optional
54− -- size.
49+ -- size.
50+ ODFile FilePath (Maybe Int64)
51+ | -- | Pass
52+ -- size
53+ -- (bytes)
54+ -- if
55+ -- known.
55− | ODStream (C.ConduitM () ByteString m ()) (Maybe Int64) -- ^ Pass
56+ ODStream (C.ConduitM () ByteString m ()) (Maybe Int64)
56− -- size
57− -- (bytes)
58− -- if
59− -- known.
60
57
61-- | Put an object from ObjectData. This high-level API handles
58-- | Put an object from ObjectData. This high-level API handles
62-- objects of all sizes, and even if the object size is unknown.
59-- objects of all sizes, and even if the object size is unknown.
63−putObjectInternal :: Bucket -> Object -> PutObjectOptions
60+putObjectInternal ::
61+ Bucket ->
62+ Object ->
63+ PutObjectOptions ->
64− -> ObjectData Minio -> Minio ETag
64+ ObjectData Minio ->
65+ Minio ETag
65putObjectInternal b o opts (ODStream src sizeMay) = do
66putObjectInternal b o opts (ODStream src sizeMay) = do
66 case sizeMay of
67 case sizeMay of
67 -- unable to get size, so assume non-seekable file
68 -- unable to get size, so assume non-seekable file
68 Nothing -> sequentialMultipartUpload b o opts Nothing src
69 Nothing -> sequentialMultipartUpload b o opts Nothing src
69−
70 -- got file size, so check for single/multipart upload
70 -- got file size, so check for single/multipart upload
71 Just size ->
71 Just size ->
72+ if
72− if | size <= 64 * oneMiB -> do
73+ | size <= 64 * oneMiB -> do
73− bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
74+ bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
74− putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
75+ putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
75− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
76+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
76− | otherwise -> sequentialMultipartUpload b o opts (Just size) src
77+ | otherwise -> sequentialMultipartUpload b o opts (Just size) src
77−
78putObjectInternal b o opts (ODFile fp sizeMay) = do
78putObjectInternal b o opts (ODFile fp sizeMay) = do
79 hResE <- withNewHandle fp $ \h ->
79 hResE <- withNewHandle fp $ \h ->
80− liftM2 (,) (isHandleSeekable h) (getFileSize h)
80+ liftA2 (,) (isHandleSeekable h) (getFileSize h)
81
81
82− (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return
82+ (isSeekable, handleSizeMay) <-
83+ either
84+ (const $ return (False, Nothing))
85+ return
83− hResE
86+ hResE
84
87
85 -- prefer given size to queried size.
88 -- prefer given size to queried size.
86 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
89 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
@@ -88,18 +91,25 @@ putObjectInternal b o opts (ODFile fp sizeMay) = do
88 case finalSizeMay of
91 case finalSizeMay of
89 -- unable to get size, so assume non-seekable file
92 -- unable to get size, so assume non-seekable file
90 Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
93 Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
91−
92 -- got file size, so check for single/multipart upload
94 -- got file size, so check for single/multipart upload
93 Just size ->
95 Just size ->
96+ if
97+ | size <= 64 * oneMiB ->
94− if | size <= 64 * oneMiB -> either throwIO return =<<
98+ either throwIO return
95− withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
99+ =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
96− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
100+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
97− | isSeekable -> parallelMultipartUpload b o opts fp size
101+ | isSeekable -> parallelMultipartUpload b o opts fp size
102+ | otherwise ->
98− | otherwise -> sequentialMultipartUpload b o opts (Just size) $
103+ sequentialMultipartUpload b o opts (Just size) $
99− CB.sourceFile fp
104+ CB.sourceFile fp
100−
105+
101−parallelMultipartUpload :: Bucket -> Object -> PutObjectOptions
106+parallelMultipartUpload ::
107+ Bucket ->
108+ Object ->
109+ PutObjectOptions ->
110+ FilePath ->
111+ Int64 ->
102− -> FilePath -> Int64 -> Minio ETag
112+ Minio ETag
103parallelMultipartUpload b o opts filePath size = do
113parallelMultipartUpload b o opts filePath size = do
104 -- get a new upload id.
114 -- get a new upload id.
105 uploadId <- newMultipartUpload b o (pooToHeaders opts)
115 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -109,15 +119,17 @@ parallelMultipartUpload b o opts filePath size = do
109 let threads = fromMaybe 10 $ pooNumThreads opts
119 let threads = fromMaybe 10 $ pooNumThreads opts
110
120
111 -- perform upload with 'threads' threads
121 -- perform upload with 'threads' threads
122+ uploadedPartsE <-
112− uploadedPartsE <- limitedMapConcurrently (fromIntegral threads)
123+ limitedMapConcurrently
124+ (fromIntegral threads)
113− (uploadPart uploadId) partSizeInfo
125+ (uploadPart uploadId)
126+ partSizeInfo
114
127
115 -- if there were any errors, rethrow exception.
128 -- if there were any errors, rethrow exception.
116 mapM_ throwIO $ lefts uploadedPartsE
129 mapM_ throwIO $ lefts uploadedPartsE
117
130
118 -- if we get here, all parts were successfully uploaded.
131 -- if we get here, all parts were successfully uploaded.
119 completeMultipartUpload b o uploadId $ rights uploadedPartsE
132 completeMultipartUpload b o uploadId $ rights uploadedPartsE
120−
121 where
133 where
122 uploadPart uploadId (partNum, offset, sz) =
134 uploadPart uploadId (partNum, offset, sz) =
123 withNewHandle filePath $ \h -> do
135 withNewHandle filePath $ \h -> do
@@ -125,10 +137,13 @@ parallelMultipartUpload b o opts filePath size = do
125 putObjectPart b o uploadId partNum [] payload
137 putObjectPart b o uploadId partNum [] payload
126
138
127-- | Upload multipart object from conduit source sequentially
139-- | Upload multipart object from conduit source sequentially
128−sequentialMultipartUpload :: Bucket -> Object -> PutObjectOptions
140+sequentialMultipartUpload ::
141+ Bucket ->
142+ Object ->
143+ PutObjectOptions ->
129− -> Maybe Int64
144+ Maybe Int64 ->
130− -> C.ConduitM () ByteString Minio ()
145+ C.ConduitM () ByteString Minio () ->
131− -> Minio ETag
146+ Minio ETag
132sequentialMultipartUpload b o opts sizeMay src = do
147sequentialMultipartUpload b o opts sizeMay src = do
133 -- get a new upload id.
148 -- get a new upload id.
134 uploadId <- newMultipartUpload b o (pooToHeaders opts)
149 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -136,22 +151,23 @@ sequentialMultipartUpload b o opts sizeMay src = do
136 -- upload parts in loop
151 -- upload parts in loop
137 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
152 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
138 (pnums, _, sizes) = List.unzip3 partSizes
153 (pnums, _, sizes) = List.unzip3 partSizes
139− uploadedParts <- C.runConduit
154+ uploadedParts <-
155+ C.runConduit $
140− $ src
156+ src
141− C..| chunkBSConduit (map fromIntegral sizes)
157+ C..| chunkBSConduit (map fromIntegral sizes)
142− C..| CL.map PayloadBS
158+ C..| CL.map PayloadBS
143− C..| uploadPart' uploadId pnums
159+ C..| uploadPart' uploadId pnums
144− C..| CC.sinkList
160+ C..| CC.sinkList
145
161
146 -- complete multipart upload
162 -- complete multipart upload
147 completeMultipartUpload b o uploadId uploadedParts
163 completeMultipartUpload b o uploadId uploadedParts
148−
149 where
164 where
150 uploadPart' _ [] = return ()
165 uploadPart' _ [] = return ()
151− uploadPart' uid (pn:pns) = do
166+ uploadPart' uid (pn : pns) = do
152 payloadMay <- C.await
167 payloadMay <- C.await
153 case payloadMay of
168 case payloadMay of
154 Nothing -> return ()
169 Nothing -> return ()
170+ Just payload -> do
155− Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload
171+ pinfo <- lift $ putObjectPart b o uid pn [] payload
156− C.yield pinfo
172+ C.yield pinfo
157− uploadPart' uid pns
173+ uploadPart' uid pns
src/Network/Minio/S3API.hs+481−319
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,150 +14,185 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
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.
17module Network.Minio.S3API
25module Network.Minio.S3API
18− (
19− Region
26+ ( Region,
20− , getLocation
27+ getLocation,
21−
28+
22− -- * Listing buckets
29+ -- * Listing buckets
30+
23− --------------------
31+ --------------------
24− , getService
32+ getService,
25−
33+
26− -- * Listing objects
34+ -- * Listing objects
35+
27− --------------------
36+ --------------------
28− , ListObjectsResult(..)
37+ ListObjectsResult (..),
29− , ListObjectsV1Result(..)
38+ ListObjectsV1Result (..),
30− , listObjects'
39+ listObjects',
31− , listObjectsV1'
40+ listObjectsV1',
32−
41+
33− -- * Retrieving buckets
42+ -- * Retrieving buckets
34− , headBucket
43+ headBucket,
35−
44+
36− -- * Retrieving objects
45+ -- * Retrieving objects
46+
37− -----------------------
47+ -----------------------
38− , getObject'
48+ getObject',
39− , headObject
49+ headObject,
40−
50+
41− -- * Creating buckets and objects
51+ -- * Creating buckets and objects
52+
42− ---------------------------------
53+ ---------------------------------
43− , putBucket
54+ putBucket,
44− , ETag
55+ ETag,
45− , maxSinglePutObjectSizeBytes
56+ maxSinglePutObjectSizeBytes,
46− , putObjectSingle'
57+ putObjectSingle',
47− , putObjectSingle
58+ putObjectSingle,
48− , copyObjectSingle
59+ copyObjectSingle,
49−
60+
50− -- * Multipart Upload APIs
61+ -- * Multipart Upload APIs
62+
51− --------------------------
63+ --------------------------
52− , UploadId
64+ UploadId,
53− , PartTuple
65+ PartTuple,
54− , Payload(..)
66+ Payload (..),
55− , PartNumber
67+ PartNumber,
56− , newMultipartUpload
68+ newMultipartUpload,
57− , putObjectPart
69+ putObjectPart,
58− , copyObjectPart
70+ copyObjectPart,
59− , completeMultipartUpload
71+ completeMultipartUpload,
60− , abortMultipartUpload
72+ abortMultipartUpload,
61− , ListUploadsResult(..)
73+ ListUploadsResult (..),
62− , listIncompleteUploads'
74+ listIncompleteUploads',
63− , ListPartsResult(..)
75+ ListPartsResult (..),
64− , listIncompleteParts'
76+ listIncompleteParts',
65−
77+
66− -- * Deletion APIs
78+ -- * Deletion APIs
79+
67− --------------------------
80+ --------------------------
68− , deleteBucket
81+ deleteBucket,
69− , deleteObject
82+ deleteObject,
70−
83+
71− -- * Presigned Operations
84+ -- * Presigned Operations
85+
72− -----------------------------
86+ -----------------------------
73− , module Network.Minio.PresignedOperations
87+ module Network.Minio.PresignedOperations,
74−
88+
75− -- ** Bucket Policies
89+ -- ** Bucket Policies
76− , getBucketPolicy
90+ getBucketPolicy,
77− , setBucketPolicy
91+ setBucketPolicy,
78−
92+
79− -- * Bucket Notifications
93+ -- * Bucket Notifications
94+
80− -------------------------
95+ -------------------------
81− , Notification(..)
96+ Notification (..),
82− , NotificationConfig(..)
97+ NotificationConfig (..),
83− , Arn
98+ Arn,
84− , Event(..)
99+ Event (..),
85− , Filter(..)
100+ Filter (..),
86− , FilterKey(..)
101+ FilterKey (..),
87− , FilterRules(..)
102+ FilterRules (..),
88− , FilterRule(..)
103+ FilterRule (..),
89− , getBucketNotification
104+ getBucketNotification,
90− , putBucketNotification
105+ putBucketNotification,
91− , removeAllBucketNotification
106+ removeAllBucketNotification,
107+ )
92− ) where
108+where
93−
109+
94−import qualified Data.ByteString as BS
110+import qualified Data.ByteString as BS
95−import qualified Data.Text as T
111+import qualified Data.Text as T
112+import Lib.Prelude
96−import qualified Network.HTTP.Conduit as NC
113+import qualified Network.HTTP.Conduit as NC
97−import qualified Network.HTTP.Types as HT
114+import qualified Network.HTTP.Types as HT
98−import Network.HTTP.Types.Status (status404)
115+import Network.HTTP.Types.Status (status404)
99−import UnliftIO (Handler (Handler))
100−
101−import Lib.Prelude
102−
103−import Network.Minio.API
116+import Network.Minio.API
104−import Network.Minio.APICommon
117+import Network.Minio.APICommon
105−import Network.Minio.Data
118+import Network.Minio.Data
106−import Network.Minio.Errors
119+import Network.Minio.Errors
107−import Network.Minio.PresignedOperations
120+import Network.Minio.PresignedOperations
108−import Network.Minio.Utils
121+import Network.Minio.Utils
109−import Network.Minio.XmlGenerator
122+import Network.Minio.XmlGenerator
110−import Network.Minio.XmlParser
123+import Network.Minio.XmlParser
124+import UnliftIO (Handler (Handler))
111
125
112-- | Fetch all buckets from the service.
126-- | Fetch all buckets from the service.
113getService :: Minio [BucketInfo]
127getService :: Minio [BucketInfo]
114getService = do
128getService = do
129+ resp <-
130+ executeRequest $
115− resp <- executeRequest $ defaultS3ReqInfo {
131+ defaultS3ReqInfo
116− riNeedsLocation = False
132+ { riNeedsLocation = False
117− }
133+ }
118 parseListBuckets $ NC.responseBody resp
134 parseListBuckets $ NC.responseBody resp
119
135
120-- Parse headers from getObject and headObject calls.
136-- Parse headers from getObject and headObject calls.
121parseGetObjectHeaders :: Object -> [HT.Header] -> Maybe ObjectInfo
137parseGetObjectHeaders :: Object -> [HT.Header] -> Maybe ObjectInfo
122parseGetObjectHeaders object headers =
138parseGetObjectHeaders object headers =
123− let metadataPairs = getMetadata headers
139+ let metadataPairs = getMetadata headers
124− userMetadata = getUserMetadataMap metadataPairs
140+ userMetadata = getUserMetadataMap metadataPairs
125− metadata = getNonUserMetadataMap metadataPairs
141+ metadata = getNonUserMetadataMap metadataPairs
126− in ObjectInfo <$> Just object
142+ in ObjectInfo
143+ <$> Just object
127− <*> getLastModifiedHeader headers
144+ <*> getLastModifiedHeader headers
128− <*> getETagHeader headers
145+ <*> getETagHeader headers
129− <*> getContentLength headers
146+ <*> getContentLength headers
130− <*> Just userMetadata
147+ <*> Just userMetadata
131− <*> Just metadata
148+ <*> Just metadata
132
149
133-- | GET an object from the service and return parsed ObjectInfo and a
150-- | GET an object from the service and return parsed ObjectInfo and a
134-- conduit source for the object content
151-- conduit source for the object content
135−getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
152+getObject' ::
153+ Bucket ->
154+ Object ->
155+ HT.Query ->
156+ [HT.Header] ->
136− -> Minio GetObjectResponse
157+ Minio GetObjectResponse
137getObject' bucket object queryParams headers = do
158getObject' bucket object queryParams headers = do
138− resp <- mkStreamRequest reqInfo
159+ resp <- mkStreamRequest reqInfo
139− let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
160+ let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
161+ objInfo <-
162+ maybe
140− objInfo <- maybe (throwIO MErrVInvalidObjectInfoResponse) return
163+ (throwIO MErrVInvalidObjectInfoResponse)
164+ return
141− objInfoMaybe
165+ objInfoMaybe
166+ return $
167+ GetObjectResponse
142− return $ GetObjectResponse { gorObjectInfo = objInfo
168+ { gorObjectInfo = objInfo,
143− , gorObjectStream = NC.responseBody resp
169+ gorObjectStream = NC.responseBody resp
144− }
170+ }
145 where
171 where
172+ reqInfo =
173+ defaultS3ReqInfo
146− reqInfo = defaultS3ReqInfo { riBucket = Just bucket
174+ { riBucket = Just bucket,
147− , riObject = Just object
175+ riObject = Just object,
148− , riQueryParams = queryParams
176+ riQueryParams = queryParams,
149− , riHeaders = headers
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")]
150− }
183+ }
151
184
152-- | Creates a bucket via a PUT bucket call.
185-- | Creates a bucket via a PUT bucket call.
153putBucket :: Bucket -> Region -> Minio ()
186putBucket :: Bucket -> Region -> Minio ()
154putBucket bucket location = do
187putBucket bucket location = do
155 ns <- asks getSvcNamespace
188 ns <- asks getSvcNamespace
189+ void $
156− void $ executeRequest $
190+ executeRequest $
191+ defaultS3ReqInfo
157− defaultS3ReqInfo { riMethod = HT.methodPut
192+ { riMethod = HT.methodPut,
158− , riBucket = Just bucket
193+ riBucket = Just bucket,
159− , riPayload = PayloadBS $ mkCreateBucketConfig ns location
194+ riPayload = PayloadBS $ mkCreateBucketConfig ns location,
160− , riNeedsLocation = False
195+ riNeedsLocation = False
161 }
196 }
162
197
163-- | Single PUT object size.
198-- | Single PUT object size.
@@ -173,314 +208,432 @@ putObjectSingle' bucket object headers bs = do
173 let size = fromIntegral (BS.length bs)
208 let size = fromIntegral (BS.length bs)
174 -- check length is within single PUT object size.
209 -- check length is within single PUT object size.
175 when (size > maxSinglePutObjectSizeBytes) $
210 when (size > maxSinglePutObjectSizeBytes) $
211+ throwIO $
176− throwIO $ MErrVSinglePUTSizeExceeded size
212+ MErrVSinglePUTSizeExceeded size
177
213
178 let payload = mkStreamingPayload $ PayloadBS bs
214 let payload = mkStreamingPayload $ PayloadBS bs
215+ resp <-
179− resp <- executeRequest $
216+ executeRequest $
217+ defaultS3ReqInfo
180− defaultS3ReqInfo { riMethod = HT.methodPut
218+ { riMethod = HT.methodPut,
181− , riBucket = Just bucket
219+ riBucket = Just bucket,
182− , riObject = Just object
220+ riObject = Just object,
183− , riHeaders = headers
221+ riHeaders = headers,
184− , riPayload = payload
222+ riPayload = payload
185− }
223+ }
186
224
187 let rheaders = NC.responseHeaders resp
225 let rheaders = NC.responseHeaders resp
188 etag = getETagHeader rheaders
226 etag = getETagHeader rheaders
189 maybe
227 maybe
190 (throwIO MErrVETagHeaderNotFound)
228 (throwIO MErrVETagHeaderNotFound)
191− return etag
229+ return
230+ etag
192
231
193-- | PUT an object into the service. This function performs a single
232-- | PUT an object into the service. This function performs a single
194-- PUT object call, and so can only transfer objects upto 5GiB.
233-- PUT object call, and so can only transfer objects upto 5GiB.
195−putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
234+putObjectSingle ::
235+ Bucket ->
236+ Object ->
237+ [HT.Header] ->
238+ Handle ->
239+ Int64 ->
240+ Int64 ->
196− -> Int64 -> Minio ETag
241+ Minio ETag
197putObjectSingle bucket object headers h offset size = do
242putObjectSingle bucket object headers h offset size = do
198 -- check length is within single PUT object size.
243 -- check length is within single PUT object size.
199 when (size > maxSinglePutObjectSizeBytes) $
244 when (size > maxSinglePutObjectSizeBytes) $
245+ throwIO $
200− throwIO $ MErrVSinglePUTSizeExceeded size
246+ MErrVSinglePUTSizeExceeded size
201
247
202 -- content-length header is automatically set by library.
248 -- content-length header is automatically set by library.
203 let payload = mkStreamingPayload $ PayloadH h offset size
249 let payload = mkStreamingPayload $ PayloadH h offset size
250+ resp <-
204− resp <- executeRequest $
251+ executeRequest $
252+ defaultS3ReqInfo
205− defaultS3ReqInfo { riMethod = HT.methodPut
253+ { riMethod = HT.methodPut,
206− , riBucket = Just bucket
254+ riBucket = Just bucket,
207− , riObject = Just object
255+ riObject = Just object,
208− , riHeaders = headers
256+ riHeaders = headers,
209− , riPayload = payload
257+ riPayload = payload
210− }
258+ }
211
259
212 let rheaders = NC.responseHeaders resp
260 let rheaders = NC.responseHeaders resp
213 etag = getETagHeader rheaders
261 etag = getETagHeader rheaders
214 maybe
262 maybe
215 (throwIO MErrVETagHeaderNotFound)
263 (throwIO MErrVETagHeaderNotFound)
216− return etag
264+ return
265+ etag
217
266
218-- | List objects in a bucket matching prefix up to delimiter,
267-- | List objects in a bucket matching prefix up to delimiter,
219-- starting from nextMarker.
268-- starting from nextMarker.
220−listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
269+listObjectsV1' ::
270+ Bucket ->
271+ Maybe Text ->
272+ Maybe Text ->
273+ Maybe Text ->
274+ Maybe Int ->
221− -> Minio ListObjectsV1Result
275+ Minio ListObjectsV1Result
222listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
276listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
277+ resp <-
278+ executeRequest $
279+ defaultS3ReqInfo
223− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
280+ { riMethod = HT.methodGet,
224− , riBucket = Just bucket
281+ riBucket = Just bucket,
225− , riQueryParams = mkOptionalParams params
282+ riQueryParams = mkOptionalParams params
226− }
283+ }
227 parseListObjectsV1Response $ NC.responseBody resp
284 parseListObjectsV1Response $ NC.responseBody resp
228 where
285 where
229− params = [
286+ params =
230− ("marker", nextMarker)
287+ [ ("marker", nextMarker),
231− , ("prefix", prefix)
288+ ("prefix", prefix),
232− , ("delimiter", delimiter)
289+ ("delimiter", delimiter),
233− , ("max-keys", show <$> maxKeys)
290+ ("max-keys", show <$> maxKeys)
234 ]
291 ]
235
292
236-- | List objects in a bucket matching prefix up to delimiter,
293-- | List objects in a bucket matching prefix up to delimiter,
237-- starting from nextToken.
294-- starting from nextToken.
238−listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
295+listObjects' ::
296+ Bucket ->
297+ Maybe Text ->
298+ Maybe Text ->
299+ Maybe Text ->
300+ Maybe Int ->
239− -> Minio ListObjectsResult
301+ Minio ListObjectsResult
240listObjects' bucket prefix nextToken delimiter maxKeys = do
302listObjects' bucket prefix nextToken delimiter maxKeys = do
303+ resp <-
304+ executeRequest $
305+ defaultS3ReqInfo
241− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
306+ { riMethod = HT.methodGet,
242− , riBucket = Just bucket
307+ riBucket = Just bucket,
243− , riQueryParams = mkOptionalParams params
308+ riQueryParams = mkOptionalParams params
244− }
309+ }
245 parseListObjectsResponse $ NC.responseBody resp
310 parseListObjectsResponse $ NC.responseBody resp
246 where
311 where
247− params = [
312+ params =
248− ("list-type", Just "2")
313+ [ ("list-type", Just "2"),
249− , ("continuation_token", nextToken)
314+ ("continuation_token", nextToken),
250− , ("prefix", prefix)
315+ ("prefix", prefix),
251− , ("delimiter", delimiter)
316+ ("delimiter", delimiter),
252− , ("max-keys", show <$> maxKeys)
317+ ("max-keys", show <$> maxKeys)
253 ]
318 ]
254
319
255-- | DELETE a bucket from the service.
320-- | DELETE a bucket from the service.
256deleteBucket :: Bucket -> Minio ()
321deleteBucket :: Bucket -> Minio ()
257−deleteBucket bucket = void $
322+deleteBucket bucket =
323+ void $
258− executeRequest $
324+ executeRequest $
325+ defaultS3ReqInfo
259− defaultS3ReqInfo { riMethod = HT.methodDelete
326+ { riMethod = HT.methodDelete,
260− , riBucket = Just bucket
327+ riBucket = Just bucket
261 }
328 }
262
329
263-- | DELETE an object from the service.
330-- | DELETE an object from the service.
264deleteObject :: Bucket -> Object -> Minio ()
331deleteObject :: Bucket -> Object -> Minio ()
265−deleteObject bucket object = void $
332+deleteObject bucket object =
333+ void $
266− executeRequest $
334+ executeRequest $
335+ defaultS3ReqInfo
267− defaultS3ReqInfo { riMethod = HT.methodDelete
336+ { riMethod = HT.methodDelete,
268− , riBucket = Just bucket
337+ riBucket = Just bucket,
269− , riObject = Just object
338+ riObject = Just object
270 }
339 }
271
340
272-- | Create a new multipart upload.
341-- | Create a new multipart upload.
273newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
342newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
274newMultipartUpload bucket object headers = do
343newMultipartUpload bucket object headers = do
344+ resp <-
345+ executeRequest $
346+ defaultS3ReqInfo
275− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPost
347+ { riMethod = HT.methodPost,
276− , riBucket = Just bucket
348+ riBucket = Just bucket,
277− , riObject = Just object
349+ riObject = Just object,
278− , riQueryParams = [("uploads", Nothing)]
350+ riQueryParams = [("uploads", Nothing)],
279− , riHeaders = headers
351+ riHeaders = headers
280− }
352+ }
281 parseNewMultipartUpload $ NC.responseBody resp
353 parseNewMultipartUpload $ NC.responseBody resp
282
354
283-- | PUT a part of an object as part of a multipart upload.
355-- | PUT a part of an object as part of a multipart upload.
284−putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
356+putObjectPart ::
357+ Bucket ->
358+ Object ->
359+ UploadId ->
360+ PartNumber ->
361+ [HT.Header] ->
362+ Payload ->
285− -> Payload -> Minio PartTuple
363+ Minio PartTuple
286putObjectPart bucket object uploadId partNumber headers payload = do
364putObjectPart bucket object uploadId partNumber headers payload = do
287 -- transform payload to conduit to enable streaming signature
365 -- transform payload to conduit to enable streaming signature
288 let payload' = mkStreamingPayload payload
366 let payload' = mkStreamingPayload payload
367+ resp <-
289− resp <- executeRequest $
368+ executeRequest $
369+ defaultS3ReqInfo
290− defaultS3ReqInfo { riMethod = HT.methodPut
370+ { riMethod = HT.methodPut,
291− , riBucket = Just bucket
371+ riBucket = Just bucket,
292− , riObject = Just object
372+ riObject = Just object,
293− , riQueryParams = mkOptionalParams params
373+ riQueryParams = mkOptionalParams params,
294− , riHeaders = headers
374+ riHeaders = headers,
295− , riPayload = payload'
375+ riPayload = payload'
296− }
376+ }
297 let rheaders = NC.responseHeaders resp
377 let rheaders = NC.responseHeaders resp
298 etag = getETagHeader rheaders
378 etag = getETagHeader rheaders
299 maybe
379 maybe
300 (throwIO MErrVETagHeaderNotFound)
380 (throwIO MErrVETagHeaderNotFound)
301− (return . (partNumber, )) etag
381+ (return . (partNumber,))
382+ etag
302 where
383 where
303− params = [
384+ params =
304− ("uploadId", Just uploadId)
385+ [ ("uploadId", Just uploadId),
305− , ("partNumber", Just $ show partNumber)
386+ ("partNumber", Just $ show partNumber)
306 ]
387 ]
307
388
308srcInfoToHeaders :: SourceInfo -> [HT.Header]
389srcInfoToHeaders :: SourceInfo -> [HT.Header]
309−srcInfoToHeaders srcInfo = ("x-amz-copy-source",
390+srcInfoToHeaders srcInfo =
391+ ( "x-amz-copy-source",
392+ encodeUtf8 $
393+ T.concat
394+ [ "/",
310− toS $ T.concat ["/", srcBucket srcInfo,
395+ srcBucket srcInfo,
396+ "/",
311− "/", srcObject srcInfo]
397+ srcObject srcInfo
398+ ]
399+ )
400+ : rangeHdr
312− ) : rangeHdr ++ zip names values
401+ ++ zip names values
313 where
402 where
403+ names =
404+ [ "x-amz-copy-source-if-match",
314− names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",
405+ "x-amz-copy-source-if-none-match",
315− "x-amz-copy-source-if-unmodified-since",
406+ "x-amz-copy-source-if-unmodified-since",
316− "x-amz-copy-source-if-modified-since"]
407+ "x-amz-copy-source-if-modified-since"
408+ ]
409+ values =
410+ mapMaybe
317− values = mapMaybe (fmap encodeUtf8 . (srcInfo &))
411+ (fmap encodeUtf8 . (srcInfo &))
412+ [ srcIfMatch,
318− [srcIfMatch, srcIfNoneMatch,
413+ srcIfNoneMatch,
319− fmap formatRFC1123 . srcIfUnmodifiedSince,
414+ fmap formatRFC1123 . srcIfUnmodifiedSince,
320− fmap formatRFC1123 . srcIfModifiedSince]
415+ fmap formatRFC1123 . srcIfModifiedSince
416+ ]
417+ rangeHdr =
321− rangeHdr = maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])])
418+ maybe [] ((\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) . toByteRange) (srcRange srcInfo)
322− $ toByteRange <$> srcRange srcInfo
323 toByteRange :: (Int64, Int64) -> HT.ByteRange
419 toByteRange :: (Int64, Int64) -> HT.ByteRange
324 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
420 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
325
421
326-- | Performs server-side copy of an object or part of an object as an
422-- | Performs server-side copy of an object or part of an object as an
327-- upload part of an ongoing multi-part upload.
423-- upload part of an ongoing multi-part upload.
424+copyObjectPart ::
328−copyObjectPart :: DestinationInfo -> SourceInfo -> UploadId
425+ DestinationInfo ->
426+ SourceInfo ->
427+ UploadId ->
428+ PartNumber ->
429+ [HT.Header] ->
329− -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
430+ Minio (ETag, UTCTime)
330copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
431copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
432+ resp <-
331− resp <- executeRequest $
433+ executeRequest $
434+ defaultS3ReqInfo
332− defaultS3ReqInfo { riMethod = HT.methodPut
435+ { riMethod = HT.methodPut,
333− , riBucket = Just $ dstBucket dstInfo
436+ riBucket = Just $ dstBucket dstInfo,
334− , riObject = Just $ dstObject dstInfo
437+ riObject = Just $ dstObject dstInfo,
335− , riQueryParams = mkOptionalParams params
438+ riQueryParams = mkOptionalParams params,
336− , riHeaders = headers ++ srcInfoToHeaders srcInfo
439+ riHeaders = headers ++ srcInfoToHeaders srcInfo
337− }
440+ }
338
441
339 parseCopyObjectResponse $ NC.responseBody resp
442 parseCopyObjectResponse $ NC.responseBody resp
340 where
443 where
341− params = [
444+ params =
342− ("uploadId", Just uploadId)
445+ [ ("uploadId", Just uploadId),
343− , ("partNumber", Just $ show partNumber)
446+ ("partNumber", Just $ show partNumber)
344 ]
447 ]
345
448
346-- | Performs server-side copy of an object that is upto 5GiB in
449-- | Performs server-side copy of an object that is upto 5GiB in
347-- size. If the object is greater than 5GiB, this function throws the
450-- size. If the object is greater than 5GiB, this function throws the
348-- error returned by the server.
451-- error returned by the server.
349−copyObjectSingle :: Bucket -> Object -> SourceInfo -> [HT.Header]
452+copyObjectSingle ::
453+ Bucket ->
454+ Object ->
455+ SourceInfo ->
456+ [HT.Header] ->
350− -> Minio (ETag, UTCTime)
457+ Minio (ETag, UTCTime)
351copyObjectSingle bucket object srcInfo headers = do
458copyObjectSingle bucket object srcInfo headers = do
352 -- validate that srcRange is Nothing for this API.
459 -- validate that srcRange is Nothing for this API.
353 when (isJust $ srcRange srcInfo) $
460 when (isJust $ srcRange srcInfo) $
354 throwIO MErrVCopyObjSingleNoRangeAccepted
461 throwIO MErrVCopyObjSingleNoRangeAccepted
462+ resp <-
355− resp <- executeRequest $
463+ executeRequest $
464+ defaultS3ReqInfo
356− defaultS3ReqInfo { riMethod = HT.methodPut
465+ { riMethod = HT.methodPut,
357− , riBucket = Just bucket
466+ riBucket = Just bucket,
358− , riObject = Just object
467+ riObject = Just object,
359− , riHeaders = headers ++ srcInfoToHeaders srcInfo
468+ riHeaders = headers ++ srcInfoToHeaders srcInfo
360− }
469+ }
361 parseCopyObjectResponse $ NC.responseBody resp
470 parseCopyObjectResponse $ NC.responseBody resp
362
471
363-- | Complete a multipart upload.
472-- | Complete a multipart upload.
364−completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
473+completeMultipartUpload ::
474+ Bucket ->
475+ Object ->
476+ UploadId ->
477+ [PartTuple] ->
365− -> Minio ETag
478+ Minio ETag
366completeMultipartUpload bucket object uploadId partTuple = do
479completeMultipartUpload bucket object uploadId partTuple = do
480+ resp <-
367− resp <- executeRequest $
481+ executeRequest $
482+ defaultS3ReqInfo
368− defaultS3ReqInfo { riMethod = HT.methodPost
483+ { riMethod = HT.methodPost,
369− , riBucket = Just bucket
484+ riBucket = Just bucket,
370− , riObject = Just object
485+ riObject = Just object,
371− , riQueryParams = mkOptionalParams params
486+ riQueryParams = mkOptionalParams params,
372− , riPayload = PayloadBS $
487+ riPayload =
488+ PayloadBS $
373− mkCompleteMultipartUploadRequest partTuple
489+ mkCompleteMultipartUploadRequest partTuple
374− }
490+ }
375 parseCompleteMultipartUploadResponse $ NC.responseBody resp
491 parseCompleteMultipartUploadResponse $ NC.responseBody resp
376 where
492 where
377 params = [("uploadId", Just uploadId)]
493 params = [("uploadId", Just uploadId)]
378
494
379-- | Abort a multipart upload.
495-- | Abort a multipart upload.
380abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
496abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
381−abortMultipartUpload bucket object uploadId = void $
497+abortMultipartUpload bucket object uploadId =
498+ void $
499+ executeRequest $
500+ defaultS3ReqInfo
382− executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
501+ { riMethod = HT.methodDelete,
383− , riBucket = Just bucket
502+ riBucket = Just bucket,
384− , riObject = Just object
503+ riObject = Just object,
385− , riQueryParams = mkOptionalParams params
504+ riQueryParams = mkOptionalParams params
386− }
505+ }
387 where
506 where
388 params = [("uploadId", Just uploadId)]
507 params = [("uploadId", Just uploadId)]
389
508
390-- | List incomplete multipart uploads.
509-- | List incomplete multipart uploads.
391−listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
510+listIncompleteUploads' ::
511+ Bucket ->
512+ Maybe Text ->
513+ Maybe Text ->
514+ Maybe Text ->
515+ Maybe Text ->
516+ Maybe Int ->
392− -> Maybe Text -> Maybe Int -> Minio ListUploadsResult
517+ Minio ListUploadsResult
393listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
518listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
519+ resp <-
520+ executeRequest $
521+ defaultS3ReqInfo
394− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
522+ { riMethod = HT.methodGet,
395− , riBucket = Just bucket
523+ riBucket = Just bucket,
396− , riQueryParams = params
524+ riQueryParams = params
397− }
525+ }
398 parseListUploadsResponse $ NC.responseBody resp
526 parseListUploadsResponse $ NC.responseBody resp
399 where
527 where
400 -- build query params
528 -- build query params
529+ params =
530+ ("uploads", Nothing)
401− params = ("uploads", Nothing) : mkOptionalParams
531+ : mkOptionalParams
402− [ ("prefix", prefix)
532+ [ ("prefix", prefix),
403− , ("delimiter", delimiter)
533+ ("delimiter", delimiter),
404− , ("key-marker", keyMarker)
534+ ("key-marker", keyMarker),
405− , ("upload-id-marker", uploadIdMarker)
535+ ("upload-id-marker", uploadIdMarker),
406− , ("max-uploads", show <$> maxKeys)
536+ ("max-uploads", show <$> maxKeys)
407− ]
537+ ]
408−
409
538
410-- | List parts of an ongoing multipart upload.
539-- | List parts of an ongoing multipart upload.
411−listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
540+listIncompleteParts' ::
541+ Bucket ->
542+ Object ->
543+ UploadId ->
544+ Maybe Text ->
545+ Maybe Text ->
412− -> Maybe Text -> Minio ListPartsResult
546+ Minio ListPartsResult
413listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
547listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
548+ resp <-
549+ executeRequest $
550+ defaultS3ReqInfo
414− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
551+ { riMethod = HT.methodGet,
415− , riBucket = Just bucket
552+ riBucket = Just bucket,
416− , riObject = Just object
553+ riObject = Just object,
417− , riQueryParams = mkOptionalParams params
554+ riQueryParams = mkOptionalParams params
418− }
555+ }
419 parseListPartsResponse $ NC.responseBody resp
556 parseListPartsResponse $ NC.responseBody resp
420 where
557 where
421 -- build optional query params
558 -- build optional query params
422− params = [
559+ params =
423− ("uploadId", Just uploadId)
560+ [ ("uploadId", Just uploadId),
424− , ("part-number-marker", partNumMarker)
561+ ("part-number-marker", partNumMarker),
425− , ("max-parts", maxParts)
562+ ("max-parts", maxParts)
426 ]
563 ]
427
564
428-- | Get metadata of an object.
565-- | Get metadata of an object.
429headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
566headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
430headObject bucket object reqHeaders = do
567headObject bucket object reqHeaders = do
568+ resp <-
569+ executeRequest $
570+ defaultS3ReqInfo
431− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
571+ { riMethod = HT.methodHead,
432− , riBucket = Just bucket
572+ riBucket = Just bucket,
433− , riObject = Just object
573+ riObject = Just object,
574+ riHeaders =
434− , riHeaders = reqHeaders
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")]
435− }
580+ }
436−
437 maybe (throwIO MErrVInvalidObjectInfoResponse) return $
581 maybe (throwIO MErrVInvalidObjectInfoResponse) return $
438− parseGetObjectHeaders object $ NC.responseHeaders resp
582+ parseGetObjectHeaders object $
439−
583+ NC.responseHeaders resp
440
584
441-- | Query the object store if a given bucket exists.
585-- | Query the object store if a given bucket exists.
442headBucket :: Bucket -> Minio Bool
586headBucket :: Bucket -> Minio Bool
443−headBucket bucket = headBucketEx `catches`
587+headBucket bucket =
588+ headBucketEx
444− [ Handler handleNoSuchBucket
589+ `catches` [ Handler handleNoSuchBucket,
445− , Handler handleStatus404
590+ Handler handleStatus404
446− ]
591+ ]
447−
448 where
592 where
449 handleNoSuchBucket :: ServiceErr -> Minio Bool
593 handleNoSuchBucket :: ServiceErr -> Minio Bool
594+ handleNoSuchBucket e
450− handleNoSuchBucket e | e == NoSuchBucket = return False
595+ | e == NoSuchBucket = return False
451− | otherwise = throwIO e
596+ | otherwise = throwIO e
452−
453 handleStatus404 :: NC.HttpException -> Minio Bool
597 handleStatus404 :: NC.HttpException -> Minio Bool
454 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
598 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
455 if NC.responseStatus res == status404
599 if NC.responseStatus res == status404
456− then return False
600+ then return False
457− else throwIO e
601+ else throwIO e
458 handleStatus404 e = throwIO e
602 handleStatus404 e = throwIO e
459−
460 headBucketEx = do
603 headBucketEx = do
604+ resp <-
605+ executeRequest $
606+ defaultS3ReqInfo
461− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
607+ { riMethod = HT.methodHead,
462− , riBucket = Just bucket
608+ riBucket = Just bucket
463− }
609+ }
464 return $ NC.responseStatus resp == HT.ok200
610 return $ NC.responseStatus resp == HT.ok200
465
611
466-- | Set the notification configuration on a bucket.
612-- | Set the notification configuration on a bucket.
467putBucketNotification :: Bucket -> Notification -> Minio ()
613putBucketNotification :: Bucket -> Notification -> Minio ()
468putBucketNotification bucket ncfg = do
614putBucketNotification bucket ncfg = do
469 ns <- asks getSvcNamespace
615 ns <- asks getSvcNamespace
616+ void $
617+ executeRequest $
618+ defaultS3ReqInfo
470− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
619+ { riMethod = HT.methodPut,
471− , riBucket = Just bucket
620+ riBucket = Just bucket,
472− , riQueryParams = [("notification", Nothing)]
621+ riQueryParams = [("notification", Nothing)],
473− , riPayload = PayloadBS $
622+ riPayload =
623+ PayloadBS $
474− mkPutNotificationRequest ns ncfg
624+ mkPutNotificationRequest ns ncfg
475− }
625+ }
476
626
477-- | Retrieve the notification configuration on a bucket.
627-- | Retrieve the notification configuration on a bucket.
478getBucketNotification :: Bucket -> Minio Notification
628getBucketNotification :: Bucket -> Minio Notification
479getBucketNotification bucket = do
629getBucketNotification bucket = do
630+ resp <-
631+ executeRequest $
632+ defaultS3ReqInfo
480− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
633+ { riMethod = HT.methodGet,
481− , riBucket = Just bucket
634+ riBucket = Just bucket,
482− , riQueryParams = [("notification", Nothing)]
635+ riQueryParams = [("notification", Nothing)]
483− }
636+ }
484 parseNotification $ NC.responseBody resp
637 parseNotification $ NC.responseBody resp
485
638
486-- | Remove all notifications configured on a bucket.
639-- | Remove all notifications configured on a bucket.
@@ -490,11 +643,14 @@ removeAllBucketNotification = flip putBucketNotification defaultNotification
490-- | Fetch the policy if any on a bucket.
643-- | Fetch the policy if any on a bucket.
491getBucketPolicy :: Bucket -> Minio Text
644getBucketPolicy :: Bucket -> Minio Text
492getBucketPolicy bucket = do
645getBucketPolicy bucket = do
646+ resp <-
647+ executeRequest $
648+ defaultS3ReqInfo
493− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
649+ { riMethod = HT.methodGet,
494− , riBucket = Just bucket
650+ riBucket = Just bucket,
495− , riQueryParams = [("policy", Nothing)]
651+ riQueryParams = [("policy", Nothing)]
496− }
652+ }
497− return $ toS $ NC.responseBody resp
653+ return $ decodeUtf8Lenient $ toStrictBS $ NC.responseBody resp
498
654
499-- | Set a new policy on a bucket.
655-- | Set a new policy on a bucket.
500-- As a special condition if the policy is empty
656-- As a special condition if the policy is empty
@@ -506,18 +662,24 @@ setBucketPolicy bucket policy = do
506 else putBucketPolicy bucket policy
662 else putBucketPolicy bucket policy
507
663
508-- | Save a new policy on a bucket.
664-- | Save a new policy on a bucket.
509−putBucketPolicy :: Bucket -> Text -> Minio()
665+putBucketPolicy :: Bucket -> Text -> Minio ()
510putBucketPolicy bucket policy = do
666putBucketPolicy bucket policy = do
667+ void $
668+ executeRequest $
669+ defaultS3ReqInfo
511− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
670+ { riMethod = HT.methodPut,
512− , riBucket = Just bucket
671+ riBucket = Just bucket,
513− , riQueryParams = [("policy", Nothing)]
672+ riQueryParams = [("policy", Nothing)],
514− , riPayload = PayloadBS $ encodeUtf8 policy
673+ riPayload = PayloadBS $ encodeUtf8 policy
515− }
674+ }
516
675
517-- | Delete any policy set on a bucket.
676-- | Delete any policy set on a bucket.
518−deleteBucketPolicy :: Bucket -> Minio()
677+deleteBucketPolicy :: Bucket -> Minio ()
519deleteBucketPolicy bucket = do
678deleteBucketPolicy bucket = do
679+ void $
680+ executeRequest $
681+ defaultS3ReqInfo
520− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
682+ { riMethod = HT.methodDelete,
521− , riBucket = Just bucket
683+ riBucket = Just bucket,
522− , riQueryParams = [("policy", Nothing)]
684+ riQueryParams = [("policy", Nothing)]
523− }
685+ }
src/Network/Minio/SelectAPI.hs+233−238
@@ -15,113 +15,103 @@
15--
15--
16
16
17module Network.Minio.SelectAPI
17module Network.Minio.SelectAPI
18− (
19−
20− -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
18+ ( -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
21− -- format objects in AWS S3 and in MinIO using SQL Select
19+ -- format objects in AWS S3 and in MinIO using SQL Select
22− -- statements. This allows significant reduction of data transfer
20+ -- statements. This allows significant reduction of data transfer
23− -- from object storage for computation-intensive tasks, as relevant
21+ -- from object storage for computation-intensive tasks, as relevant
24− -- data is filtered close to the storage.
22+ -- data is filtered close to the storage.
25−
26− selectObjectContent
23+ selectObjectContent,
27−
28− , SelectRequest
24+ SelectRequest,
29− , selectRequest
25+ selectRequest,
30−
26+
31− -- *** Input Serialization
27+ -- *** Input Serialization
32−
33− , InputSerialization
28+ InputSerialization,
34− , defaultCsvInput
29+ defaultCsvInput,
35− , linesJsonInput
30+ linesJsonInput,
36− , documentJsonInput
31+ documentJsonInput,
37− , defaultParquetInput
32+ defaultParquetInput,
38− , setInputCSVProps
33+ setInputCSVProps,
39−
40− , CompressionType(..)
34+ CompressionType (..),
41− , setInputCompressionType
35+ setInputCompressionType,
42−
36+
43− -- *** CSV Format details
37+ -- *** CSV Format details
44−
38+
45− -- | CSV format options such as delimiters and quote characters are
39+ -- | CSV format options such as delimiters and quote characters are
46− -- specified using using the functions below. Options are combined
40+ -- specified using using the functions below. Options are combined
47− -- monoidally.
41+ -- monoidally.
48−
49− , CSVProp
42+ CSVProp,
50− , recordDelimiter
43+ recordDelimiter,
51− , fieldDelimiter
44+ fieldDelimiter,
52− , quoteCharacter
45+ quoteCharacter,
53− , quoteEscapeCharacter
46+ quoteEscapeCharacter,
54− , commentCharacter
47+ commentCharacter,
55− , allowQuotedRecordDelimiter
48+ allowQuotedRecordDelimiter,
56− , FileHeaderInfo(..)
49+ FileHeaderInfo (..),
57− , fileHeaderInfo
50+ fileHeaderInfo,
58− , QuoteFields(..)
51+ QuoteFields (..),
59− , quoteFields
52+ quoteFields,
60−
53+
61− -- *** Output Serialization
54+ -- *** Output Serialization
62−
63− , OutputSerialization
55+ OutputSerialization,
64− , defaultCsvOutput
56+ defaultCsvOutput,
65− , defaultJsonOutput
57+ defaultJsonOutput,
66− , outputCSVFromProps
58+ outputCSVFromProps,
67− , outputJSONFromRecordDelimiter
59+ outputJSONFromRecordDelimiter,
68−
60+
69− -- *** Progress messages
61+ -- *** Progress messages
70−
71− , setRequestProgressEnabled
62+ setRequestProgressEnabled,
72−
63+
73− -- *** Interpreting Select output
64+ -- *** Interpreting Select output
74−
65+
75− -- | The conduit returned by `selectObjectContent` returns values of
66+ -- | The conduit returned by `selectObjectContent` returns values of
76− -- the `EventMessage` data type. This returns the query output
67+ -- the `EventMessage` data type. This returns the query output
77− -- messages formatted according to the chosen output serialization,
68+ -- messages formatted according to the chosen output serialization,
78− -- interleaved with progress messages (if enabled by
69+ -- interleaved with progress messages (if enabled by
79− -- `setRequestProgressEnabled`), and at the end a statistics
70+ -- `setRequestProgressEnabled`), and at the end a statistics
80− -- message.
71+ -- message.
81− --
72+ --
82− -- If the application is interested in only the payload, then
73+ -- If the application is interested in only the payload, then
83− -- `getPayloadBytes` can be used. For example to simply print the
74+ -- `getPayloadBytes` can be used. For example to simply print the
84− -- payload to stdout:
75+ -- payload to stdout:
85− --
76+ --
86− -- > resultConduit <- selectObjectContent bucket object mySelectRequest
77+ -- > resultConduit <- selectObjectContent bucket object mySelectRequest
87− -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
78+ -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
88− --
79+ --
89− -- Note that runConduit, the connect operator (.|) and stdoutC are
80+ -- Note that runConduit, the connect operator (.|) and stdoutC are
90− -- all from the "conduit" package.
81+ -- all from the "conduit" package.
91−
92− , getPayloadBytes
82+ getPayloadBytes,
93− , EventMessage(..)
83+ EventMessage (..),
94− , Progress(..)
84+ Progress (..),
95− , Stats
85+ Stats,
86+ )
96− ) where
87+where
97−
88+
98−import Conduit ((.|))
89+import Conduit ((.|))
99−import qualified Conduit as C
90+import qualified Conduit as C
100−import qualified Data.Binary as Bin
91+import qualified Data.Binary as Bin
101−import qualified Data.ByteString as B
92+import qualified Data.ByteString as B
102−import qualified Data.ByteString.Lazy as LB
93+import qualified Data.ByteString.Lazy as LB
103−import Data.Digest.CRC32 (crc32, crc32Update)
94+import Data.Digest.CRC32 (crc32, crc32Update)
95+import Lib.Prelude
104−import qualified Network.HTTP.Conduit as NC
96+import qualified Network.HTTP.Conduit as NC
105−import qualified Network.HTTP.Types as HT
97+import qualified Network.HTTP.Types as HT
106−import UnliftIO (MonadUnliftIO)
107−
108−import Lib.Prelude
109−
110−import Network.Minio.API
98+import Network.Minio.API
111−import Network.Minio.Data
99+import Network.Minio.Data
112−import Network.Minio.Errors
100+import Network.Minio.Errors
113−import Network.Minio.Utils
101+import Network.Minio.Utils
114−import Network.Minio.XmlGenerator
102+import Network.Minio.XmlGenerator
115−import Network.Minio.XmlParser
103+import Network.Minio.XmlParser
116−
104+import UnliftIO (MonadUnliftIO)
105+
117−data EventStreamException = ESEPreludeCRCFailed
106+data EventStreamException
107+ = ESEPreludeCRCFailed
118− | ESEMessageCRCFailed
108+ | ESEMessageCRCFailed
119− | ESEUnexpectedEndOfStream
109+ | ESEUnexpectedEndOfStream
120− | ESEDecodeFail [Char]
110+ | ESEDecodeFail [Char]
121− | ESEInvalidHeaderType
111+ | ESEInvalidHeaderType
122− | ESEInvalidHeaderValueType
112+ | ESEInvalidHeaderValueType
123− | ESEInvalidMessageType
113+ | ESEInvalidMessageType
124− deriving (Eq, Show)
114+ deriving stock (Eq, Show)
125
115
126instance Exception EventStreamException
116instance Exception EventStreamException
127
117
@@ -129,171 +119,176 @@ instance Exception EventStreamException
129chunkSize :: Int
119chunkSize :: Int
130chunkSize = 32 * 1024
120chunkSize = 32 * 1024
131
121
132−parseBinary :: Bin.Binary a => ByteString -> IO a
122+parseBinary :: (Bin.Binary a) => ByteString -> IO a
133parseBinary b = do
123parseBinary b = do
134− case Bin.decodeOrFail $ LB.fromStrict b of
124+ case Bin.decodeOrFail $ LB.fromStrict b of
135− Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
125+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
136− Right (_, _, r) -> return r
126+ Right (_, _, r) -> return r
137
127
138bytesToHeaderName :: Text -> IO MsgHeaderName
128bytesToHeaderName :: Text -> IO MsgHeaderName
139bytesToHeaderName t = case t of
129bytesToHeaderName t = case t of
140− ":message-type" -> return MessageType
130+ ":message-type" -> return MessageType
141− ":event-type" -> return EventType
131+ ":event-type" -> return EventType
142− ":content-type" -> return ContentType
132+ ":content-type" -> return ContentType
143− ":error-code" -> return ErrorCode
133+ ":error-code" -> return ErrorCode
144 ":error-message" -> return ErrorMessage
134 ":error-message" -> return ErrorMessage
145− _ -> throwIO ESEInvalidHeaderType
135+ _ -> throwIO ESEInvalidHeaderType
146
136
137+parseHeaders ::
147−parseHeaders :: MonadUnliftIO m
138+ (MonadUnliftIO m) =>
139+ Word32 ->
148− => Word32 -> C.ConduitM ByteString a m [MessageHeader]
140+ C.ConduitM ByteString a m [MessageHeader]
149parseHeaders 0 = return []
141parseHeaders 0 = return []
150parseHeaders hdrLen = do
142parseHeaders hdrLen = do
151− bs1 <- readNBytes 1
143+ bs1 <- readNBytes 1
152− n :: Word8 <- liftIO $ parseBinary bs1
144+ n :: Word8 <- liftIO $ parseBinary bs1
153
145
154− headerKeyBytes <- readNBytes $ fromIntegral n
146+ headerKeyBytes <- readNBytes $ fromIntegral n
155− let headerKey = decodeUtf8Lenient headerKeyBytes
147+ let headerKey = decodeUtf8Lenient headerKeyBytes
156− headerName <- liftIO $ bytesToHeaderName headerKey
148+ headerName <- liftIO $ bytesToHeaderName headerKey
157
149
158− bs2 <- readNBytes 1
150+ bs2 <- readNBytes 1
159− headerValueType :: Word8 <- liftIO $ parseBinary bs2
151+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
160− when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
152+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
161
153
162− bs3 <- readNBytes 2
154+ bs3 <- readNBytes 2
163− vLen :: Word16 <- liftIO $ parseBinary bs3
155+ vLen :: Word16 <- liftIO $ parseBinary bs3
164− headerValueBytes <- readNBytes $ fromIntegral vLen
156+ headerValueBytes <- readNBytes $ fromIntegral vLen
165− let headerValue = decodeUtf8Lenient headerValueBytes
157+ let headerValue = decodeUtf8Lenient headerValueBytes
166− m = (headerName, headerValue)
158+ m = (headerName, headerValue)
167− k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
159+ k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
168
160
169− ms <- parseHeaders (hdrLen - k)
161+ ms <- parseHeaders (hdrLen - k)
170− return (m:ms)
162+ return (m : ms)
171
163
172-- readNBytes returns N bytes read from the string and throws an
164-- readNBytes returns N bytes read from the string and throws an
173-- exception if N bytes are not present on the stream.
165-- exception if N bytes are not present on the stream.
174−readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
166+readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
175readNBytes n = do
167readNBytes n = do
176− b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
168+ b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
177− if B.length b /= n
169+ if B.length b /= n
178− then throwIO ESEUnexpectedEndOfStream
170+ then throwIO ESEUnexpectedEndOfStream
179− else return b
171+ else return b
180−
172+
173+crcCheck ::
181−crcCheck :: MonadUnliftIO m
174+ (MonadUnliftIO m) =>
182− => C.ConduitM ByteString ByteString m ()
175+ C.ConduitM ByteString ByteString m ()
183crcCheck = do
176crcCheck = do
184− b <- readNBytes 12
177+ b <- readNBytes 12
185− n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
178+ n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
186− preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
179+ preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
187− when (crc32 (B.take 8 b) /= preludeCRC) $
180+ when (crc32 (B.take 8 b) /= preludeCRC) $
188− throwIO ESEPreludeCRCFailed
181+ throwIO ESEPreludeCRCFailed
189
182
190− -- we do not yield the checksum
183+ -- we do not yield the checksum
191− C.yield $ B.take 8 b
184+ C.yield $ B.take 8 b
192
185
193− -- 12 bytes have been read off the current message. Now read the
186+ -- 12 bytes have been read off the current message. Now read the
194− -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
187+ -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
195− let startCrc = crc32 b
188+ let startCrc = crc32 b
196− finalCrc <- accumulateYield (fromIntegral n-16) startCrc
189+ finalCrc <- accumulateYield (fromIntegral n - 16) startCrc
197
190
198− bs <- readNBytes 4
191+ bs <- readNBytes 4
199− expectedCrc :: Word32 <- liftIO $ parseBinary bs
192+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
200
193
201− when (finalCrc /= expectedCrc) $
194+ when (finalCrc /= expectedCrc) $
202− throwIO ESEMessageCRCFailed
195+ throwIO ESEMessageCRCFailed
203
196
204− -- we unconditionally recurse - downstream figures out when to
197+ -- we unconditionally recurse - downstream figures out when to
205− -- quit reading the stream
198+ -- quit reading the stream
206− crcCheck
199+ crcCheck
207 where
200 where
208 accumulateYield n checkSum = do
201 accumulateYield n checkSum = do
209− let toRead = min n chunkSize
202+ let toRead = min n chunkSize
210− b <- readNBytes toRead
203+ b <- readNBytes toRead
211− let c' = crc32Update checkSum b
204+ let c' = crc32Update checkSum b
212− n' = n - B.length b
205+ n' = n - B.length b
213− C.yield b
206+ C.yield b
214− if n' > 0
207+ if n' > 0
215− then accumulateYield n' c'
208+ then accumulateYield n' c'
216− else return c'
209+ else return c'
217−
210+
218−handleMessage :: MonadUnliftIO m => C.ConduitT ByteString EventMessage m ()
211+handleMessage :: (MonadUnliftIO m) => C.ConduitT ByteString EventMessage m ()
219handleMessage = do
212handleMessage = do
220− b1 <- readNBytes 4
213+ b1 <- readNBytes 4
221− msgLen :: Word32 <- liftIO $ parseBinary b1
214+ msgLen :: Word32 <- liftIO $ parseBinary b1
222−
215+
223− b2 <- readNBytes 4
216+ b2 <- readNBytes 4
224− hdrLen :: Word32 <- liftIO $ parseBinary b2
217+ hdrLen :: Word32 <- liftIO $ parseBinary b2
225−
218+
226− hs <- parseHeaders hdrLen
219+ hs <- parseHeaders hdrLen
227−
220+
228− let payloadLen = msgLen - hdrLen - 16
221+ let payloadLen = msgLen - hdrLen - 16
229− getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)
222+ getHdrVal h = fmap snd . find ((h ==) . fst)
230− eventHdrValue = getHdrVal EventType hs
223+ eventHdrValue = getHdrVal EventType hs
231− msgHdrValue = getHdrVal MessageType hs
224+ msgHdrValue = getHdrVal MessageType hs
232− errCode = getHdrVal ErrorCode hs
225+ errCode = getHdrVal ErrorCode hs
233− errMsg = getHdrVal ErrorMessage hs
226+ errMsg = getHdrVal ErrorMessage hs
234−
227+
235− case msgHdrValue of
228+ case msgHdrValue of
236− Just "event" -> do
229+ Just "event" -> do
237− case eventHdrValue of
230+ case eventHdrValue of
238− Just "Records" -> passThrough $ fromIntegral payloadLen
231+ Just "Records" -> passThrough $ fromIntegral payloadLen
239− Just "Cont" -> return ()
232+ Just "Cont" -> return ()
240− Just "Progress" -> do
233+ Just "Progress" -> do
241− bs <- readNBytes $ fromIntegral payloadLen
234+ bs <- readNBytes $ fromIntegral payloadLen
242− progress <- parseSelectProgress bs
235+ progress <- parseSelectProgress bs
243− C.yield $ ProgressEventMessage progress
236+ C.yield $ ProgressEventMessage progress
244− Just "Stats" -> do
237+ Just "Stats" -> do
245− bs <- readNBytes $ fromIntegral payloadLen
238+ bs <- readNBytes $ fromIntegral payloadLen
246− stats <- parseSelectProgress bs
239+ stats <- parseSelectProgress bs
247− C.yield $ StatsEventMessage stats
240+ C.yield $ StatsEventMessage stats
248− Just "End" -> return ()
241+ Just "End" -> return ()
249− _ -> throwIO ESEInvalidMessageType
242+ _ -> throwIO ESEInvalidMessageType
250− when (eventHdrValue /= Just "End") handleMessage
243+ when (eventHdrValue /= Just "End") handleMessage
251−
252− Just "error" -> do
244+ Just "error" -> do
253− let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
245+ let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
254− maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
246+ maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
255−
256− _ -> throwIO ESEInvalidMessageType
247+ _ -> throwIO ESEInvalidMessageType
257−
258 where
248 where
259 passThrough 0 = return ()
249 passThrough 0 = return ()
260 passThrough n = do
250 passThrough n = do
261− let c = min n chunkSize
251+ let c = min n chunkSize
262− b <- readNBytes c
252+ b <- readNBytes c
263− C.yield $ RecordPayloadEventMessage b
253+ C.yield $ RecordPayloadEventMessage b
264− passThrough $ n - B.length b
254+ passThrough $ n - B.length b
265−
255+
266−
267−selectProtoConduit :: MonadUnliftIO m
256+selectProtoConduit ::
257+ (MonadUnliftIO m) =>
268− => C.ConduitT ByteString EventMessage m ()
258+ C.ConduitT ByteString EventMessage m ()
269selectProtoConduit = crcCheck .| handleMessage
259selectProtoConduit = crcCheck .| handleMessage
270
260
271-- | selectObjectContent calls the SelectRequest on the given
261-- | selectObjectContent calls the SelectRequest on the given
272-- object. It returns a Conduit of event messages that can be consumed
262-- object. It returns a Conduit of event messages that can be consumed
273-- by the client.
263-- by the client.
274−selectObjectContent :: Bucket -> Object -> SelectRequest
264+selectObjectContent ::
265+ Bucket ->
266+ Object ->
267+ SelectRequest ->
275− -> Minio (C.ConduitT () EventMessage Minio ())
268+ Minio (C.ConduitT () EventMessage Minio ())
276selectObjectContent b o r = do
269selectObjectContent b o r = do
270+ let reqInfo =
271+ defaultS3ReqInfo
277− let reqInfo = defaultS3ReqInfo { riMethod = HT.methodPost
272+ { riMethod = HT.methodPost,
278− , riBucket = Just b
273+ riBucket = Just b,
279− , riObject = Just o
274+ riObject = Just o,
280− , riPayload = PayloadBS $ mkSelectRequest r
275+ riPayload = PayloadBS $ mkSelectRequest r,
281− , riNeedsLocation = False
276+ riNeedsLocation = False,
282− , riQueryParams = [("select", Nothing), ("select-type", Just "2")]
277+ riQueryParams = [("select", Nothing), ("select-type", Just "2")]
283− }
278+ }
284− --print $ mkSelectRequest r
279+ -- print $ mkSelectRequest r
285− resp <- mkStreamRequest reqInfo
280+ resp <- mkStreamRequest reqInfo
286− return $ NC.responseBody resp .| selectProtoConduit
281+ return $ NC.responseBody resp .| selectProtoConduit
287
282
288-- | A helper conduit that returns only the record payload bytes.
283-- | A helper conduit that returns only the record payload bytes.
289−getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
284+getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
290getPayloadBytes = do
285getPayloadBytes = do
291− evM <- C.await
286+ evM <- C.await
292− case evM of
287+ case evM of
293− Just v -> do
288+ Just v -> do
294− case v of
289+ case v of
295− RecordPayloadEventMessage b -> C.yield b
290+ RecordPayloadEventMessage b -> C.yield b
296− RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
291+ RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
297− _ -> return ()
292+ _ -> return ()
298− getPayloadBytes
293+ getPayloadBytes
299− Nothing -> return ()
294+ Nothing -> return ()
src/Network/Minio/Sign/V4.hs+430−327
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,88 +13,94 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
16+{-# LANGUAGE BangPatterns #-}
17+
17−module Network.Minio.Sign.V4 where
18+module Network.Minio.Sign.V4
18−
19+ ( SignParams (..),
20+ signV4QueryParams,
21+ signV4,
22+ signV4PostPolicy,
23+ signV4Stream,
24+ Service (..),
25+ credentialScope,
26+ )
27+where
28+
19−import qualified Conduit as C
29+import qualified Conduit as C
20−import qualified Data.ByteString as B
30+import qualified Data.ByteArray as BA
31+import qualified Data.ByteString as B
21−import qualified Data.ByteString.Base64 as Base64
32+import qualified Data.ByteString.Base64 as Base64
22−import qualified Data.ByteString.Char8 as B8
33+import qualified Data.ByteString.Char8 as B8
34+import qualified Data.ByteString.Lazy as LB
23−import Data.CaseInsensitive (mk)
35+import Data.CaseInsensitive (mk)
24−import qualified Data.CaseInsensitive as CI
36+import qualified Data.CaseInsensitive as CI
25−import qualified Data.HashMap.Strict as Map
37+import qualified Data.HashMap.Strict as Map
26−import qualified Data.HashSet as Set
38+import qualified Data.HashSet as Set
39+import Data.List (partition)
40+import qualified Data.List.NonEmpty as NE
27−import qualified Data.Time as Time
41+import qualified Data.Time as Time
42+import Lib.Prelude
28−import qualified Network.HTTP.Conduit as NC
43+import qualified Network.HTTP.Conduit as NC
29−import Network.HTTP.Types (Header, parseQuery)
44+import Network.HTTP.Types (Header, SimpleQuery, hContentEncoding, parseQuery)
30−import qualified Network.HTTP.Types as H
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
31−import Text.Printf (printf)
51+import Text.Printf (printf)
32−
33−import Lib.Prelude
34−
35−import Network.Minio.Data.ByteString
36−import Network.Minio.Data.Crypto
37−import Network.Minio.Data.Time
38−import Network.Minio.Errors
39
52
40-- these headers are not included in the string to sign when signing a
53-- these headers are not included in the string to sign when signing a
41-- request
54-- request
42ignoredHeaders :: Set.HashSet ByteString
55ignoredHeaders :: Set.HashSet ByteString
43−ignoredHeaders = Set.fromList $ map CI.foldedCase
56+ignoredHeaders =
57+ Set.fromList $
58+ map
59+ CI.foldedCase
44− [ H.hAuthorization
60+ [ H.hAuthorization,
45− , H.hContentType
61+ H.hContentType,
46− , H.hUserAgent
62+ H.hUserAgent
47− ]
63+ ]
48−
64+
49−data SignV4Data = SignV4Data {
65+data Service = ServiceS3 | ServiceSTS
50− sv4SignTime :: UTCTime
66+ deriving stock (Eq, Show)
51− , sv4Scope :: ByteString
67+
52− , sv4CanonicalRequest :: ByteString
68+toByteString :: Service -> ByteString
53− , sv4HeadersToSign :: [(ByteString, ByteString)]
69+toByteString ServiceS3 = "s3"
54− , sv4Output :: [(ByteString, ByteString)]
70+toByteString ServiceSTS = "sts"
55− , sv4StringToSign :: ByteString
71+
56− , sv4SigningKey :: ByteString
57− } deriving (Show)
58−
59−data SignParams = SignParams {
72+data SignParams = SignParams
60− spAccessKey :: Text
73+ { spAccessKey :: Text,
61− , spSecretKey :: Text
74+ spSecretKey :: BA.ScrubbedBytes,
75+ spSessionToken :: Maybe BA.ScrubbedBytes,
76+ spService :: Service,
62− , spTimeStamp :: UTCTime
77+ spTimeStamp :: UTCTime,
63− , spRegion :: Maybe Text
78+ spRegion :: Maybe Text,
64− , spExpirySecs :: Maybe Int
79+ spExpirySecs :: Maybe UrlExpiry,
65− , spPayloadHash :: Maybe ByteString
80+ spPayloadHash :: Maybe ByteString
66− } deriving (Show)
81+ }
67−
82+ deriving stock (Show)
68−debugPrintSignV4Data :: SignV4Data -> IO ()
69−debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
70− B8.putStrLn "SignV4Data:"
71− B8.putStr "Timestamp: " >> print t
72− B8.putStr "Scope: " >> B8.putStrLn s
73− B8.putStrLn "Canonical Request:"
74− B8.putStrLn cr
75− B8.putStr "Headers to Sign: " >> print h2s
76− B8.putStr "Output: " >> print o
77− B8.putStr "StringToSign: " >> B8.putStrLn sts
78− B8.putStr "SigningKey: " >> printBytes sk
79− B8.putStrLn "END of SignV4Data ========="
80− where
81− printBytes b = do
82− mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b
83− B8.putStrLn ""
84
83
85mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
84mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
86mkAuthHeader accessKey scope signedHeaderKeys sign =
85mkAuthHeader accessKey scope signedHeaderKeys sign =
87− let authValue = B.concat
86+ let authValue =
87+ B.concat
88− [ "AWS4-HMAC-SHA256 Credential="
88+ [ "AWS4-HMAC-SHA256 Credential=",
89− , toS accessKey
89+ encodeUtf8 accessKey,
90− , "/"
90+ "/",
91− , scope
91+ scope,
92− , ", SignedHeaders="
92+ ", SignedHeaders=",
93− , signedHeaderKeys
93+ signedHeaderKeys,
94− , ", Signature="
94+ ", Signature=",
95− , sign
95+ sign
96− ]
96+ ]
97− in (H.hAuthorization, authValue)
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"
98
104
99-- | Given SignParams and request details, including request method,
105-- | Given SignParams and request details, including request method,
100-- request path, headers, query params and payload hash, generates an
106-- request path, headers, query params and payload hash, generates an
@@ -108,124 +114,212 @@ mkAuthHeader accessKey scope signedHeaderKeys sign =
108-- is being created. The expiry is interpreted as an integer number of
114-- is being created. The expiry is interpreted as an integer number of
109-- seconds. The output will be the list of query-parameters to add to
115-- seconds. The output will be the list of query-parameters to add to
110-- the request.
116-- the request.
117+signV4QueryParams :: SignParams -> NC.Request -> SimpleQuery
118+signV4QueryParams !sp !req =
119+ let scope = credentialScope sp
120+ expiry = spExpirySecs sp
121+
122+ headersToSign = getHeadersToSign $ NC.requestHeaders req
123+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
124+ -- query-parameters to be added before signing for presigned URLs
125+ -- (i.e. when `isJust expiry`)
126+ authQP =
127+ [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
128+ ("X-Amz-Credential", B.concat [encodeUtf8 $ spAccessKey sp, "/", scope]),
129+ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
130+ ("X-Amz-Expires", maybe "" showBS expiry),
131+ ("X-Amz-SignedHeaders", signedHeaderKeys)
132+ ]
133+ ++ maybeToList ((amzSecurityToken,) . BA.convert <$> spSessionToken sp)
134+ finalQP =
135+ parseQuery (NC.queryString req)
136+ ++ if isJust expiry
137+ then (fmap . fmap) Just authQP
138+ else []
139+ -- 1. compute canonical request
140+ canonicalRequest =
141+ mkCanonicalRequest
142+ False
143+ sp
144+ (NC.setQueryString finalQP req)
145+ headersToSign
146+
147+ -- 2. compute string to sign
148+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
149+ -- 3.1 compute signing key
150+ signingKey = getSigningKey sp
151+ -- 3.2 compute signature
152+ signature = computeSignature stringToSign signingKey
153+ in ("X-Amz-Signature", signature) : authQP
154+
155+-- | Given SignParams and request details, including request method, request
156+-- path, headers, query params and payload hash, generates an updated set of
157+-- headers, including the x-amz-date header and the Authorization header, which
158+-- includes the signature.
159+--
160+-- The output is the list of headers to be added to authenticate the request.
111−signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
161+signV4 :: SignParams -> NC.Request -> [Header]
112signV4 !sp !req =
162signV4 !sp !req =
113− let
163+ let scope = credentialScope sp
114− region = fromMaybe "" $ spRegion sp
164+
115− ts = spTimeStamp sp
165+ -- extra headers to be added for signing purposes.
116− scope = mkScope ts region
166+ extraHeaders =
117− accessKey = toS $ spAccessKey sp
167+ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp)
118− secretKey = toS $ spSecretKey sp
168+ : ( -- payload hash is only used for S3 (not STS)
119− expiry = spExpirySecs sp
120− sha256Hdr = ("x-amz-content-sha256",
169+ [ ( "x-amz-content-sha256",
121− fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp)
170+ fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
122−
171+ )
123− -- headers to be added to the request
172+ | spService sp == ServiceS3
124− datePair = ("X-Amz-Date", awsTimeFormatBS ts)
173+ ]
125− computedHeaders = NC.requestHeaders req ++
174+ )
126− if isJust $ expiry
175+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
127− then []
176+
128− else map (\(x, y) -> (mk x, y)) [datePair, sha256Hdr]
129− headersToSign = getHeadersToSign computedHeaders
130− signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
131−
132− -- query-parameters to be added before signing for presigned URLs
133− -- (i.e. when `isJust expiry`)
134− authQP = [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
135− , ("X-Amz-Credential", B.concat [accessKey, "/", scope])
136− , datePair
137− , ("X-Amz-Expires", maybe "" show expiry)
138− , ("X-Amz-SignedHeaders", signedHeaderKeys)
139− ]
140− finalQP = parseQuery (NC.queryString req) ++
141− if isJust expiry
142− then (fmap . fmap) Just authQP
143− else []
144−
145− -- 1. compute canonical request
177+ -- 1. compute canonical request
146− canonicalRequest = mkCanonicalRequest False sp
178+ reqHeaders = NC.requestHeaders req ++ extraHeaders
147− (NC.setQueryString finalQP req)
179+ (canonicalRequest, signedHeaderKeys) =
148− headersToSign
180+ getCanonicalRequestAndSignedHeaders
149−
181+ NotStreaming
182+ sp
183+ req
184+ reqHeaders
185+
150− -- 2. compute string to sign
186+ -- 2. compute string to sign
151− stringToSign = mkStringToSign ts scope canonicalRequest
187+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
152−
153− -- 3.1 compute signing key
188+ -- 3.1 compute signing key
154− signingKey = mkSigningKey ts region secretKey
189+ signingKey = getSigningKey sp
155−
156− -- 3.2 compute signature
190+ -- 3.2 compute signature
157− signature = computeSignature stringToSign signingKey
191+ signature = computeSignature stringToSign signingKey
158−
159− -- 4. compute auth header
192+ -- 4. compute auth header
160− authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
193+ authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
161−
194+ in authHeader : extraHeaders
162− -- finally compute output pairs
195+
163− output = if isJust expiry
196+credentialScope :: SignParams -> ByteString
164− then ("X-Amz-Signature", signature) : authQP
197+credentialScope sp =
165− else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
198+ let region = fromMaybe "" $ spRegion sp
166− datePair, sha256Hdr]
167−
168− in output
169−
170−
171−mkScope :: UTCTime -> Text -> ByteString
172−mkScope ts region = B.intercalate "/"
199+ in B.intercalate
200+ "/"
173− [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts
201+ [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" $ spTimeStamp sp,
174− , toS region
202+ encodeUtf8 region,
175− , "s3"
203+ toByteString $ spService sp,
176− , "aws4_request"
204+ "aws4_request"
177− ]
205+ ]
178
206
207+-- Folds header name, trims whitespace in header values, skips ignored headers
208+-- and sorts headers.
179getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
209getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
180getHeadersToSign !h =
210getHeadersToSign !h =
181 filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
211 filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
182− map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
212+ map (bimap CI.foldedCase stripBS) h
183−
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 ->
184−mkCanonicalRequest :: Bool -> SignParams -> NC.Request -> [(ByteString, ByteString)]
274+ [(ByteString, ByteString)] ->
185− -> ByteString
275+ ByteString
186mkCanonicalRequest !isStreaming !sp !req !headersForSign =
276mkCanonicalRequest !isStreaming !sp !req !headersForSign =
187− let
277+ let httpMethod = NC.method req
278+ canonicalUri = uriEncode False $ NC.path req
188− canonicalQueryString = B.intercalate "&" $
279+ canonicalQueryString =
280+ B.intercalate "&" $
189− map (\(x, y) -> B.concat [x, "=", y]) $
281+ map (\(x, y) -> B.concat [x, "=", y]) $
190− sort $ map (\(x, y) ->
282+ sortBy (\a b -> compare (fst a) (fst b)) $
283+ map
191− (uriEncode True x, maybe "" (uriEncode True) y)) $
284+ ( bimap (uriEncode True) (maybe "" (uriEncode True))
285+ )
192− (parseQuery $ NC.queryString req)
286+ (parseQuery $ NC.queryString req)
193−
194− sortedHeaders = sort headersForSign
287+ sortedHeaders = sort headersForSign
195−
196− canonicalHeaders = B.concat $
288+ canonicalHeaders =
289+ B.concat $
197− map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
290+ map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
198−
199− signedHeaders = B.intercalate ";" $ map fst sortedHeaders
291+ signedHeaders = B.intercalate ";" $ map fst sortedHeaders
200−
201− payloadHashStr =
292+ payloadHashStr =
202 if isStreaming
293 if isStreaming
203− then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
294+ then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
204− else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
295+ else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
205− in
206− B.intercalate "\n"
296+ in B.intercalate
207− [ NC.method req
297+ "\n"
208− , uriEncode False $ NC.path req
298+ [ httpMethod,
299+ canonicalUri,
209− , canonicalQueryString
300+ canonicalQueryString,
210− , canonicalHeaders
301+ canonicalHeaders,
211− , signedHeaders
302+ signedHeaders,
212− , payloadHashStr
303+ payloadHashStr
213− ]
304+ ]
214
305
215mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
306mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
216−mkStringToSign ts !scope !canonicalRequest = B.intercalate "\n"
307+mkStringToSign ts !scope !canonicalRequest =
308+ B.intercalate
309+ "\n"
217− [ "AWS4-HMAC-SHA256"
310+ [ "AWS4-HMAC-SHA256",
218− , awsTimeFormatBS ts
311+ awsTimeFormatBS ts,
219− , scope
312+ scope,
220− , hashSHA256 canonicalRequest
313+ hashSHA256 canonicalRequest
221− ]
314+ ]
222−
315+
223−mkSigningKey :: UTCTime -> Text -> ByteString -> ByteString
316+getSigningKey :: SignParams -> ByteString
317+getSigningKey sp =
224−mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"
318+ hmacSHA256RawBS "aws4_request"
225− . hmacSHA256RawBS "s3"
319+ . hmacSHA256RawBS (toByteString $ spService sp)
226− . hmacSHA256RawBS (toS region)
320+ . hmacSHA256RawBS (encodeUtf8 $ fromMaybe "" $ spRegion sp)
227− . hmacSHA256RawBS (awsDateFormatBS ts)
321+ . hmacSHA256RawBS (awsDateFormatBS $ spTimeStamp sp)
228− $ B.concat ["AWS4", secretKey]
322+ $ B.concat ["AWS4", BA.convert $ spSecretKey sp]
229
323
230computeSignature :: ByteString -> ByteString -> ByteString
324computeSignature :: ByteString -> ByteString -> ByteString
231computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
325computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -233,159 +327,168 @@ computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
233-- | Takes a validated Post Policy JSON bytestring, the signing time,
327-- | Takes a validated Post Policy JSON bytestring, the signing time,
234-- and ConnInfo and returns form-data for the POST upload containing
328-- and ConnInfo and returns form-data for the POST upload containing
235-- just the signature and the encoded post-policy.
329-- just the signature and the encoded post-policy.
236−signV4PostPolicy :: ByteString -> SignParams
330+signV4PostPolicy ::
331+ ByteString ->
332+ SignParams ->
237− -> Map.HashMap Text ByteString
333+ Map.HashMap Text ByteString
238signV4PostPolicy !postPolicyJSON !sp =
334signV4PostPolicy !postPolicyJSON !sp =
239− let
240− stringToSign = Base64.encode postPolicyJSON
335+ let stringToSign = Base64.encode postPolicyJSON
241− region = fromMaybe "" $ spRegion sp
336+ signingKey = getSigningKey sp
242− signingKey = mkSigningKey (spTimeStamp sp) region $ toS $ spSecretKey sp
243− signature = computeSignature stringToSign signingKey
337+ signature = computeSignature stringToSign signingKey
244− in
338+ in Map.fromList $
245− Map.fromList [ ("x-amz-signature", signature)
339+ [ ("x-amz-signature", signature),
246− , ("policy", stringToSign)
340+ ("policy", stringToSign)
247− ]
341+ ]
342+ ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
248
343
249chunkSizeConstant :: Int
344chunkSizeConstant :: Int
250chunkSizeConstant = 64 * 1024
345chunkSizeConstant = 64 * 1024
251
346
252-- base16Len computes the number of bytes required to represent @n (> 0)@ in
347-- base16Len computes the number of bytes required to represent @n (> 0)@ in
253-- hexadecimal.
348-- hexadecimal.
254−base16Len :: Integral a => a -> Int
349+base16Len :: (Integral a) => a -> Int
255−base16Len n | n == 0 = 0
350+base16Len n
351+ | n == 0 = 0
256− | otherwise = 1 + base16Len (n `div` 16)
352+ | otherwise = 1 + base16Len (n `div` 16)
257
353
258signedStreamLength :: Int64 -> Int64
354signedStreamLength :: Int64 -> Int64
259signedStreamLength dataLen =
355signedStreamLength dataLen =
260− let
261− chunkSzInt = fromIntegral chunkSizeConstant
356+ let chunkSzInt = fromIntegral chunkSizeConstant
262− (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
357+ (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
263−
264−
265− -- Structure of a chunk:
358+ -- Structure of a chunk:
266− -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
359+ -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
267− encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
360+ encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
268− fullChunkSize = encodedChunkLen chunkSzInt
361+ fullChunkSize = encodedChunkLen chunkSzInt
269− lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
362+ lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
270− finalChunkSize = 1 + 17 + 64 + 2 + 2
363+ finalChunkSize = 1 + 17 + 64 + 2 + 2
271− in
272− numChunks * fullChunkSize + lastChunkSize + finalChunkSize
364+ in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
273−
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+
274−signV4Stream :: Int64 -> SignParams -> NC.Request
377+signV4Stream ::
275− -> (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
378+ Int64 ->
379+ SignParams ->
380+ NC.Request ->
276− -- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
381+ (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
277signV4Stream !payloadLength !sp !req =
382signV4Stream !payloadLength !sp !req =
278− let
279− ts = spTimeStamp sp
383+ let ts = spTimeStamp sp
280−
384+
281− addContentEncoding hs =
385+ -- compute the updated list of headers to be added for signing purposes.
282− let ceMay = headMay $ filter (\(x, _) -> x == "content-encoding") hs
386+ signedContentLength = signedStreamLength payloadLength
283− in case ceMay of
387+ extraHeaders =
284− Nothing -> ("content-encoding", "aws-chunked") : hs
285− Just (_, ce) -> ("content-encoding", ce <> ",aws-chunked") :
286− filter (\(x, _) -> x /= "content-encoding") hs
287−
288− -- headers to be added to the request
289− datePair = ("X-Amz-Date", awsTimeFormatBS ts)
388+ [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
290− computedHeaders = addContentEncoding $
291− datePair : NC.requestHeaders req
292−
293− -- headers specific to streaming signature
294− signedContentLength = signedStreamLength payloadLength
295− streamingHeaders :: [Header]
296− streamingHeaders =
297− [ ("x-amz-decoded-content-length", show payloadLength)
389+ ("x-amz-decoded-content-length", showBS payloadLength),
298− , ("content-length", show signedContentLength )
390+ ("content-length", showBS signedContentLength),
299− , ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
391+ ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
300− ]
301− headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders
302− signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
303− finalQP = parseQuery (NC.queryString req)
304−
305− -- 1. Compute Seed Signature
306− -- 1.1 Canonical Request
307− canonicalReq = mkCanonicalRequest True sp
308− (NC.setQueryString finalQP req)
309− headersToSign
310−
311− region = fromMaybe "" $ spRegion sp
312− scope = mkScope ts region
313− accessKey = spAccessKey sp
314− secretKey = spSecretKey sp
315−
316− -- 1.2 String toSign
317− stringToSign = mkStringToSign ts scope canonicalReq
318−
319− -- 1.3 Compute signature
320− -- 1.3.1 compute signing key
321− signingKey = mkSigningKey ts region $ toS secretKey
322−
323− -- 1.3.2 Compute signature
324− seedSignature = computeSignature stringToSign signingKey
325−
326− -- 1.3.3 Compute Auth Header
327− authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
328−
329− -- 1.4 Updated headers for the request
330− finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)
331− -- headersToAdd = authHeader : datePair : streamingHeaders
332−
333− toHexStr n = B8.pack $ printf "%x" n
334−
335− (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
336−
337− -- Function to compute string to sign for each chunk.
338− chunkStrToSign prevSign currChunkHash =
339− B.intercalate "\n"
340− [ "AWS4-HMAC-SHA256-PAYLOAD"
341− , awsTimeFormatBS ts
342− , scope
343− , prevSign
344− , hashSHA256 ""
345− , currChunkHash
346 ]
392 ]
347−
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+ ]
348− -- Read n byte from upstream and return a strict bytestring.
435+ -- Read n byte from upstream and return a strict bytestring.
349− mustTakeN n = do
436+ mustTakeN n = do
350− bs <- toS <$> (C.takeCE n C..| C.sinkLazy)
437+ bs <- LB.toStrict <$> (C.takeCE n C..| C.sinkLazy)
351 when (B.length bs /= n) $
438 when (B.length bs /= n) $
352− throwIO MErrVStreamingBodyUnexpectedEOF
439+ throwIO MErrVStreamingBodyUnexpectedEOF
353 return bs
440 return bs
354−
355− signerConduit n lps prevSign =
441+ signerConduit n lps prevSign =
356− -- First case encodes a full chunk of length
442+ -- First case encodes a full chunk of length
357− -- 'chunkSizeConstant'.
443+ -- 'chunkSizeConstant'.
444+ if
358− if | n > 0 -> do
445+ | n > 0 -> do
359− bs <- mustTakeN chunkSizeConstant
446+ bs <- mustTakeN chunkSizeConstant
360− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
447+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
361− nextSign = computeSignature strToSign signingKey
448+ nextSign = computeSignature strToSign signingKey
449+ chunkBS =
362− chunkBS = toHexStr chunkSizeConstant
450+ toHexStr chunkSizeConstant
363− <> ";chunk-signature="
451+ <> ";chunk-signature="
364− <> nextSign <> "\r\n" <> bs <> "\r\n"
452+ <> nextSign
453+ <> "\r\n"
454+ <> bs
455+ <> "\r\n"
365− C.yield chunkBS
456+ C.yield chunkBS
366− signerConduit (n-1) lps nextSign
457+ signerConduit (n - 1) lps nextSign
367−
458+
368− -- Second case encodes the last chunk which is smaller than
459+ -- Second case encodes the last chunk which is smaller than
369− -- 'chunkSizeConstant'
460+ -- 'chunkSizeConstant'
370− | lps > 0 -> do
461+ | lps > 0 -> do
371− bs <- mustTakeN $ fromIntegral lps
462+ bs <- mustTakeN $ fromIntegral lps
372− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
463+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
373− nextSign = computeSignature strToSign signingKey
464+ nextSign = computeSignature strToSign signingKey
465+ chunkBS =
466+ toHexStr lps
374− chunkBS = toHexStr lps <> ";chunk-signature="
467+ <> ";chunk-signature="
375− <> nextSign <> "\r\n" <> bs <> "\r\n"
468+ <> nextSign
469+ <> "\r\n"
470+ <> bs
471+ <> "\r\n"
376− C.yield chunkBS
472+ C.yield chunkBS
377− signerConduit 0 0 nextSign
473+ signerConduit 0 0 nextSign
378−
474+
379− -- Last case encodes the final signature chunk that has no
475+ -- Last case encodes the final signature chunk that has no
380− -- data.
476+ -- data.
381− | otherwise -> do
477+ | otherwise -> do
382− let strToSign = chunkStrToSign prevSign (hashSHA256 "")
478+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")
383− nextSign = computeSignature strToSign signingKey
479+ nextSign = computeSignature strToSign signingKey
384− lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
480+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
385− C.yield lastChunkBS
481+ C.yield lastChunkBS
386− in
482+ in \src ->
483+ req
387− \src -> req { NC.requestHeaders = finalReqHeaders
484+ { NC.requestHeaders = finalReqHeaders,
388− , NC.requestBody =
485+ NC.requestBody =
389− NC.requestBodySource signedContentLength $
486+ NC.requestBodySource signedContentLength $
390− src C..| signerConduit numParts lastPSize seedSignature
487+ src C..| signerConduit numParts lastPSize seedSignature
391− }
488+ }
489+
490+-- "setHeader r hdr" adds the hdr to r, replacing it in r if it already exists.
491+setHeader :: Header -> RequestHeaders -> RequestHeaders
492+setHeader hdr r =
493+ let r' = filter (\(name, _) -> name /= fst hdr) r
494+ in hdr : r'
src/Network/Minio/Utils.hs+140−127
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -16,70 +16,77 @@
16
16
17module Network.Minio.Utils where
17module Network.Minio.Utils where
18
18
19−import qualified Conduit as C
19+import qualified Conduit as C
20−import Control.Monad.IO.Unlift (MonadUnliftIO)
20+import Control.Monad.IO.Unlift (MonadUnliftIO)
21−import qualified Control.Monad.Trans.Resource as R
21+import qualified Control.Monad.Trans.Resource as R
22−import qualified Data.ByteString as B
22+import qualified Data.ByteString as B
23−import qualified Data.ByteString.Lazy as LB
23+import qualified Data.ByteString.Lazy as LB
24−import Data.CaseInsensitive (mk, original)
24+import Data.CaseInsensitive (mk, original)
25−import qualified Data.Conduit.Binary as CB
25+import qualified Data.Conduit.Binary as CB
26−import qualified Data.HashMap.Strict as H
26+import qualified Data.HashMap.Strict as H
27−import qualified Data.List as List
27+import qualified Data.Text as T
28−import qualified Data.Text as T
29−import Data.Text.Read (decimal)
28+import Data.Text.Read (decimal)
29+import Data.Time
30−import Data.Time (defaultTimeLocale, parseTimeM,
30+ ( defaultTimeLocale,
31+ parseTimeM,
31− rfc822DateFormat)
32+ rfc822DateFormat,
33+ )
34+import Lib.Prelude
32−import Network.HTTP.Conduit (Response)
35+import Network.HTTP.Conduit (Response)
33−import qualified Network.HTTP.Conduit as NC
36+import qualified Network.HTTP.Conduit as NC
34−import qualified Network.HTTP.Types as HT
37+import qualified Network.HTTP.Types as HT
35−import qualified Network.HTTP.Types.Header as Hdr
38+import qualified Network.HTTP.Types.Header as Hdr
39+import Network.Minio.Data.ByteString
40+import Network.Minio.JsonParser (parseErrResponseJSON)
41+import Network.Minio.XmlCommon (parseErrResponse)
36−import qualified System.IO as IO
42+import qualified System.IO as IO
37−import qualified UnliftIO as U
43+import qualified UnliftIO as U
38−import qualified UnliftIO.Async as A
44+import qualified UnliftIO.Async as A
39−import qualified UnliftIO.MVar as UM
45+
40−
46+allocateReadFile ::
41−import Lib.Prelude
42−
43−import Network.Minio.Data
44−import Network.Minio.Data.ByteString
45−import Network.Minio.JsonParser (parseErrResponseJSON)
46−import Network.Minio.XmlParser (parseErrResponse)
47−
48−allocateReadFile :: (MonadUnliftIO m, R.MonadResource m)
47+ (MonadUnliftIO m, R.MonadResource m) =>
48+ FilePath ->
49− => FilePath -> m (R.ReleaseKey, Handle)
49+ m (R.ReleaseKey, Handle)
50allocateReadFile fp = do
50allocateReadFile fp = do
51 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
51 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
52− either (\(e :: IOException) -> throwIO e) (return . (rk,)) hdlE
52+ either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
53 where
53 where
54 openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
54 openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
55 cleanup = either (const $ return ()) IO.hClose
55 cleanup = either (const $ return ()) IO.hClose
56
56
57-- | Queries the file size from the handle. Catches any file operation
57-- | Queries the file size from the handle. Catches any file operation
58-- exceptions and returns Nothing instead.
58-- exceptions and returns Nothing instead.
59+getFileSize ::
59−getFileSize :: (MonadUnliftIO m, R.MonadResource m)
60+ (MonadUnliftIO m) =>
61+ Handle ->
60− => Handle -> m (Maybe Int64)
62+ m (Maybe Int64)
61getFileSize h = do
63getFileSize h = do
62 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
64 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
63 case resE of
65 case resE of
64− Left (_ :: IOException) -> return Nothing
66+ Left (_ :: U.IOException) -> return Nothing
65− Right s -> return $ Just s
67+ Right s -> return $ Just s
66
68
67-- | Queries if handle is seekable. Catches any file operation
69-- | Queries if handle is seekable. Catches any file operation
68-- exceptions and return False instead.
70-- exceptions and return False instead.
69−isHandleSeekable :: (R.MonadResource m, MonadUnliftIO m)
71+isHandleSeekable ::
72+ (R.MonadResource m) =>
70− => Handle -> m Bool
73+ Handle ->
74+ m Bool
71isHandleSeekable h = do
75isHandleSeekable h = do
72 resE <- liftIO $ try $ IO.hIsSeekable h
76 resE <- liftIO $ try $ IO.hIsSeekable h
73 case resE of
77 case resE of
74− Left (_ :: IOException) -> return False
78+ Left (_ :: U.IOException) -> return False
75− Right v -> return v
79+ Right v -> return v
76
80
77-- | Helper function that opens a handle to the filepath and performs
81-- | Helper function that opens a handle to the filepath and performs
78-- the given action on it. Exceptions of type MError are caught and
82-- the given action on it. Exceptions of type MError are caught and
79-- returned - both during file handle allocation and when the action
83-- returned - both during file handle allocation and when the action
80-- is run.
84-- is run.
85+withNewHandle ::
81−withNewHandle :: (MonadUnliftIO m, R.MonadResource m)
86+ (MonadUnliftIO m, R.MonadResource m) =>
87+ FilePath ->
88+ (Handle -> m a) ->
82− => FilePath -> (Handle -> m a) -> m (Either IOException a)
89+ m (Either U.IOException a)
83withNewHandle fp fileAction = do
90withNewHandle fp fileAction = do
84 -- opening a handle can throw MError exception.
91 -- opening a handle can throw MError exception.
85 handleE <- try $ allocateReadFile fp
92 handleE <- try $ allocateReadFile fp
@@ -93,34 +100,61 @@ withNewHandle fp fileAction = do
93 return resE
100 return resE
94
101
95mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
102mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
96−mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
103+mkHeaderFromPairs = map (first mk)
97
104
98lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
105lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
99−lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
106+lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
100
107
101getETagHeader :: [HT.Header] -> Maybe Text
108getETagHeader :: [HT.Header] -> Maybe Text
102getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
109getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
103
110
104getMetadata :: [HT.Header] -> [(Text, Text)]
111getMetadata :: [HT.Header] -> [(Text, Text)]
105getMetadata =
112getMetadata =
106− map ((\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y)))
113+ map (\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y))
114+
115+-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
116+-- stripped and a Just is returned.
117+userMetadataHeaderNameMaybe :: Text -> Maybe Text
118+userMetadataHeaderNameMaybe k =
119+ let prefix = T.toCaseFold "X-Amz-Meta-"
120+ n = T.length prefix
121+ in if T.toCaseFold (T.take n k) == prefix
122+ then Just (T.drop n k)
123+ else Nothing
107
124
108toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
125toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
109toMaybeMetadataHeader (k, v) =
126toMaybeMetadataHeader (k, v) =
110− (, v) <$> userMetadataHeaderNameMaybe k
127+ (,v) <$> userMetadataHeaderNameMaybe k
111
128
112getNonUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
129getNonUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
113−getNonUserMetadataMap = H.fromList
130+getNonUserMetadataMap =
131+ H.fromList
132+ . filter
114− . filter ( isNothing
133+ ( isNothing
115− . userMetadataHeaderNameMaybe
134+ . userMetadataHeaderNameMaybe
116− . fst
135+ . fst
117− )
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))
118
145
119-- | This function collects all headers starting with `x-amz-meta-`
146-- | This function collects all headers starting with `x-amz-meta-`
120-- and strips off this prefix, and returns a map.
147-- and strips off this prefix, and returns a map.
121getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
148getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
122−getUserMetadataMap = H.fromList
149+getUserMetadataMap =
150+ H.fromList
123− . mapMaybe toMaybeMetadataHeader
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_
124
158
125getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
159getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
126getLastModifiedHeader hs = do
160getLastModifiedHeader hs = do
@@ -130,19 +164,21 @@ getLastModifiedHeader hs = do
130getContentLength :: [HT.Header] -> Maybe Int64
164getContentLength :: [HT.Header] -> Maybe Int64
131getContentLength hs = do
165getContentLength hs = do
132 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
166 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
133− fst <$> hush (decimal nbs)
167+ fst <$> either (const Nothing) Just (decimal nbs)
134−
135
168
136decodeUtf8Lenient :: ByteString -> Text
169decodeUtf8Lenient :: ByteString -> Text
137decodeUtf8Lenient = decodeUtf8With lenientDecode
170decodeUtf8Lenient = decodeUtf8With lenientDecode
138
171
139isSuccessStatus :: HT.Status -> Bool
172isSuccessStatus :: HT.Status -> Bool
173+isSuccessStatus sts =
140−isSuccessStatus sts = let s = HT.statusCode sts
174+ let s = HT.statusCode sts
141− in (s >= 200 && s < 300)
175+ in (s >= 200 && s < 300)
142−
176+
177+httpLbs ::
143−httpLbs :: MonadIO m
178+ (MonadIO m) =>
144− => NC.Request -> NC.Manager
179+ NC.Request ->
180+ NC.Manager ->
145− -> m (NC.Response LByteString)
181+ m (NC.Response LByteString)
146httpLbs req mgr = do
182httpLbs req mgr = do
147 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
183 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
148 resp <- either throwIO return respE
184 resp <- either throwIO return respE
@@ -154,21 +190,26 @@ httpLbs req mgr = do
154 Just "application/json" -> do
190 Just "application/json" -> do
155 sErr <- parseErrResponseJSON $ NC.responseBody resp
191 sErr <- parseErrResponseJSON $ NC.responseBody resp
156 throwIO sErr
192 throwIO sErr
157−
193+ _ ->
194+ throwIO $
158− _ -> throwIO $ NC.HttpExceptionRequest req $
195+ NC.HttpExceptionRequest req $
159− NC.StatusCodeException (void resp) (show resp)
196+ NC.StatusCodeException (void resp) (showBS resp)
160
197
161 return resp
198 return resp
162 where
199 where
200+ tryHttpEx ::
163− tryHttpEx :: IO (NC.Response LByteString)
201+ IO (NC.Response LByteString) ->
164− -> IO (Either NC.HttpException (NC.Response LByteString))
202+ IO (Either NC.HttpException (NC.Response LByteString))
165 tryHttpEx = try
203 tryHttpEx = try
204+ contentTypeMay resp =
166− contentTypeMay resp = lookupHeader Hdr.hContentType $
205+ lookupHeader Hdr.hContentType $
167− NC.responseHeaders resp
206+ NC.responseHeaders resp
168−
207+
208+http ::
169−http :: (MonadUnliftIO m, R.MonadResource m)
209+ (MonadUnliftIO m, R.MonadResource m) =>
170− => NC.Request -> NC.Manager
210+ NC.Request ->
211+ NC.Manager ->
171− -> m (Response (C.ConduitT () ByteString m ()))
212+ m (Response (C.ConduitT () ByteString m ()))
172http req mgr = do
213http req mgr = do
173 respE <- tryHttpEx $ NC.http req mgr
214 respE <- tryHttpEx $ NC.http req mgr
174 resp <- either throwIO return respE
215 resp <- either throwIO return respE
@@ -178,25 +219,31 @@ http req mgr = do
178 respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
219 respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
179 sErr <- parseErrResponse respBody
220 sErr <- parseErrResponse respBody
180 throwIO sErr
221 throwIO sErr
181−
182 _ -> do
222 _ -> do
183 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
223 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
224+ throwIO $
184− throwIO $ NC.HttpExceptionRequest req $
225+ NC.HttpExceptionRequest req $
185− NC.StatusCodeException (void resp) content
226+ NC.StatusCodeException (void resp) content
186−
187
227
188 return resp
228 return resp
189 where
229 where
230+ tryHttpEx ::
190− tryHttpEx :: (MonadUnliftIO m) => m a
231+ (MonadUnliftIO m) =>
232+ m a ->
191− -> m (Either NC.HttpException a)
233+ m (Either NC.HttpException a)
192 tryHttpEx = try
234 tryHttpEx = try
235+ contentTypeMay resp =
193− contentTypeMay resp = lookupHeader Hdr.hContentType $
236+ lookupHeader Hdr.hContentType $
194− NC.responseHeaders resp
237+ NC.responseHeaders resp
195
238
196-- Similar to mapConcurrently but limits the number of threads that
239-- Similar to mapConcurrently but limits the number of threads that
197-- can run using a quantity semaphore.
240-- can run using a quantity semaphore.
198−limitedMapConcurrently :: MonadUnliftIO m
241+limitedMapConcurrently ::
242+ (MonadUnliftIO m) =>
199− => Int -> (t -> m a) -> [t] -> m [a]
243+ Int ->
244+ (t -> m a) ->
245+ [t] ->
246+ m [a]
200limitedMapConcurrently 0 _ _ = return []
247limitedMapConcurrently 0 _ _ = return []
201limitedMapConcurrently count act args = do
248limitedMapConcurrently count act args = do
202 t' <- U.newTVarIO count
249 t' <- U.newTVarIO count
@@ -205,17 +252,15 @@ limitedMapConcurrently count act args = do
205 where
252 where
206 wThread t arg =
253 wThread t arg =
207 U.bracket_ (waitSem t) (signalSem t) $ act arg
254 U.bracket_ (waitSem t) (signalSem t) $ act arg
208−
209 -- quantity semaphore implementation using TVar
255 -- quantity semaphore implementation using TVar
210 waitSem t = U.atomically $ do
256 waitSem t = U.atomically $ do
211 v <- U.readTVar t
257 v <- U.readTVar t
212 if v > 0
258 if v > 0
213− then U.writeTVar t (v-1)
259+ then U.writeTVar t (v - 1)
214− else U.retrySTM
260+ else U.retrySTM
215−
216 signalSem t = U.atomically $ do
261 signalSem t = U.atomically $ do
217 v <- U.readTVar t
262 v <- U.readTVar t
218− U.writeTVar t (v+1)
263+ U.writeTVar t (v + 1)
219
264
220-- helper function to 'drop' empty optional parameter.
265-- helper function to 'drop' empty optional parameter.
221mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
266mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -224,7 +269,7 @@ mkQuery k mv = (k,) <$> mv
224-- helper function to build query parameters that are optional.
269-- helper function to build query parameters that are optional.
225-- don't use it with mandatory query params with empty value.
270-- don't use it with mandatory query params with empty value.
226mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
271mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
227−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
272+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
228
273
229-- | Conduit that rechunks bytestrings into the given chunk
274-- | Conduit that rechunks bytestrings into the given chunk
230-- lengths. Stops after given chunk lengths are yielded. Stops if
275-- lengths. Stops after given chunk lengths are yielded. Stops if
@@ -232,41 +277,9 @@ mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
232-- received. Does not throw any errors.
277-- received. Does not throw any errors.
233chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
278chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
234chunkBSConduit [] = return ()
279chunkBSConduit [] = return ()
235−chunkBSConduit (s:ss) = do
280+chunkBSConduit (s : ss) = do
236− bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
281+ bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
282+ if
237− if | B.length bs == s -> C.yield bs >> chunkBSConduit ss
283+ | B.length bs == s -> C.yield bs >> chunkBSConduit ss
238− | B.length bs > 0 -> C.yield bs
284+ | B.length bs > 0 -> C.yield bs
239− | otherwise -> return ()
240−
241−-- | Select part sizes - the logic is that the minimum part-size will
242−-- be 64MiB.
243−selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
244−selectPartSizes size = uncurry (List.zip3 [1..]) $
245− List.unzip $ loop 0 size
246− where
247− ceil :: Double -> Int64
248− ceil = ceiling
249− partSize = max minPartSize (ceil $ fromIntegral size /
250− fromIntegral maxMultipartParts)
251−
252− m = fromIntegral partSize
253− loop st sz
254− | st > sz = []
255− | st + m >= sz = [(st, sz - st)]
256− | otherwise = (st, m) : loop (st + m) sz
285+ | otherwise -> return ()
257−
258−lookupRegionCache :: Bucket -> Minio (Maybe Region)
259−lookupRegionCache b = do
260− rMVar <- asks mcRegionMap
261− rMap <- UM.readMVar rMVar
262− return $ H.lookup b rMap
263−
264−addToRegionCache :: Bucket -> Region -> Minio ()
265−addToRegionCache b region = do
266− rMVar <- asks mcRegionMap
267− UM.modifyMVar_ rMVar $ return . H.insert b region
268−
269−deleteFromRegionCache :: Bucket -> Minio ()
270−deleteFromRegionCache b = do
271− rMVar <- asks mcRegionMap
272− UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/XmlCommon.hsnew file+65
@@ -0,0 +1,65 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.XmlCommon where
18+
19+import qualified Data.Text as T
20+import Data.Text.Read (decimal)
21+import Data.Time (UTCTime)
22+import Data.Time.Format.ISO8601 (iso8601ParseM)
23+import Lib.Prelude (throwIO)
24+import Network.Minio.Errors
25+import Text.XML (Name (Name), def, parseLBS)
26+import Text.XML.Cursor (Axis, Cursor, content, element, fromDocument, laxElement, ($/), (&/))
27+
28+s3Name :: Text -> Text -> Name
29+s3Name ns s = Name s (Just ns) Nothing
30+
31+uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
32+uncurry4 f (a, b, c, d) = f a b c d
33+
34+uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
35+uncurry6 f (a, b, c, d, e, g) = f a b c d e g
36+
37+-- | Parse time strings from XML
38+parseS3XMLTime :: (MonadIO m) => Text -> m UTCTime
39+parseS3XMLTime t =
40+ maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
41+ iso8601ParseM $
42+ toString t
43+
44+parseDecimal :: (MonadIO m, Integral a) => Text -> m a
45+parseDecimal numStr =
46+ either (throwIO . MErrVXmlParse . show) return $
47+ fst <$> decimal numStr
48+
49+parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
50+parseDecimals numStr = forM numStr parseDecimal
51+
52+s3Elem :: Text -> Text -> Axis
53+s3Elem ns = element . s3Name ns
54+
55+parseRoot :: (MonadIO m) => LByteString -> m Cursor
56+parseRoot =
57+ either (throwIO . MErrVXmlParse . show) (return . fromDocument)
58+ . parseLBS def
59+
60+parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
61+parseErrResponse xmldata = do
62+ r <- parseRoot xmldata
63+ let code = T.concat $ r $/ laxElement "Code" &/ content
64+ message = T.concat $ r $/ laxElement "Message" &/ content
65+ return $ toServiceErr code message
src/Network/Minio/XmlGenerator.hs+165−99
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,89 +15,112 @@
15--
15--
16
16
17module Network.Minio.XmlGenerator
17module Network.Minio.XmlGenerator
18− ( mkCreateBucketConfig
18+ ( mkCreateBucketConfig,
19− , mkCompleteMultipartUploadRequest
19+ mkCompleteMultipartUploadRequest,
20− , mkPutNotificationRequest
20+ mkPutNotificationRequest,
21− , mkSelectRequest
21+ mkSelectRequest,
22+ )
22− ) where
23+where
23−
24
24
25import qualified Data.ByteString.Lazy as LBS
25import qualified Data.ByteString.Lazy as LBS
26−import qualified Data.HashMap.Strict as H
26+import qualified Data.Text as T
27−import qualified Data.Text as T
27+import Network.Minio.Data
28+import Network.Minio.XmlCommon
28−import Text.XML
29+import Text.XML
29−
30−import Lib.Prelude
31−
32−import Network.Minio.Data
33−
34
30
35-- | Create a bucketConfig request body XML
31-- | Create a bucketConfig request body XML
36mkCreateBucketConfig :: Text -> Region -> ByteString
32mkCreateBucketConfig :: Text -> Region -> ByteString
37mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
33mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
38 where
34 where
39− s3Element n = Element (s3Name ns n) mempty
35+ s3Element n = Element (s3Name ns n) mempty
36+ root =
37+ s3Element
40− root = s3Element "CreateBucketConfiguration"
38+ "CreateBucketConfiguration"
39+ [ NodeElement $
40+ s3Element
41− [ NodeElement $ s3Element "LocationConstraint"
41+ "LocationConstraint"
42− [ NodeContent location]
42+ [NodeContent location]
43 ]
43 ]
44− bucketConfig = Document (Prologue [] Nothing []) root []
44+ bucketConfig = Document (Prologue [] Nothing []) root []
45
45
46-- | Create a completeMultipartUpload request body XML
46-- | Create a completeMultipartUpload request body XML
47mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
47mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
48mkCompleteMultipartUploadRequest partInfo =
48mkCompleteMultipartUploadRequest partInfo =
49 LBS.toStrict $ renderLBS def cmur
49 LBS.toStrict $ renderLBS def cmur
50 where
50 where
51+ root =
51− root = Element "CompleteMultipartUpload" mempty $
52+ Element "CompleteMultipartUpload" mempty $
52− map (NodeElement . mkPart) partInfo
53+ map (NodeElement . mkPart) partInfo
53− mkPart (n, etag) = Element "Part" mempty
54+ mkPart (n, etag) =
55+ Element
56+ "Part"
57+ mempty
54− [ NodeElement $ Element "PartNumber" mempty
58+ [ NodeElement $
59+ Element
60+ "PartNumber"
61+ mempty
55− [NodeContent $ T.pack $ show n]
62+ [NodeContent $ T.pack $ show n],
56− , NodeElement $ Element "ETag" mempty
63+ NodeElement $
64+ Element
65+ "ETag"
66+ mempty
57− [NodeContent etag]
67+ [NodeContent etag]
58− ]
68+ ]
59 cmur = Document (Prologue [] Nothing []) root []
69 cmur = Document (Prologue [] Nothing []) root []
60
70
61-- Simplified XML representation without element attributes.
71-- Simplified XML representation without element attributes.
72+data XNode
62−data XNode = XNode Text [XNode]
73+ = XNode Text [XNode]
63− | XLeaf Text Text
74+ | XLeaf Text Text
64− deriving (Eq, Show)
75+ deriving stock (Eq, Show)
65
76
66toXML :: Text -> XNode -> ByteString
77toXML :: Text -> XNode -> ByteString
78+toXML ns node =
79+ LBS.toStrict $
67−toXML ns node = LBS.toStrict $ renderLBS def $
80+ renderLBS def $
68− Document (Prologue [] Nothing []) (xmlNode node) []
81+ Document (Prologue [] Nothing []) (xmlNode node) []
69 where
82 where
70 xmlNode :: XNode -> Element
83 xmlNode :: XNode -> Element
84+ xmlNode (XNode name nodes) =
71− xmlNode (XNode name nodes) = Element (s3Name ns name) mempty $
85+ Element (s3Name ns name) mempty $
72− map (NodeElement . xmlNode) nodes
86+ map (NodeElement . xmlNode) nodes
73− xmlNode (XLeaf name content) = Element (s3Name ns name) mempty
87+ xmlNode (XLeaf name content) =
88+ Element
89+ (s3Name ns name)
90+ mempty
74− [NodeContent content]
91+ [NodeContent content]
75
92
76class ToXNode a where
93class ToXNode a where
77 toXNode :: a -> XNode
94 toXNode :: a -> XNode
78
95
79instance ToXNode Event where
96instance ToXNode Event where
80− toXNode = XLeaf "Event" . show
97+ toXNode = XLeaf "Event" . toText
81
98
82instance ToXNode Notification where
99instance ToXNode Notification where
100+ toXNode (Notification qc tc lc) =
83− toXNode (Notification qc tc lc) = XNode "NotificationConfiguration" $
101+ XNode "NotificationConfiguration" $
84− map (toXNodesWithArnName "QueueConfiguration" "Queue") qc ++
102+ map (toXNodesWithArnName "QueueConfiguration" "Queue") qc
85− map (toXNodesWithArnName "TopicConfiguration" "Topic") tc ++
103+ ++ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc
86− map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
104+ ++ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
87
105
88toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
106toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
89−toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
107+toXNodesWithArnName eltName arnName (NotificationConfig itemId arn events fRule) =
108+ XNode eltName $
90− XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events ++
109+ [XLeaf "Id" itemId, XLeaf arnName arn]
110+ ++ map toXNode events
91− [toXNode fRule]
111+ ++ [toXNode fRule]
92
112
93instance ToXNode Filter where
113instance ToXNode Filter where
94 toXNode (Filter (FilterKey (FilterRules rules))) =
114 toXNode (Filter (FilterKey (FilterRules rules))) =
95 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
115 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
96
116
97getFRXNode :: FilterRule -> XNode
117getFRXNode :: FilterRule -> XNode
98−getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
118+getFRXNode (FilterRule n v) =
119+ XNode
120+ "FilterRule"
121+ [ XLeaf "Name" n,
99− , XLeaf "Value" v
122+ XLeaf "Value" v
100− ]
123+ ]
101
124
102mkPutNotificationRequest :: Text -> Notification -> ByteString
125mkPutNotificationRequest :: Text -> Notification -> ByteString
103mkPutNotificationRequest ns = toXML ns . toXNode
126mkPutNotificationRequest ns = toXML ns . toXNode
@@ -106,60 +129,103 @@ mkSelectRequest :: SelectRequest -> ByteString
106mkSelectRequest r = LBS.toStrict $ renderLBS def sr
129mkSelectRequest r = LBS.toStrict $ renderLBS def sr
107 where
130 where
108 sr = Document (Prologue [] Nothing []) root []
131 sr = Document (Prologue [] Nothing []) root []
132+ root =
109− root = Element "SelectRequest" mempty $
133+ Element "SelectRequest" mempty $
110− [ NodeElement (Element "Expression" mempty
134+ [ NodeElement
135+ ( Element
136+ "Expression"
137+ mempty
111− [NodeContent $ srExpression r])
138+ [NodeContent $ srExpression r]
139+ ),
140+ NodeElement
141+ ( Element
112− , NodeElement (Element "ExpressionType" mempty
142+ "ExpressionType"
143+ mempty
113− [NodeContent $ show $ srExpressionType r])
144+ [NodeContent $ show $ srExpressionType r]
145+ ),
146+ NodeElement
114− , NodeElement (Element "InputSerialization" mempty $
147+ ( Element "InputSerialization" mempty $
115− inputSerializationNodes $ srInputSerialization r)
148+ inputSerializationNodes $
149+ srInputSerialization r
150+ ),
151+ NodeElement
116− , NodeElement (Element "OutputSerialization" mempty $
152+ ( Element "OutputSerialization" mempty $
117− outputSerializationNodes $ srOutputSerialization r)
153+ outputSerializationNodes $
154+ srOutputSerialization r
155+ )
156+ ]
118− ] ++ maybe [] reqProgElem (srRequestProgressEnabled r)
157+ ++ maybe [] reqProgElem (srRequestProgressEnabled r)
119− reqProgElem enabled = [NodeElement
158+ reqProgElem enabled =
159+ [ NodeElement
160+ ( Element
120− (Element "RequestProgress" mempty
161+ "RequestProgress"
162+ mempty
121− [NodeElement
163+ [ NodeElement
122− (Element "Enabled" mempty
164+ ( Element
165+ "Enabled"
166+ mempty
123− [NodeContent
167+ [ NodeContent
124− (if enabled then "TRUE" else "FALSE")]
168+ (if enabled then "TRUE" else "FALSE")
169+ ]
125− )
170+ )
126− ]
171+ ]
127− )
172+ )
128− ]
173+ ]
174+ inputSerializationNodes is =
129− inputSerializationNodes is = comprTypeNode (isCompressionType is) ++
175+ comprTypeNode (isCompressionType is)
130− [NodeElement $ formatNode (isFormatInfo is)]
176+ ++ [NodeElement $ formatNode (isFormatInfo is)]
131− comprTypeNode (Just c) = [NodeElement $ Element "CompressionType" mempty
177+ comprTypeNode (Just c) =
178+ [ NodeElement $
179+ Element
180+ "CompressionType"
181+ mempty
132− [NodeContent $ case c of
182+ [ NodeContent $ case c of
133− CompressionTypeNone -> "NONE"
183+ CompressionTypeNone -> "NONE"
134− CompressionTypeGzip -> "GZIP"
184+ CompressionTypeGzip -> "GZIP"
135− CompressionTypeBzip2 -> "BZIP2"
185+ CompressionTypeBzip2 -> "BZIP2"
136− ]
186+ ]
137− ]
187+ ]
138 comprTypeNode Nothing = []
188 comprTypeNode Nothing = []
139−
140 kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
189 kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
141− formatNode (InputFormatCSV (CSVProp h)) =
190+ formatNode (InputFormatCSV c) =
142− Element "CSV" mempty
191+ Element
192+ "CSV"
193+ mempty
143− (map NodeElement $ map kvElement $ H.toList h)
194+ (map (NodeElement . kvElement) (csvPropsList c))
144 formatNode (InputFormatJSON p) =
195 formatNode (InputFormatJSON p) =
145− Element "JSON" mempty
196+ Element
197+ "JSON"
198+ mempty
146− [NodeElement
199+ [ NodeElement
147− (Element "Type" mempty
200+ ( Element
201+ "Type"
202+ mempty
148− [NodeContent $ case jsonipType p of
203+ [ NodeContent $ case jsonipType p of
149− JSONTypeDocument -> "DOCUMENT"
204+ JSONTypeDocument -> "DOCUMENT"
150− JSONTypeLines -> "LINES"
205+ JSONTypeLines -> "LINES"
151− ]
206+ ]
152− )
207+ )
153− ]
208+ ]
154 formatNode InputFormatParquet = Element "Parquet" mempty []
209 formatNode InputFormatParquet = Element "Parquet" mempty []
155−
156 outputSerializationNodes (OutputSerializationJSON j) =
210 outputSerializationNodes (OutputSerializationJSON j) =
211+ [ NodeElement
157− [NodeElement (Element "JSON" mempty $
212+ ( Element "JSON" mempty $
213+ rdElem $
158− rdElem $ jsonopRecordDelimiter j)]
214+ jsonopRecordDelimiter j
215+ )
216+ ]
159− outputSerializationNodes (OutputSerializationCSV (CSVProp h)) =
217+ outputSerializationNodes (OutputSerializationCSV c) =
160− [NodeElement $ Element "CSV" mempty
218+ [ NodeElement $
219+ Element
220+ "CSV"
221+ mempty
161− (map NodeElement $ map kvElement $ H.toList h)]
222+ (map (NodeElement . kvElement) (csvPropsList c))
162−
223+ ]
163 rdElem Nothing = []
224 rdElem Nothing = []
225+ rdElem (Just t) =
226+ [ NodeElement $
227+ Element
164− rdElem (Just t) = [NodeElement $ Element "RecordDelimiter" mempty
228+ "RecordDelimiter"
229+ mempty
165− [NodeContent t]]
230+ [NodeContent t]
231+ ]
src/Network/Minio/XmlParser.hs+107−154
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,75 +15,38 @@
15--
15--
16
16
17module Network.Minio.XmlParser
17module Network.Minio.XmlParser
18− ( parseListBuckets
18+ ( parseListBuckets,
19− , parseLocation
19+ parseLocation,
20− , parseNewMultipartUpload
20+ parseNewMultipartUpload,
21− , parseCompleteMultipartUploadResponse
21+ parseCompleteMultipartUploadResponse,
22− , parseCopyObjectResponse
22+ parseCopyObjectResponse,
23− , parseListObjectsResponse
23+ parseListObjectsResponse,
24− , parseListObjectsV1Response
24+ parseListObjectsV1Response,
25− , parseListUploadsResponse
25+ parseListUploadsResponse,
26− , parseListPartsResponse
26+ parseListPartsResponse,
27− , parseErrResponse
27+ parseErrResponse,
28− , parseNotification
28+ parseNotification,
29− , parseSelectProgress
29+ parseSelectProgress,
30+ )
30− ) where
31+where
31
32
32import qualified Data.ByteString.Lazy as LB
33import qualified Data.ByteString.Lazy as LB
33−import qualified Data.HashMap.Strict as H
34+import qualified Data.HashMap.Strict as H
34−import Data.List (zip3, zip4, zip6)
35+import Data.List (zip4, zip6)
35−import qualified Data.Text as T
36+import qualified Data.Text as T
36−import Data.Text.Read (decimal)
37−import Data.Time
37+import Data.Time
38−import Text.XML
38+import Network.Minio.Data
39+import Network.Minio.XmlCommon
39−import Text.XML.Cursor hiding (bool)
40+import Text.XML.Cursor hiding (bool)
40−
41−import Lib.Prelude
42−
43−import Network.Minio.Data
44−import Network.Minio.Errors
45−
46−
47−-- | Represent the time format string returned by S3 API calls.
48−s3TimeFormat :: [Char]
49−s3TimeFormat = iso8601DateFormat $ Just "%T%QZ"
50−
51−-- | Helper functions.
52−uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
53−uncurry4 f (a, b, c, d) = f a b c d
54−
55−uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
56−uncurry6 f (a, b, c, d, e, g) = f a b c d e g
57−
58−-- | Parse time strings from XML
59−parseS3XMLTime :: MonadIO m => Text -> m UTCTime
60−parseS3XMLTime t =
61− maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
62− parseTimeM True defaultTimeLocale s3TimeFormat $ T.unpack t
63−
64−parseDecimal :: (MonadIO m, Integral a) => Text -> m a
65−parseDecimal numStr = either (throwIO . MErrVXmlParse . show) return $
66− fst <$> decimal numStr
67−
68−parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
69−parseDecimals numStr = forM numStr parseDecimal
70−
71−s3Elem :: Text -> Text -> Axis
72−s3Elem ns = element . s3Name ns
73−
74−parseRoot :: (MonadIO m) => LByteString -> m Cursor
75−parseRoot = either (throwIO . MErrVXmlParse . show) (return . fromDocument)
76− . parseLBS def
77
41
78-- | Parse the response XML of a list buckets call.
42-- | Parse the response XML of a list buckets call.
79parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
43parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
80parseListBuckets xmldata = do
44parseListBuckets xmldata = do
81 r <- parseRoot xmldata
45 r <- parseRoot xmldata
82 ns <- asks getSvcNamespace
46 ns <- asks getSvcNamespace
83− let
84− s3Elem' = s3Elem ns
47+ let s3Elem' = s3Elem ns
85− names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
48+ names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
86− timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
49+ timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
87
50
88 times <- mapM parseS3XMLTime timeStrings
51 times <- mapM parseS3XMLTime timeStrings
89 return $ zipWith BucketInfo names times
52 return $ zipWith BucketInfo names times
@@ -116,41 +79,38 @@ parseCopyObjectResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =
116parseCopyObjectResponse xmldata = do
79parseCopyObjectResponse xmldata = do
117 r <- parseRoot xmldata
80 r <- parseRoot xmldata
118 ns <- asks getSvcNamespace
81 ns <- asks getSvcNamespace
119− let
120− s3Elem' = s3Elem ns
82+ let s3Elem' = s3Elem ns
121− mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
83+ mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
122
84
123 mtime <- parseS3XMLTime mtimeStr
85 mtime <- parseS3XMLTime mtimeStr
124 return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
86 return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
125
87
126-- | Parse the response XML of a list objects v1 call.
88-- | Parse the response XML of a list objects v1 call.
89+parseListObjectsV1Response ::
127−parseListObjectsV1Response :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
90+ (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
91+ LByteString ->
128− => LByteString -> m ListObjectsV1Result
92+ m ListObjectsV1Result
129parseListObjectsV1Response xmldata = do
93parseListObjectsV1Response xmldata = do
130 r <- parseRoot xmldata
94 r <- parseRoot xmldata
131 ns <- asks getSvcNamespace
95 ns <- asks getSvcNamespace
132− let
133− s3Elem' = s3Elem ns
96+ let s3Elem' = s3Elem ns
134− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
97+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
135−
136− nextMarker = headMay $ r $/ s3Elem' "NextMarker" &/ content
98+ nextMarker = listToMaybe $ r $/ s3Elem' "NextMarker" &/ content
137−
138− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
99+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
139−
140− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
100+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
141− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
101+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
142− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
102+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
143− -- if response xml contains empty etag response fill them with as
103+ -- if response xml contains empty etag response fill them with as
144− -- many empty Text for the zip4 below to work as intended.
104+ -- many empty Text for the zip4 below to work as intended.
145− etags = etagsList ++ repeat ""
105+ etags = etagsList ++ repeat ""
146− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
106+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
147
107
148 modTimes <- mapM parseS3XMLTime modTimeStr
108 modTimes <- mapM parseS3XMLTime modTimeStr
149 sizes <- parseDecimals sizeStr
109 sizes <- parseDecimals sizeStr
150
110
151− let
111+ let objects =
152− objects = map (uncurry6 ObjectInfo) $
112+ map (uncurry6 ObjectInfo) $
153− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
113+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
154
114
155 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
115 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
156
116
@@ -159,28 +119,24 @@ parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
159parseListObjectsResponse xmldata = do
119parseListObjectsResponse xmldata = do
160 r <- parseRoot xmldata
120 r <- parseRoot xmldata
161 ns <- asks getSvcNamespace
121 ns <- asks getSvcNamespace
162− let
163− s3Elem' = s3Elem ns
122+ let s3Elem' = s3Elem ns
164− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
123+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
165−
166− nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ content
124+ nextToken = listToMaybe $ r $/ s3Elem' "NextContinuationToken" &/ content
167−
168− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
125+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
169−
170− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
126+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
171− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
127+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
172− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
128+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
173− -- if response xml contains empty etag response fill them with as
129+ -- if response xml contains empty etag response fill them with as
174− -- many empty Text for the zip4 below to work as intended.
130+ -- many empty Text for the zip4 below to work as intended.
175− etags = etagsList ++ repeat ""
131+ etags = etagsList ++ repeat ""
176− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
132+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
177
133
178 modTimes <- mapM parseS3XMLTime modTimeStr
134 modTimes <- mapM parseS3XMLTime modTimeStr
179 sizes <- parseDecimals sizeStr
135 sizes <- parseDecimals sizeStr
180
136
181− let
137+ let objects =
182− objects = map (uncurry6 ObjectInfo) $
138+ map (uncurry6 ObjectInfo) $
183− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
139+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
184
140
185 return $ ListObjectsResult hasMore nextToken objects prefixes
141 return $ ListObjectsResult hasMore nextToken objects prefixes
186
142
@@ -189,20 +145,18 @@ parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
189parseListUploadsResponse xmldata = do
145parseListUploadsResponse xmldata = do
190 r <- parseRoot xmldata
146 r <- parseRoot xmldata
191 ns <- asks getSvcNamespace
147 ns <- asks getSvcNamespace
192− let
193− s3Elem' = s3Elem ns
148+ let s3Elem' = s3Elem ns
194− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
149+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
195− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
150+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
196− nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content
151+ nextKey = listToMaybe $ r $/ s3Elem' "NextKeyMarker" &/ content
197− nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content
152+ nextUpload = listToMaybe $ r $/ s3Elem' "NextUploadIdMarker" &/ content
198− uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
153+ uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
199− uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
154+ uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
200− uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
155+ uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
201
156
202 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
157 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
203
158
204− let
205− uploads = zip3 uploadKeys uploadIds uploadInitTimes
159+ let uploads = zip3 uploadKeys uploadIds uploadInitTimes
206
160
207 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
161 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
208
162
@@ -210,34 +164,25 @@ parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
210parseListPartsResponse xmldata = do
164parseListPartsResponse xmldata = do
211 r <- parseRoot xmldata
165 r <- parseRoot xmldata
212 ns <- asks getSvcNamespace
166 ns <- asks getSvcNamespace
213− let
214− s3Elem' = s3Elem ns
167+ let s3Elem' = s3Elem ns
215− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
168+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
216− nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content
169+ nextPartNumStr = listToMaybe $ r $/ s3Elem' "NextPartNumberMarker" &/ content
217− partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
170+ partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
218− partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
171+ partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
219− partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
172+ partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
220− partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
173+ partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
221
174
222 partModTimes <- mapM parseS3XMLTime partModTimeStr
175 partModTimes <- mapM parseS3XMLTime partModTimeStr
223 partSizes <- parseDecimals partSizeStr
176 partSizes <- parseDecimals partSizeStr
224 partNumbers <- parseDecimals partNumberStr
177 partNumbers <- parseDecimals partNumberStr
225 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
178 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
226
179
227− let
180+ let partInfos =
228− partInfos = map (uncurry4 ObjectPartInfo) $
181+ map (uncurry4 ObjectPartInfo) $
229− zip4 partNumbers partETags partSizes partModTimes
182+ zip4 partNumbers partETags partSizes partModTimes
230
183
231 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
184 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
232
185
233−
234−parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
235−parseErrResponse xmldata = do
236− r <- parseRoot xmldata
237− let code = T.concat $ r $/ element "Code" &/ content
238− message = T.concat $ r $/ element "Message" &/ content
239− return $ toServiceErr code message
240−
241parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
186parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
242parseNotification xmldata = do
187parseNotification xmldata = do
243 r <- parseRoot xmldata
188 r <- parseRoot xmldata
@@ -246,32 +191,40 @@ parseNotification xmldata = do
246 qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
191 qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
247 tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
192 tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
248 lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
193 lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
194+ Notification
249− Notification <$> (mapM (parseNode ns "Queue") qcfg)
195+ <$> mapM (parseNode ns "Queue") qcfg
250− <*> (mapM (parseNode ns "Topic") tcfg)
196+ <*> mapM (parseNode ns "Topic") tcfg
251− <*> (mapM (parseNode ns "CloudFunction") lcfg)
197+ <*> mapM (parseNode ns "CloudFunction") lcfg
252 where
198 where
253−
254 getFilterRule ns c =
199 getFilterRule ns c =
255 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
200 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
256 value = T.concat $ c $/ s3Elem ns "Value" &/ content
201 value = T.concat $ c $/ s3Elem ns "Value" &/ content
257− in FilterRule name value
202+ in FilterRule name value
258−
259 parseNode ns arnName nodeData = do
203 parseNode ns arnName nodeData = do
260 let c = fromNode nodeData
204 let c = fromNode nodeData
261− id = T.concat $ c $/ s3Elem ns "Id" &/ content
205+ itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
262 arn = T.concat $ c $/ s3Elem ns arnName &/ content
206 arn = T.concat $ c $/ s3Elem ns arnName &/ content
263− events = catMaybes $ map textToEvent $ c $/ s3Elem ns "Event" &/ content
207+ events = mapMaybe textToEvent (c $/ s3Elem ns "Event" &/ content)
208+ rules =
209+ c
264− rules = c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key" &/
210+ $/ s3Elem ns "Filter"
211+ &/ s3Elem ns "S3Key"
265− s3Elem ns "FilterRule" &| getFilterRule ns
212+ &/ s3Elem ns "FilterRule"
213+ &| getFilterRule ns
214+ return $
266− return $ NotificationConfig id arn events
215+ NotificationConfig
216+ itemId
217+ arn
218+ events
267− (Filter $ FilterKey $ FilterRules rules)
219+ (Filter $ FilterKey $ FilterRules rules)
268−
220+
269−parseSelectProgress :: MonadIO m => ByteString -> m Progress
221+parseSelectProgress :: (MonadIO m) => ByteString -> m Progress
270parseSelectProgress xmldata = do
222parseSelectProgress xmldata = do
271− r <- parseRoot $ LB.fromStrict xmldata
223+ r <- parseRoot $ LB.fromStrict xmldata
272− let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
224+ let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
273− bProcessed = T.concat $ r $/element "BytesProcessed" &/ content
225+ bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content
274− bReturned = T.concat $ r $/element "BytesReturned" &/ content
226+ bReturned = T.concat $ r $/ element "BytesReturned" &/ content
227+ Progress
275− Progress <$> parseDecimal bScanned
228+ <$> parseDecimal bScanned
276− <*> parseDecimal bProcessed
229+ <*> parseDecimal bProcessed
277− <*> parseDecimal bReturned
230+ <*> parseDecimal bReturned
stack.yaml+4−4
@@ -15,7 +15,7 @@
15# resolver:
15# resolver:
16# name: custom-snapshot
16# name: custom-snapshot
17# location: "./custom-snapshot.yaml"
17# location: "./custom-snapshot.yaml"
18−resolver: lts-14.6
18+resolver: lts-22.19
19
19
20# User packages to be built.
20# User packages to be built.
21# Various formats can be used as shown in the example below.
21# Various formats can be used as shown in the example below.
@@ -36,17 +36,17 @@ resolver: lts-14.6
36# non-dependency (i.e. a user package), and its test suites and benchmarks
36# non-dependency (i.e. a user package), and its test suites and benchmarks
37# will not be run. This is useful for tweaking upstream packages.
37# will not be run. This is useful for tweaking upstream packages.
38packages:
38packages:
39−- '.'
39+ - "."
40# Dependency packages to be pulled from upstream that are not in the resolver
40# Dependency packages to be pulled from upstream that are not in the resolver
41# (e.g., acme-missiles-0.3)
41# (e.g., acme-missiles-0.3)
42−extra-deps: []
42+extra-deps:
43+ - crypton-connection-0.3.2
43
44
44# Override default flag values for local packages and extra-deps
45# Override default flag values for local packages and extra-deps
45flags: {}
46flags: {}
46
47
47# Extra package databases containing global packages
48# Extra package databases containing global packages
48extra-package-dbs: []
49extra-package-dbs: []
49−
50# Control whether we use the GHC we find on the path
50# Control whether we use the GHC we find on the path
51# system-ghc: true
51# system-ghc: true
52#
52#
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+754−480
@@ -1,6 +1,5 @@
1−{-# LANGUAGE OverloadedStrings #-}
2--
1--
3−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
4--
3--
5-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
6-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,37 +14,34 @@
15-- limitations under the License.
14-- limitations under the License.
16--
15--
17
16
18−import qualified Test.QuickCheck as Q
19−import Test.Tasty
20−import Test.Tasty.HUnit
21−import Test.Tasty.QuickCheck as QC
22−
23−import Conduit (replicateC)
17+import Conduit (replicateC)
24−import qualified Conduit as C
18+import qualified Conduit as C
25−import qualified Control.Monad.Trans.Resource as R
19+import qualified Control.Monad.Trans.Resource as R
26−import qualified Data.ByteString as BS
20+import qualified Data.ByteString as BS
27−import Data.Conduit (yield)
21+import Data.Conduit (yield)
28−import qualified Data.Conduit.Binary as CB
22+import qualified Data.Conduit.Binary as CB
29−import Data.Conduit.Combinators (sinkList)
23+import Data.Conduit.Combinators (sinkList)
30−import qualified Data.HashMap.Strict as H
24+import qualified Data.HashMap.Strict as H
31−import qualified Data.Text as T
25+import qualified Data.Text as T
32−import Data.Time (fromGregorian)
26+import Data.Time (fromGregorian)
33−import qualified Data.Time as Time
27+import qualified Data.Time as Time
28+import Lib.Prelude
34import qualified Network.HTTP.Client.MultipartFormData as Form
29import qualified Network.HTTP.Client.MultipartFormData as Form
35−import qualified Network.HTTP.Conduit as NC
30+import qualified Network.HTTP.Conduit as NC
36−import qualified Network.HTTP.Types as HT
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
37−import System.Directory (getTemporaryDirectory)
38+import System.Directory (getTemporaryDirectory)
38−import System.Environment (lookupEnv)
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
39−import qualified System.IO as SIO
44+import qualified UnliftIO.IO as UIO
40−
41−import Lib.Prelude
42−
43−import Network.Minio
44−import Network.Minio.Data
45−import Network.Minio.Data.Crypto
46−import Network.Minio.PutObject
47−import Network.Minio.S3API
48−import Network.Minio.Utils
49
45
50main :: IO ()
46main :: IO ()
51main = defaultMain tests
47main = defaultMain tests
@@ -53,47 +49,107 @@ main = defaultMain tests
53tests :: TestTree
49tests :: TestTree
54tests = testGroup "Tests" [liveServerUnitTests]
50tests = testGroup "Tests" [liveServerUnitTests]
55
51
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+
56-- conduit that generates random binary stream of given length
97-- conduit that generates random binary stream of given length
57−randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
98+randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
58−randomDataSrc s' = genBS s'
99+randomDataSrc = genBS
59 where
100 where
101+ concatIt bs n =
102+ BS.concat $
60− concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++
103+ replicate (fromIntegral q) bs
61− [BS.take (fromIntegral r) bs]
104+ ++ [BS.take (fromIntegral r) bs]
105+ where
62− where (q, r) = n `divMod` fromIntegral (BS.length bs)
106+ (q, r) = n `divMod` fromIntegral (BS.length bs)
63−
64 genBS s = do
107 genBS s = do
65 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
108 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
66 let byteArr64 = BS.pack w8s
109 let byteArr64 = BS.pack w8s
67 if s < oneMiB
110 if s < oneMiB
68 then yield $ concatIt byteArr64 s
111 then yield $ concatIt byteArr64 s
112+ else do
69− else do yield $ concatIt byteArr64 oneMiB
113+ yield $ concatIt byteArr64 oneMiB
70− genBS (s - oneMiB)
114+ genBS (s - oneMiB)
71
115
72−mkRandFile :: R.MonadResource m => Int64 -> m FilePath
116+mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
73mkRandFile size = do
117mkRandFile size = do
74− dir <- liftIO $ getTemporaryDirectory
118+ dir <- liftIO getTemporaryDirectory
75 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
119 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
76
120
77funTestBucketPrefix :: Text
121funTestBucketPrefix :: Text
78funTestBucketPrefix = "miniohstest-"
122funTestBucketPrefix = "miniohstest-"
79
123
80−loadTestServer :: IO ConnectInfo
124+funTestWithBucketNoSTS ::
81−loadTestServer = do
125+ TestName ->
82− val <- lookupEnv "MINIO_LOCAL"
126+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
83− isSecure <- lookupEnv "MINIO_SECURE"
127+ TestTree
84− return $ case (val, isSecure) of
128+funTestWithBucketNoSTS t minioTest = testCaseSteps t $ \step -> do
85− (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"
129+ -- generate a random name for the bucket
86− (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"
130+ bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
87− (Nothing, _) -> minioPlayCI
131+ let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
88−
132+ liftStep = liftIO . step
133+ connInfo <- loadTestServerConnInfo
134+ ret <- runMinio connInfo $ do
135+ liftStep $ "Creating bucket for test - " ++ t
136+ foundBucket <- bucketExists b
137+ liftIO $ foundBucket @?= False
138+ makeBucket b Nothing
139+ minioTest liftStep b
140+ deleteBucket b
141+ isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
142+
89−funTestWithBucket :: TestName
143+funTestWithBucket ::
144+ TestName ->
90− -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree
145+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
146+ TestTree
91funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
147funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
92 -- generate a random name for the bucket
148 -- generate a random name for the bucket
93 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
149 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
94 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
150 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
95 liftStep = liftIO . step
151 liftStep = liftIO . step
96− connInfo <- loadTestServer
152+ connInfo <- loadTestServerConnInfo
97 ret <- runMinio connInfo $ do
153 ret <- runMinio connInfo $ do
98 liftStep $ "Creating bucket for test - " ++ t
154 liftStep $ "Creating bucket for test - " ++ t
99 foundBucket <- bucketExists b
155 foundBucket <- bucketExists b
@@ -103,26 +159,40 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
103 deleteBucket b
159 deleteBucket b
104 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
160 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
105
161
162+ connInfoSTS <- loadTestServerConnInfoSTS
163+ let t' = t ++ " (with AssumeRole Credentials)"
164+ ret' <- runMinio connInfoSTS $ do
165+ liftStep $ "Creating bucket for test - " ++ t'
166+ foundBucket <- bucketExists b
167+ liftIO $ foundBucket @?= False
168+ makeBucket b Nothing
169+ minioTest liftStep b
170+ deleteBucket b
171+ isRight ret' @? ("Functional test " ++ t' ++ " failed => " ++ show ret')
172+
106liveServerUnitTests :: TestTree
173liveServerUnitTests :: TestTree
174+liveServerUnitTests =
175+ testGroup
107−liveServerUnitTests = testGroup "Unit tests against a live server"
176+ "Unit tests against a live server"
108− [ basicTests
177+ [ basicTests,
109− , listingTest
178+ listingTest,
110− , highLevelListingTest
179+ highLevelListingTest,
111− , lowLevelMultipartTest
180+ lowLevelMultipartTest,
112− , putObjectSizeTest
181+ putObjectSizeTest,
113− , putObjectNoSizeTest
182+ putObjectNoSizeTest,
114− , multipartTest
183+ multipartTest,
115− , putObjectContentTypeTest
184+ putObjectContentTypeTest,
116− , putObjectContentLanguageTest
185+ putObjectContentLanguageTest,
117− , putObjectStorageClassTest
186+ putObjectStorageClassTest,
118− , putObjectUserMetadataTest
187+ putObjectUserMetadataTest,
119− , getObjectTest
188+ getObjectTest,
120− , copyObjectTests
189+ copyObjectTests,
121− , presignedUrlFunTest
190+ presignedUrlFunTest,
122− , presignedPostPolicyFunTest
191+ presignedPostPolicyFunTest,
123− , bucketPolicyFunTest
192+ bucketPolicyFunTest,
124− , getNPutSSECTest
193+ getNPutSSECTest,
194+ assumeRoleRequestTest
125− ]
195+ ]
126
196
127basicTests :: TestTree
197basicTests :: TestTree
128basicTests = funTestWithBucket "Basic tests" $
198basicTests = funTestWithBucket "Basic tests" $
@@ -131,83 +201,109 @@ basicTests = funTestWithBucket "Basic tests" $
131 buckets <- getService
201 buckets <- getService
132 unless (length (filter (== bucket) $ map biName buckets) == 1) $
202 unless (length (filter (== bucket) $ map biName buckets) == 1) $
133 liftIO $
203 liftIO $
134− assertFailure ("The bucket " ++ show bucket ++
204+ assertFailure
205+ ( "The bucket "
206+ ++ show bucket
135− " was expected to exist.")
207+ ++ " was expected to exist."
208+ )
136
209
137 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
210 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
138 mbE <- try $ makeBucket bucket Nothing
211 mbE <- try $ makeBucket bucket Nothing
139 case mbE of
212 case mbE of
140 Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
213 Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
141− _ -> return ()
214+ _ -> return ()
142
215
143 step "makeBucket with an invalid bucket name and check for appropriate exception."
216 step "makeBucket with an invalid bucket name and check for appropriate exception."
144 invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
217 invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
145 case invalidMBE of
218 case invalidMBE of
146 Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
219 Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
147− _ -> return ()
220+ _ -> return ()
148
221
149 step "getLocation works"
222 step "getLocation works"
150 region <- getLocation bucket
223 region <- getLocation bucket
151 liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
224 liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
152
225
153 step "singlepart putObject works"
226 step "singlepart putObject works"
227+ testFilepath <- mkRandFile 200
154− fPutObject bucket "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
228+ fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
155
229
156 step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
230 step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
157− fpE <- try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
231+ fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
158 case fpE of
232 case fpE of
159 Left exn -> liftIO $ exn @?= NoSuchBucket
233 Left exn -> liftIO $ exn @?= NoSuchBucket
160− _ -> return ()
234+ _ -> return ()
161
235
162 outFile <- mkRandFile 0
236 outFile <- mkRandFile 0
163 step "simple fGetObject works"
237 step "simple fGetObject works"
164− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions
238+ fGetObject bucket "test-file" outFile defaultGetObjectOptions
165
239
166 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
240 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
167 step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
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
168− resE <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
248+ defaultGetObjectOptions
169− gooIfUnmodifiedSince = (Just unmodifiedTime)
249+ { gooIfUnmodifiedSince = Just unmodifiedTime
170− }
250+ }
171 case resE of
251 case resE of
172 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
252 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
173− _ -> return ()
253+ _ -> return ()
174
254
175 step "fGetObject an object with no matching etag, check for exception"
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
176− resE1 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
262+ defaultGetObjectOptions
177− gooIfMatch = (Just "invalid-etag")
263+ { gooIfMatch = Just "invalid-etag"
178− }
264+ }
179 case resE1 of
265 case resE1 of
180 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
266 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
181− _ -> return ()
267+ _ -> return ()
182
268
183 step "fGetObject an object with no valid range, check for exception"
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
184− resE2 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
276+ defaultGetObjectOptions
185− gooRange = (Just $ HT.ByteRangeFromTo 100 200)
277+ { gooRange = Just $ HT.ByteRangeFromTo 100 300
186− }
278+ }
187 case resE2 of
279 case resE2 of
188− Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
280+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
189− _ -> return ()
281+ _ -> return ()
190
282
191 step "fGetObject on object with a valid range"
283 step "fGetObject on object with a valid range"
284+ fGetObject
285+ bucket
286+ "test-file"
287+ outFile
192− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
288+ defaultGetObjectOptions
193− gooRange = (Just $ HT.ByteRangeFrom 1)
289+ { gooRange = Just $ HT.ByteRangeFrom 1
194− }
290+ }
195
291
196 step "fGetObject a non-existent object and check for NoSuchKey exception"
292 step "fGetObject a non-existent object and check for NoSuchKey exception"
197 resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
293 resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
198 case resE3 of
294 case resE3 of
199 Left exn -> liftIO $ exn @?= NoSuchKey
295 Left exn -> liftIO $ exn @?= NoSuchKey
200− _ -> return ()
296+ _ -> return ()
201
297
202 step "create new multipart upload works"
298 step "create new multipart upload works"
203 uid <- newMultipartUpload bucket "newmpupload" []
299 uid <- newMultipartUpload bucket "newmpupload" []
204− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
300+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
205
301
206 step "abort a new multipart upload works"
302 step "abort a new multipart upload works"
207 abortMultipartUpload bucket "newmpupload" uid
303 abortMultipartUpload bucket "newmpupload" uid
208
304
209 step "delete object works"
305 step "delete object works"
210− deleteObject bucket "lsb-release"
306+ deleteObject bucket "test-file"
211
307
212 step "statObject test"
308 step "statObject test"
213 let object = "sample"
309 let object = "sample"
@@ -217,257 +313,310 @@ basicTests = funTestWithBucket "Basic tests" $
217
313
218 step "get metadata of the object"
314 step "get metadata of the object"
219 res <- statObject bucket object defaultGetObjectOptions
315 res <- statObject bucket object defaultGetObjectOptions
220− liftIO $ (oiSize res) @?= 0
316+ liftIO $ oiSize res @?= 0
221
317
222 step "delete object"
318 step "delete object"
223 deleteObject bucket object
319 deleteObject bucket object
224
320
225lowLevelMultipartTest :: TestTree
321lowLevelMultipartTest :: TestTree
226lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
322lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
227− \step bucket -> do
323+ \step bucket -> do
228− -- low-level multipart operation tests.
324+ -- low-level multipart operation tests.
229− let object = "newmpupload"
325+ let object = "newmpupload"
230− mb15 = 15 * 1024 * 1024
326+ mb15 = 15 * 1024 * 1024
231
327
232− step "Prepare for low-level multipart tests."
328+ step "Prepare for low-level multipart tests."
233− step "create new multipart upload"
329+ step "create new multipart upload"
234− uid <- newMultipartUpload bucket object []
330+ uid <- newMultipartUpload bucket object []
235− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
331+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
236
332
237− randFile <- mkRandFile mb15
333+ randFile <- mkRandFile mb15
238
334
239− step "put object parts 1 of 1"
335+ step "put object parts 1 of 1"
336+ partInfo <-
240− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
337+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
241− partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
338+ putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
242
339
243− step "complete multipart"
340+ step "complete multipart"
244− void $ completeMultipartUpload bucket object uid [partInfo]
341+ void $ completeMultipartUpload bucket object uid [partInfo]
245
342
246− destFile <- mkRandFile 0
343+ destFile <- mkRandFile 0
247− step "Retrieve the created object and check size"
344+ step "Retrieve the created object and check size"
248− fGetObject bucket object destFile defaultGetObjectOptions
345+ fGetObject bucket object destFile defaultGetObjectOptions
249− gotSize <- withNewHandle destFile getFileSize
346+ gotSize <- withNewHandle destFile getFileSize
347+ liftIO $
348+ gotSize
250− liftIO $ gotSize == Right (Just mb15) @?
349+ == Right (Just mb15)
251− "Wrong file size of put file after getting"
350+ @? "Wrong file size of put file after getting"
252
351
253− step "Cleanup actions"
352+ step "Cleanup actions"
254− removeObject bucket object
353+ removeObject bucket object
255
354
256putObjectSizeTest :: TestTree
355putObjectSizeTest :: TestTree
257putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
356putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
258 \step bucket -> do
357 \step bucket -> do
259− -- putObject test (conduit source, size specified)
358+ -- putObject test (conduit source, size specified)
260− let obj = "msingle"
359+ let obj = "msingle"
261− mb1 = 1 * 1024 * 1024
360+ mb1 = 1 * 1024 * 1024
262
361
263− step "Prepare for putObject with from source with size."
362+ step "Prepare for putObject with from source with size."
264− rFile <- mkRandFile mb1
363+ rFile <- mkRandFile mb1
265
364
266− step "Upload single file."
365+ step "Upload single file."
267− putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
366+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
268
367
269− step "Retrieve and verify file size"
368+ step "Retrieve and verify file size"
270− destFile <- mkRandFile 0
369+ destFile <- mkRandFile 0
271− fGetObject bucket obj destFile defaultGetObjectOptions
370+ fGetObject bucket obj destFile defaultGetObjectOptions
272− gotSize <- withNewHandle destFile getFileSize
371+ gotSize <- withNewHandle destFile getFileSize
372+ liftIO $
373+ gotSize
273− liftIO $ gotSize == Right (Just mb1) @?
374+ == Right (Just mb1)
274− "Wrong file size of put file after getting"
375+ @? "Wrong file size of put file after getting"
275
376
276− step "Cleanup actions"
377+ step "Cleanup actions"
277− deleteObject bucket obj
378+ deleteObject bucket obj
278
379
279putObjectNoSizeTest :: TestTree
380putObjectNoSizeTest :: TestTree
280putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
381putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
281 \step bucket -> do
382 \step bucket -> do
282− -- putObject test (conduit source, no size specified)
383+ -- putObject test (conduit source, no size specified)
283− let obj = "mpart"
384+ let obj = "mpart"
284− mb70 = 70 * 1024 * 1024
385+ mb70 = 70 * 1024 * 1024
285
386
286− step "Prepare for putObject with from source without providing size."
387+ step "Prepare for putObject with from source without providing size."
287− rFile <- mkRandFile mb70
388+ rFile <- mkRandFile mb70
288
389
289− step "Upload multipart file."
390+ step "Upload multipart file."
290− putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
391+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
291
392
292− step "Retrieve and verify file size"
393+ step "Retrieve and verify file size"
293− destFile <- mkRandFile 0
394+ destFile <- mkRandFile 0
294− fGetObject bucket obj destFile defaultGetObjectOptions
395+ fGetObject bucket obj destFile defaultGetObjectOptions
295− gotSize <- withNewHandle destFile getFileSize
396+ gotSize <- withNewHandle destFile getFileSize
397+ liftIO $
398+ gotSize
296− liftIO $ gotSize == Right (Just mb70) @?
399+ == Right (Just mb70)
297− "Wrong file size of put file after getting"
400+ @? "Wrong file size of put file after getting"
298
401
299− step "Cleanup actions"
402+ step "Cleanup actions"
300− deleteObject bucket obj
403+ deleteObject bucket obj
301
404
302highLevelListingTest :: TestTree
405highLevelListingTest :: TestTree
303highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
406highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
304 \step bucket -> do
407 \step bucket -> do
305− step "High-level listObjects Test"
408+ step "High-level listObjects Test"
306− step "put 3 objects"
409+ step "put 3 objects"
307− let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
308− extractObjectsFromList os =
410+ let extractObjectsFromList =
309− mapM (\t -> case t of
411+ mapM
412+ ( \case
310− ListItemObject o -> Just $ oiObject o
413+ ListItemObject o -> Just $ oiObject o
311− _ -> Nothing) os
414+ _ -> Nothing
312− expectedNonRecList = ["o4", "dir/"]
415+ )
313− extractObjectsAndDirsFromList os =
416+ extractObjectsAndDirsFromList =
417+ map
314− map (\t -> case t of
418+ ( \case
315− ListItemObject o -> oiObject o
419+ ListItemObject o -> oiObject o
316− ListItemPrefix d -> d) os
420+ ListItemPrefix d -> d
317−
421+ )
422+ expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
423+ expectedNonRecList = ["o4", "dir/"]
424+
425+ testFilepath <- mkRandFile 200
318− forM_ expectedObjects $
426+ forM_ expectedObjects $
319− \obj -> fPutObject bucket obj "/etc/lsb-release" defaultPutObjectOptions
427+ \obj -> fPutObject bucket obj testFilepath defaultPutObjectOptions
320−
428+
321− step "High-level listing of objects"
429+ step "High-level listing of objects"
322− items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
430+ items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
431+ liftIO $
323− liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
432+ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
324 extractObjectsAndDirsFromList items
433 extractObjectsAndDirsFromList items
325
434
326− step "High-level recursive listing of objects"
435+ step "High-level recursive listing of objects"
327− objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
436+ objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
328
437
438+ liftIO
439+ $ assertEqual
329− liftIO $ assertEqual "Objects match failed!"
440+ "Objects match failed!"
330− (Just $ sort expectedObjects) $
441+ (Just $ sort expectedObjects)
331− extractObjectsFromList objects
442+ $ extractObjectsFromList objects
332
443
333− step "High-level listing of objects (version 1)"
444+ step "High-level listing of objects (version 1)"
334− itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
445+ itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
446+ liftIO $
335− liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
447+ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
336 extractObjectsAndDirsFromList itemsV1
448 extractObjectsAndDirsFromList itemsV1
337
449
338− step "High-level recursive listing of objects (version 1)"
450+ step "High-level recursive listing of objects (version 1)"
451+ objectsV1 <-
452+ C.runConduit $
339− objectsV1 <- C.runConduit $ listObjectsV1 bucket Nothing True C..|
453+ listObjectsV1 bucket Nothing True
340− sinkList
454+ C..| sinkList
341−
455+
342− liftIO $ assertEqual "Objects match failed!"
456+ liftIO
343− (Just $ sort expectedObjects) $
457+ $ assertEqual
344− extractObjectsFromList objectsV1
458+ "Objects match failed!"
345−
459+ (Just $ sort expectedObjects)
346− let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
460+ $ extractObjectsFromList objectsV1
461+
462+ let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
347− expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
463+ expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
348− step "High-level listing with prefix"
464+ step "High-level listing with prefix"
349− prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
465+ prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
466+ liftIO
467+ $ assertEqual
350− liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
468+ "Objects/Dirs under prefix match failed!"
469+ expectedPrefListing
351− expectedPrefListing $ extractObjectsAndDirsFromList prefItems
470+ $ extractObjectsAndDirsFromList prefItems
352−
471+
353− step "High-level listing with prefix recursive"
472+ step "High-level listing with prefix recursive"
354− prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
473+ prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
474+ liftIO
475+ $ assertEqual
355− liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
476+ "Objects/Dirs under prefix match recursive failed!"
477+ expectedPrefListingRec
356− expectedPrefListingRec $ extractObjectsFromList prefItemsRec
478+ $ extractObjectsFromList prefItemsRec
357−
479+
358− step "High-level listing with prefix (version 1)"
480+ step "High-level listing with prefix (version 1)"
359− prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
481+ prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
482+ liftIO
483+ $ assertEqual
360− liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
484+ "Objects/Dirs under prefix match failed!"
485+ expectedPrefListing
361− expectedPrefListing $ extractObjectsAndDirsFromList prefItemsV1
486+ $ extractObjectsAndDirsFromList prefItemsV1
362−
487+
363− step "High-level listing with prefix recursive (version 1)"
488+ step "High-level listing with prefix recursive (version 1)"
364− prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
489+ prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
490+ liftIO
491+ $ assertEqual
365− liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
492+ "Objects/Dirs under prefix match recursive failed!"
493+ expectedPrefListingRec
366− expectedPrefListingRec $ extractObjectsFromList prefItemsRecV1
494+ $ extractObjectsFromList prefItemsRecV1
367−
495+
368− step "Cleanup actions"
496+ step "Cleanup actions"
369− forM_ expectedObjects $
497+ forM_ expectedObjects $
370− \obj -> removeObject bucket obj
498+ \obj -> removeObject bucket obj
371−
499+
372− step "High-level listIncompleteUploads Test"
500+ step "High-level listIncompleteUploads Test"
373− let object = "newmpupload"
501+ let object = "newmpupload"
374− step "create 10 multipart uploads"
502+ step "create 10 multipart uploads"
375− forM_ [1..10::Int] $ \_ -> do
503+ forM_ [1 .. 10 :: Int] $ \_ -> do
376− uid <- newMultipartUpload bucket object []
377− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
378−
379− step "High-level listing of incomplete multipart uploads"
380− uploads <- C.runConduit $
381− listIncompleteUploads bucket (Just "newmpupload") True C..|
382− sinkList
383− liftIO $ length uploads @?= 10
384−
385− step "cleanup"
386− forM_ uploads $ \(UploadInfo _ uid _ _) ->
387− abortMultipartUpload bucket object uid
388−
389− step "High-level listIncompleteParts Test"
390− let mb5 = 5 * 1024 * 1024
391−
392− step "create a multipart upload"
393 uid <- newMultipartUpload bucket object []
504 uid <- newMultipartUpload bucket object []
394 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
505 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
395
506
507+ step "High-level listing of incomplete multipart uploads"
508+ uploads <-
509+ C.runConduit $
510+ listIncompleteUploads bucket (Just "newmpupload") True
511+ C..| sinkList
512+ liftIO $ length uploads @?= 10
513+
514+ step "cleanup"
515+ forM_ uploads $ \(UploadInfo _ uid _ _) ->
516+ abortMultipartUpload bucket object uid
517+
518+ step "High-level listIncompleteParts Test"
519+ let mb5 = 5 * 1024 * 1024
520+
521+ step "create a multipart upload"
522+ uid <- newMultipartUpload bucket object []
523+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
524+
396− step "put object parts 1..10"
525+ step "put object parts 1..10"
397− inputFile <- mkRandFile mb5
526+ inputFile <- mkRandFile mb5
398− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
527+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
399− forM_ [1..10] $ \pnum ->
528+ forM_ [1 .. 10] $ \pnum ->
400 putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
529 putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
401
530
402− step "fetch list parts"
531+ step "fetch list parts"
532+ incompleteParts <-
533+ C.runConduit $
403− incompleteParts <- C.runConduit $ listIncompleteParts bucket object uid
534+ listIncompleteParts bucket object uid
404− C..| sinkList
535+ C..| sinkList
405− liftIO $ length incompleteParts @?= 10
536+ liftIO $ length incompleteParts @?= 10
406
537
407− step "cleanup"
538+ step "cleanup"
408− abortMultipartUpload bucket object uid
539+ abortMultipartUpload bucket object uid
409
540
410listingTest :: TestTree
541listingTest :: TestTree
411listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
542listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
412− step "listObjects' test"
543+ step "listObjects' test"
413− step "put 10 objects"
544+ step "put 10 objects"
414− let objects = (\s ->T.concat ["lsb-release", T.pack (show s)]) <$> [1..10::Int]
545+ let objects = (\s -> T.concat ["test-file-", T.pack (show s)]) <$> [1 .. 10 :: Int]
415−
546+
547+ testFilepath <- mkRandFile 200
416− forM_ [1..10::Int] $ \s ->
548+ forM_ [1 .. 10 :: Int] $ \s ->
417− fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release" defaultPutObjectOptions
549+ fPutObject bucket (T.concat ["test-file-", T.pack (show s)]) testFilepath defaultPutObjectOptions
418−
550+
419− step "Simple list"
551+ step "Simple list"
420− res <- listObjects' bucket Nothing Nothing Nothing Nothing
552+ res <- listObjects' bucket Nothing Nothing Nothing Nothing
421− let expectedObjects = sort objects
553+ let expectedObjects = sort objects
554+ liftIO $
555+ assertEqual
422− liftIO $ assertEqual "Objects match failed!" expectedObjects
556+ "Objects match failed!"
557+ expectedObjects
423− (map oiObject $ lorObjects res)
558+ (map oiObject $ lorObjects res)
424−
559+
425− step "Simple list version 1"
560+ step "Simple list version 1"
426− resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
561+ resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
427− let expected = sort $ map (T.concat .
562+ let expected =
428− ("lsb-release":) .
563+ sort $
564+ map
565+ ( T.concat
566+ . ("test-file-" :)
429− (\x -> [x]) .
567+ . (: [])
430− T.pack .
568+ . T.pack
569+ . show
570+ )
431− show) [1..10::Int]
571+ [1 .. 10 :: Int]
572+ liftIO $
573+ assertEqual
432− liftIO $ assertEqual "Objects match failed!" expected
574+ "Objects match failed!"
575+ expected
433− (map oiObject $ lorObjects' resV1)
576+ (map oiObject $ lorObjects' resV1)
434−
577+
435− step "Cleanup actions"
578+ step "Cleanup actions"
436− forM_ objects $ \obj -> deleteObject bucket obj
579+ forM_ objects $ \obj -> deleteObject bucket obj
437−
580+
438− step "listIncompleteUploads' test"
581+ step "listIncompleteUploads' test"
439− step "create 10 multipart uploads"
582+ step "create 10 multipart uploads"
440− let object = "newmpupload"
583+ let object = "newmpupload"
441− forM_ [1..10::Int] $ \_ -> do
584+ forM_ [1 .. 10 :: Int] $ \_ -> do
442− uid <- newMultipartUpload bucket object []
585+ uid <- newMultipartUpload bucket object []
443− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
444−
445− step "list incomplete multipart uploads"
446− incompleteUploads <- listIncompleteUploads' bucket (Just "newmpupload") Nothing
447− Nothing Nothing Nothing
448− liftIO $ (length $ lurUploads incompleteUploads) @?= 10
449−
450− step "cleanup"
451− forM_ (lurUploads incompleteUploads) $
452− \(_, uid, _) -> abortMultipartUpload bucket object uid
453−
454− step "Basic listIncompleteParts Test"
455− let mb5 = 5 * 1024 * 1024
456−
457− step "create a multipart upload"
458− uid <- newMultipartUpload bucket object []
459− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
586+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
460
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+
461− step "put object parts 1..10"
610+ step "put object parts 1..10"
462− inputFile <- mkRandFile mb5
611+ inputFile <- mkRandFile mb5
463− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
612+ UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
464− forM_ [1..10] $ \pnum ->
613+ forM_ [1 .. 10] $ \pnum ->
465− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
614+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
466−
615+
467− step "fetch list parts"
616+ step "fetch list parts"
468− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
617+ listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
469− liftIO $ (length $ lprParts listPartsResult) @?= 10
618+ liftIO $ length (lprParts listPartsResult) @?= 10
470− abortMultipartUpload bucket object uid
619+ abortMultipartUpload bucket object uid
471
620
472presignedUrlFunTest :: TestTree
621presignedUrlFunTest :: TestTree
473presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
622presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -479,29 +628,47 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
479 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
628 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
480
629
481 step "PUT object presigned URL - makePresignedUrl"
630 step "PUT object presigned URL - makePresignedUrl"
631+ putUrl <-
482− putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
632+ makePresignedUrl
633+ 3600
634+ HT.methodPut
635+ (Just bucket)
636+ (Just obj)
483− (Just obj) (Just "us-east-1") [] []
637+ (Just "us-east-1")
484−
638+ []
639+ []
640+
641+ -- print putUrl
485 let size1 = 1000 :: Int64
642 let size1 = 1000 :: Int64
486 inputFile <- mkRandFile size1
643 inputFile <- mkRandFile size1
487
644
488 -- attempt to upload using the presigned URL
645 -- attempt to upload using the presigned URL
489 putResp <- putR size1 inputFile mgr putUrl
646 putResp <- putR size1 inputFile mgr putUrl
647+ liftIO $
490− liftIO $ (NC.responseStatus putResp == HT.status200) @?
648+ (NC.responseStatus putResp == HT.status200)
491− "presigned PUT failed"
649+ @? "presigned PUT failed"
492
650
493 step "GET object presigned URL - makePresignedUrl"
651 step "GET object presigned URL - makePresignedUrl"
652+ getUrl <-
494− getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
653+ makePresignedUrl
654+ 3600
655+ HT.methodGet
656+ (Just bucket)
657+ (Just obj)
495− (Just obj) (Just "us-east-1") [] []
658+ (Just "us-east-1")
659+ []
660+ []
496
661
497 getResp <- getR mgr getUrl
662 getResp <- getR mgr getUrl
663+ liftIO $
498− liftIO $ (NC.responseStatus getResp == HT.status200) @?
664+ (NC.responseStatus getResp == HT.status200)
499− "presigned GET failed"
665+ @? "presigned GET failed"
500
666
501 -- read content from file to compare with response above
667 -- read content from file to compare with response above
502 bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
668 bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
669+ liftIO $
503− liftIO $ (bs == NC.responseBody getResp) @?
670+ (bs == NC.responseBody getResp)
504− "presigned put and get got mismatched data"
671+ @? "presigned put and get got mismatched data"
505
672
506 step "PUT object presigned - presignedPutObjectURL"
673 step "PUT object presigned - presignedPutObjectURL"
507 putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
674 putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
@@ -510,66 +677,71 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
510 testFile <- mkRandFile size2
677 testFile <- mkRandFile size2
511
678
512 putResp2 <- putR size2 testFile mgr putUrl2
679 putResp2 <- putR size2 testFile mgr putUrl2
680+ liftIO $
513− liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
681+ (NC.responseStatus putResp2 == HT.status200)
514− "presigned PUT failed (presignedPutObjectUrl)"
682+ @? "presigned PUT failed (presignedPutObjectUrl)"
515
683
516 step "HEAD object presigned URL - presignedHeadObjectUrl"
684 step "HEAD object presigned URL - presignedHeadObjectUrl"
517 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
685 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
518
686
687+ headResp <- do
519− headResp <- do let req = NC.parseRequest_ $ toS headUrl
688+ let req = NC.parseRequest_ $ decodeUtf8 headUrl
520− NC.httpLbs (req {NC.method = HT.methodHead}) mgr
689+ NC.httpLbs (req {NC.method = HT.methodHead}) mgr
690+ liftIO $
521− liftIO $ (NC.responseStatus headResp == HT.status200) @?
691+ (NC.responseStatus headResp == HT.status200)
522− "presigned HEAD failed (presignedHeadObjectUrl)"
692+ @? "presigned HEAD failed (presignedHeadObjectUrl)"
523
693
524 -- check that header info is accurate
694 -- check that header info is accurate
525 let h = H.fromList $ NC.responseHeaders headResp
695 let h = H.fromList $ NC.responseHeaders headResp
526 cLen = H.lookupDefault "0" HT.hContentLength h
696 cLen = H.lookupDefault "0" HT.hContentLength h
527− liftIO $ (cLen == show size2) @? "Head req returned bad content length"
697+ liftIO $ (cLen == showBS size2) @? "Head req returned bad content length"
528
698
529 step "GET object presigned URL - presignedGetObjectUrl"
699 step "GET object presigned URL - presignedGetObjectUrl"
530 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
700 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
531
701
532 getResp2 <- getR mgr getUrl2
702 getResp2 <- getR mgr getUrl2
703+ liftIO $
533− liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
704+ (NC.responseStatus getResp2 == HT.status200)
534− "presigned GET failed (presignedGetObjectUrl)"
705+ @? "presigned GET failed (presignedGetObjectUrl)"
535
706
536 -- read content from file to compare with response above
707 -- read content from file to compare with response above
537 bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
708 bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
709+ liftIO $
538− liftIO $ (bs2 == NC.responseBody getResp2) @?
710+ (bs2 == NC.responseBody getResp2)
539− "presigned put and get got mismatched data (presigned*Url)"
711+ @? "presigned put and get got mismatched data (presigned*Url)"
540−
541
712
542 mapM_ (removeObject bucket) [obj, obj2]
713 mapM_ (removeObject bucket) [obj, obj2]
543 where
714 where
544 putR size filePath mgr url = do
715 putR size filePath mgr url = do
545− let req = NC.parseRequest_ $ toS url
716+ let req = NC.parseRequest_ $ decodeUtf8 url
717+ let req' =
718+ req
546− let req' = req { NC.method = HT.methodPut
719+ { NC.method = HT.methodPut,
720+ NC.requestBody =
547− , NC.requestBody = NC.requestBodySource size $
721+ NC.requestBodySource size $
548− CB.sourceFile filePath}
722+ CB.sourceFile filePath
723+ }
549 NC.httpLbs req' mgr
724 NC.httpLbs req' mgr
550−
551 getR mgr url = do
725 getR mgr url = do
552− let req = NC.parseRequest_ $ toS url
726+ let req = NC.parseRequest_ $ decodeUtf8 url
553 NC.httpLbs req mgr
727 NC.httpLbs req mgr
554
728
555presignedPostPolicyFunTest :: TestTree
729presignedPostPolicyFunTest :: TestTree
556−presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
730+presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
557 \step bucket -> do
731 \step bucket -> do
558−
559 step "presignedPostPolicy basic test"
732 step "presignedPostPolicy basic test"
560− now <- liftIO $ Time.getCurrentTime
733+ now <- liftIO Time.getCurrentTime
561
734
562 let key = "presignedPostPolicyTest/myfile"
735 let key = "presignedPostPolicyTest/myfile"
736+ policyConds =
563− policyConds = [ ppCondBucket bucket
737+ [ ppCondBucket bucket,
564− , ppCondKey key
738+ ppCondKey key,
565− , ppCondContentLengthRange 1 1000
739+ ppCondContentLengthRange 1 1000,
566− , ppCondContentType "application/octet-stream"
740+ ppCondContentType "application/octet-stream",
567− , ppCondSuccessActionStatus 200
741+ ppCondSuccessActionStatus 200
568− ]
742+ ]
569−
570 expirationTime = Time.addUTCTime 3600 now
743 expirationTime = Time.addUTCTime 3600 now
571 postPolicyE = newPostPolicy expirationTime policyConds
744 postPolicyE = newPostPolicy expirationTime policyConds
572−
573 size = 1000 :: Int64
745 size = 1000 :: Int64
574
746
575 inputFile <- mkRandFile size
747 inputFile <- mkRandFile size
@@ -580,35 +752,35 @@ presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
580 (url, formData) <- presignedPostPolicy postPolicy
752 (url, formData) <- presignedPostPolicy postPolicy
581 -- liftIO (print url) >> liftIO (print formData)
753 -- liftIO (print url) >> liftIO (print formData)
582 result <- liftIO $ postForm url formData inputFile
754 result <- liftIO $ postForm url formData inputFile
755+ liftIO $
583− liftIO $ (NC.responseStatus result == HT.status200) @?
756+ (NC.responseStatus result == HT.status200)
584− "presigned POST failed"
757+ @? "presigned POST failed"
585
758
586 mapM_ (removeObject bucket) [key]
759 mapM_ (removeObject bucket) [key]
587− where
760+ where
588−
589− postForm url formData inputFile = do
761+ postForm url formData inputFile = do
590− req <- NC.parseRequest $ toS url
762+ req <- NC.parseRequest $ decodeUtf8 url
763+ let parts =
591− let parts = map (\(x, y) -> Form.partBS x y) $
764+ map (uncurry Form.partBS) $
592− H.toList formData
765+ H.toList formData
593− parts' = parts ++ [Form.partFile "file" inputFile]
766+ parts' = parts ++ [Form.partFile "file" inputFile]
594− req' <- Form.formDataBody parts' req
767+ req' <- Form.formDataBody parts' req
595− mgr <- NC.newManager NC.tlsManagerSettings
768+ mgr <- NC.newManager NC.tlsManagerSettings
596− NC.httpLbs req' mgr
769+ NC.httpLbs req' mgr
597
770
598bucketPolicyFunTest :: TestTree
771bucketPolicyFunTest :: TestTree
599bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
772bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
600 \step bucket -> do
773 \step bucket -> do
601−
602 step "bucketPolicy basic test - no policy exception"
774 step "bucketPolicy basic test - no policy exception"
603 resE <- try $ getBucketPolicy bucket
775 resE <- try $ getBucketPolicy bucket
604 case resE of
776 case resE of
605 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
777 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
606− _ -> return ()
778+ _ -> return ()
607
779
608 resE' <- try $ setBucketPolicy bucket T.empty
780 resE' <- try $ setBucketPolicy bucket T.empty
609 case resE' of
781 case resE' of
610 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
782 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
611− _ -> return ()
783+ _ -> return ()
612
784
613 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/*\"]}]}"
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/*\"]}]}"
614
786
@@ -616,7 +788,7 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
616 resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
788 resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
617 case resE'' of
789 case resE'' of
618 Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
790 Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
619− _ -> return ()
791+ _ -> return ()
620
792
621 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 <> "/*\"]}]}"
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 <> "/*\"]}]}"
622
794
@@ -631,13 +803,22 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
631 step "verify bucket policy: (2) get `myobject` anonymously"
803 step "verify bucket policy: (2) get `myobject` anonymously"
632 connInfo <- asks mcConnInfo
804 connInfo <- asks mcConnInfo
633 let proto = bool "http://" "https://" $ connectIsSecure connInfo
805 let proto = bool "http://" "https://" $ connectIsSecure connInfo
806+ url =
807+ BS.concat
808+ [ proto,
634− url = BS.concat [proto, getHostAddr connInfo, "/", toS bucket,
809+ getHostAddr connInfo,
810+ "/",
811+ encodeUtf8 bucket,
635− "/", toS obj]
812+ "/",
813+ encodeUtf8 obj
814+ ]
815+ respE <-
816+ liftIO $
636− respE <- liftIO $ (fmap (Right . toS) $ NC.simpleHttp $ toS url) `catch`
817+ fmap (Right . toStrictBS) (NC.simpleHttp $ decodeUtf8 url)
637− (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
818+ `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
638 case respE of
819 case respE of
639− Left err -> liftIO $ assertFailure $ show err
820+ Left err -> liftIO $ assertFailure $ show err
640− Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")
821+ Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
641
822
642 deleteObject bucket obj
823 deleteObject bucket obj
643
824
@@ -647,25 +828,28 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
647multipartTest :: TestTree
828multipartTest :: TestTree
648multipartTest = funTestWithBucket "Multipart Tests" $
829multipartTest = funTestWithBucket "Multipart Tests" $
649 \step bucket -> do
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.
650− step "Prepare for putObjectInternal with non-seekable file, with size."
833+ -- step "Prepare for putObjectInternal with non-seekable file, with size."
651− step "Upload multipart file."
834+ -- step "Upload multipart file."
652− let mb80 = 80 * 1024 * 1024
835+ -- let mb80 = 80 * 1024 * 1024
653− obj = "mpart"
836+ -- obj = "mpart"
654
837
655− void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
838+ -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
656
839
657− step "Retrieve and verify file size"
840+ -- step "Retrieve and verify file size"
658− destFile <- mkRandFile 0
841+ -- destFile <- mkRandFile 0
659− fGetObject bucket obj destFile defaultGetObjectOptions
842+ -- fGetObject bucket obj destFile defaultGetObjectOptions
660− gotSize <- withNewHandle destFile getFileSize
843+ -- gotSize <- withNewHandle destFile getFileSize
844+ -- liftIO $
661− liftIO $ gotSize == Right (Just mb80) @?
845+ -- gotSize == Right (Just mb80)
662− "Wrong file size of put file after getting"
846+ -- @? "Wrong file size of put file after getting"
663
847
664− step "Cleanup actions"
848+ -- step "Cleanup actions"
665− removeObject bucket obj
849+ -- removeObject bucket obj
666
850
667− step "cleanup"
851+ -- step "cleanup"
668− removeObject bucket "big"
852+ -- removeObject bucket "big"
669
853
670 step "Prepare for removeIncompleteUpload"
854 step "Prepare for removeIncompleteUpload"
671 -- low-level multipart operation tests.
855 -- low-level multipart operation tests.
@@ -679,15 +863,17 @@ multipartTest = funTestWithBucket "Multipart Tests" $
679 randFile <- mkRandFile kb5
863 randFile <- mkRandFile kb5
680
864
681 step "upload 2 parts"
865 step "upload 2 parts"
682− forM_ [1,2] $ \partNum -> do
866+ forM_ [1, 2] $ \partNum -> do
683− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
867+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
684− void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
868+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
685
869
686 step "remove ongoing upload"
870 step "remove ongoing upload"
687 removeIncompleteUpload bucket object
871 removeIncompleteUpload bucket object
872+ uploads <-
873+ C.runConduit $
688− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
874+ listIncompleteUploads bucket (Just object) False
689− C..| sinkList
875+ C..| sinkList
690− liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
876+ liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
691
877
692putObjectContentTypeTest :: TestTree
878putObjectContentTypeTest :: TestTree
693putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
879putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
@@ -698,9 +884,13 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
698
884
699 step "create server object with content-type"
885 step "create server object with content-type"
700 inputFile <- mkRandFile size1
886 inputFile <- mkRandFile size1
887+ fPutObject
888+ bucket
889+ object
890+ inputFile
701− fPutObject bucket object inputFile defaultPutObjectOptions {
891+ defaultPutObjectOptions
702− pooContentType = Just "application/javascript"
892+ { pooContentType = Just "application/javascript"
703− }
893+ }
704
894
705 -- retrieve obj info to check
895 -- retrieve obj info to check
706 oi <- headObject bucket object []
896 oi <- headObject bucket object []
@@ -710,16 +900,23 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
710 liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
900 liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
711
901
712 step "upload object with content-encoding set to identity"
902 step "upload object with content-encoding set to identity"
903+ fPutObject
904+ bucket
905+ object
906+ inputFile
713− fPutObject bucket object inputFile defaultPutObjectOptions {
907+ defaultPutObjectOptions
714− pooContentEncoding = Just "identity"
908+ { pooContentEncoding = Just "identity"
715− }
909+ }
716
910
717 oiCE <- headObject bucket object []
911 oiCE <- headObject bucket object []
718 let m' = oiMetadata oiCE
912 let m' = oiMetadata oiCE
719
913
720 step "Validate content-encoding"
914 step "Validate content-encoding"
915+ liftIO $
916+ assertEqual
721− liftIO $ assertEqual "Content-Encoding did not match" (Just "identity")
917+ "Content-Encoding did not match"
918+ (Just "identity")
722− (H.lookup "Content-Encoding" m')
919+ (H.lookup "Content-Encoding" m')
723
920
724 step "Cleanup actions"
921 step "Cleanup actions"
725
922
@@ -734,17 +931,24 @@ putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage test
734
931
735 step "create server object with content-language"
932 step "create server object with content-language"
736 inputFile <- mkRandFile size1
933 inputFile <- mkRandFile size1
934+ fPutObject
935+ bucket
936+ object
937+ inputFile
737− fPutObject bucket object inputFile defaultPutObjectOptions {
938+ defaultPutObjectOptions
738− pooContentLanguage = Just "en-US"
939+ { pooContentLanguage = Just "en-US"
739− }
940+ }
740
941
741 -- retrieve obj info to check
942 -- retrieve obj info to check
742 oi <- headObject bucket object []
943 oi <- headObject bucket object []
743 let m = oiMetadata oi
944 let m = oiMetadata oi
744
945
745 step "Validate content-language"
946 step "Validate content-language"
947+ liftIO $
948+ assertEqual
746− liftIO $ assertEqual "content-language did not match" (Just "en-US")
949+ "content-language did not match"
950+ (Just "en-US")
747− (H.lookup "Content-Language" m)
951+ (H.lookup "Content-Language" m)
748 step "Cleanup actions"
952 step "Cleanup actions"
749
953
750 removeObject bucket object
954 removeObject bucket object
@@ -759,19 +963,26 @@ putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
759 step "create server object with usermetdata"
963 step "create server object with usermetdata"
760 inputFile <- mkRandFile size1
964 inputFile <- mkRandFile size1
761
965
966+ fPutObject
967+ bucket
968+ object
969+ inputFile
762− fPutObject bucket object inputFile defaultPutObjectOptions {
970+ defaultPutObjectOptions
971+ { pooUserMetadata =
763− pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")
972+ [ ("x-Amz-meta-mykey1", "myval1"),
764− , ("mykey2", "myval2")
973+ ("mykey2", "myval2")
765− ]
974+ ]
766− }
975+ }
767
976
768 step "Validate user-metadata"
977 step "Validate user-metadata"
769 -- retrieve obj info to check
978 -- retrieve obj info to check
770 oi <- headObject bucket object []
979 oi <- headObject bucket object []
771 let m = oiUserMetadata oi
980 let m = oiUserMetadata oi
772 -- need to do a case-insensitive comparison
981 -- need to do a case-insensitive comparison
982+ sortedMeta =
983+ sort $
773− sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
984+ map (bimap T.toLower T.toLower) $
774− H.toList m
985+ H.toList m
775 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
986 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
776
987
777 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
988 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -787,19 +998,26 @@ getObjectTest = funTestWithBucket "getObject test" $
787 size1 = 100 :: Int64
998 size1 = 100 :: Int64
788
999
789 inputFile <- mkRandFile size1
1000 inputFile <- mkRandFile size1
1001+ fPutObject
1002+ bucket
1003+ object
1004+ inputFile
790− fPutObject bucket object inputFile defaultPutObjectOptions {
1005+ defaultPutObjectOptions
1006+ { pooUserMetadata =
791− pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")
1007+ [ ("x-Amz-meta-mykey1", "myval1"),
792− , ("mykey2", "myval2")
1008+ ("mykey2", "myval2")
793− ]
1009+ ]
794− }
1010+ }
795
1011
796 step "get the object - check the metadata matches"
1012 step "get the object - check the metadata matches"
797 -- retrieve obj info to check
1013 -- retrieve obj info to check
798 gor <- getObject bucket object defaultGetObjectOptions
1014 gor <- getObject bucket object defaultGetObjectOptions
799 let m = oiUserMetadata $ gorObjectInfo gor
1015 let m = oiUserMetadata $ gorObjectInfo gor
800 -- need to do a case-insensitive comparison
1016 -- need to do a case-insensitive comparison
1017+ sortedMeta =
1018+ sort $
801− sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
1019+ map (bimap T.toLower T.toLower) $
802− H.toList m
1020+ H.toList m
803 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
1021 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
804
1022
805 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
1023 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -827,13 +1045,21 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
827 inputFile' <- mkRandFile size1
1045 inputFile' <- mkRandFile size1
828 inputFile'' <- mkRandFile size0
1046 inputFile'' <- mkRandFile size0
829
1047
830− fPutObject bucket object inputFile defaultPutObjectOptions {
1048+ fPutObject
1049+ bucket
1050+ object
1051+ inputFile
1052+ defaultPutObjectOptions
831− pooStorageClass = Just "STANDARD"
1053+ { pooStorageClass = Just "STANDARD"
832− }
1054+ }
833−
1055+
1056+ fPutObject
1057+ bucket
1058+ object'
1059+ inputFile'
834− fPutObject bucket object' inputFile' defaultPutObjectOptions {
1060+ defaultPutObjectOptions
835− pooStorageClass = Just "REDUCED_REDUNDANCY"
1061+ { pooStorageClass = Just "REDUCED_REDUNDANCY"
836− }
1062+ }
837
1063
838 removeObject bucket object
1064 removeObject bucket object
839
1065
@@ -842,15 +1068,24 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
842 let m' = oiMetadata oi'
1068 let m' = oiMetadata oi'
843
1069
844 step "Validate x-amz-storage-class rrs"
1070 step "Validate x-amz-storage-class rrs"
1071+ liftIO $
1072+ assertEqual
845− liftIO $ assertEqual "storageClass did not match" (Just "REDUCED_REDUNDANCY")
1073+ "storageClass did not match"
1074+ (Just "REDUCED_REDUNDANCY")
846− (H.lookup "X-Amz-Storage-Class" m')
1075+ (H.lookup "X-Amz-Storage-Class" m')
847−
1076+
1077+ fpE <-
1078+ try $
1079+ fPutObject
1080+ bucket
1081+ object''
1082+ inputFile''
848− fpE <- try $ fPutObject bucket object'' inputFile'' defaultPutObjectOptions {
1083+ defaultPutObjectOptions
849− pooStorageClass = Just "INVALID_STORAGE_CLASS"
1084+ { pooStorageClass = Just "INVALID_STORAGE_CLASS"
850− }
1085+ }
851 case fpE of
1086 case fpE of
852 Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
1087 Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
853− _ -> return ()
1088+ _ -> return ()
854
1089
855 step "Cleanup actions"
1090 step "Cleanup actions"
856
1091
@@ -869,7 +1104,7 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
869 fPutObject bucket object inputFile defaultPutObjectOptions
1104 fPutObject bucket object inputFile defaultPutObjectOptions
870
1105
871 step "copy object"
1106 step "copy object"
872− let srcInfo = defaultSourceInfo { srcBucket = bucket, srcObject = object}
1107+ let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
873 (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
1108 (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
874
1109
875 -- retrieve obj info to check
1110 -- retrieve obj info to check
@@ -880,8 +1115,9 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
880
1115
881 let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
1116 let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
882
1117
1118+ liftIO $
883− liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
1119+ (s == size1 && e == etag && isMTimeDiffOk)
884− "Copied object did not match expected."
1120+ @? "Copied object did not match expected."
885
1121
886 step "cleanup actions"
1122 step "cleanup actions"
887 removeObject bucket object
1123 removeObject bucket object
@@ -902,12 +1138,18 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
902 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
1138 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
903
1139
904 step "put object parts 1-3"
1140 step "put object parts 1-3"
905− let srcInfo' = defaultSourceInfo { srcBucket = bucket, srcObject = srcObj }
1141+ let srcInfo' = defaultSourceInfo {srcBucket = bucket, srcObject = srcObj}
906− dstInfo' = defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj }
1142+ dstInfo' = defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
907− parts <- forM [1..3] $ \p -> do
1143+ parts <- forM [1 .. 3] $ \p -> do
1144+ (etag', _) <-
908− (etag', _) <- copyObjectPart dstInfo' srcInfo'{
1145+ copyObjectPart
1146+ dstInfo'
1147+ srcInfo'
909− srcRange = Just $ (,) ((p-1)*mb5) ((p-1)*mb5 + (mb5 - 1))
1148+ { srcRange = Just $ (,) ((p - 1) * mb5) ((p - 1) * mb5 + (mb5 - 1))
1149+ }
1150+ uid
910− } uid (fromIntegral p) []
1151+ (fromIntegral p)
1152+ []
911 return (fromIntegral p, etag')
1153 return (fromIntegral p, etag')
912
1154
913 step "complete multipart"
1155 step "complete multipart"
@@ -953,11 +1195,13 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
953 fPutObject bucket src inputFile' defaultPutObjectOptions
1195 fPutObject bucket src inputFile' defaultPutObjectOptions
954
1196
955 step "copy last 10MiB of object"
1197 step "copy last 10MiB of object"
1198+ copyObject
956− copyObject defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj } defaultSourceInfo {
1199+ defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1200+ defaultSourceInfo
957− srcBucket = bucket
1201+ { srcBucket = bucket,
958− , srcObject = src
1202+ srcObject = src,
959− , srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
1203+ srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
960− }
1204+ }
961
1205
962 step "verify uploaded object"
1206 step "verify uploaded object"
963 cSize <- oiSize <$> headObject bucket copyObj []
1207 cSize <- oiSize <$> headObject bucket copyObj []
@@ -968,42 +1212,72 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
968
1212
969getNPutSSECTest :: TestTree
1213getNPutSSECTest :: TestTree
970getNPutSSECTest =
1214getNPutSSECTest =
971− funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
1215+ funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
972− -- Skip this test if the server is not using TLS as encryption is
1216+ -- Skip this test if the server is not using TLS as encryption is
973− -- disabled anyway.
1217+ -- disabled anyway.
974− isTLSConn <- asks (connectIsSecure . mcConnInfo)
1218+ isTLSConn <- asks (connectIsSecure . mcConnInfo)
975− if isTLSConn
1219+ if isTLSConn
1220+ then do
976− then do step "Make an encryption key"
1221+ step "Make an encryption key"
977− key <- case mkSSECKey $ BS.pack [0..31] of
1222+ key <- case mkSSECKey $ BS.pack [0 .. 31] of
978− Nothing -> liftIO $ assertFailure "This should not happen"
1223+ Nothing -> liftIO $ assertFailure "This should not happen"
979− Just k -> return k
1224+ Just k -> return k
980−
1225+
981− let mb1 = 1024*1024
1226+ let mb1 = 1024 * 1024
982− obj = "1"
1227+ obj = "1"
983− step "Upload an object using the encryption key"
1228+ step "Upload an object using the encryption key"
984− rFile <- mkRandFile mb1
1229+ rFile <- mkRandFile mb1
985− let putOpts = defaultPutObjectOptions { pooSSE = Just $ SSEC key }
1230+ let putOpts = defaultPutObjectOptions {pooSSE = Just $ SSEC key}
986− fPutObject bucket obj rFile putOpts
1231+ fPutObject bucket obj rFile putOpts
987−
1232+
988− step "Stat object without key - should fail"
1233+ step "Stat object without key - should fail"
989− headRes <- try $ statObject bucket obj defaultGetObjectOptions
1234+ headRes <- try $ statObject bucket obj defaultGetObjectOptions
990− case headRes of
1235+ case headRes of
991− Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
1236+ Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
992− Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
1237+ Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
993− | NC.responseStatus rsp == HT.status400 -> return ()
1238+ | NC.responseStatus rsp == HT.status400 -> return ()
994− | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1239+ | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
995− Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1240+ Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
996−
1241+
997− step "Get file and check length"
1242+ step "Get file and check length"
998− dstFile <- mkRandFile 0
1243+ dstFile <- mkRandFile 0
999− let getOpts = defaultGetObjectOptions { gooSSECKey = Just key }
1244+ let getOpts = defaultGetObjectOptions {gooSSECKey = Just key}
1000− fGetObject bucket obj dstFile getOpts
1245+ fGetObject bucket obj dstFile getOpts
1001−
1246+
1002− gotSize <- withNewHandle dstFile getFileSize
1247+ gotSize <- withNewHandle dstFile getFileSize
1248+ liftIO $
1249+ gotSize
1003− liftIO $ gotSize == Right (Just mb1) @?
1250+ == Right (Just mb1)
1004− "Wrong file size of object when getting"
1251+ @? "Wrong file size of object when getting"
1005−
1252+
1006− step "Cleanup"
1253+ step "Cleanup"
1007− deleteObject bucket obj
1254+ deleteObject bucket obj
1008−
1009− else step "Skipping encryption test as server is not using TLS"
1255+ else step "Skipping encryption test as server is not using TLS"
1256+
1257+assumeRoleRequestTest :: TestTree
1258+assumeRoleRequestTest = testCaseSteps "Assume Role STS API" $ \step -> do
1259+ step "Load credentials"
1260+ (_, (ak, sk)) <- lookupTestEnv
1261+ val <- Env.lookupEnv "MINIO_LOCAL"
1262+ isSecure <- Env.lookupEnv "MINIO_SECURE"
1263+ let localMinioCred = Just $ CredentialValue ak sk mempty
1264+ playCreds =
1265+ case connectCreds minioPlayCI of
1266+ CredsStatic c -> Just c
1267+ _ -> Nothing
1268+ (cvMay, loc) =
1269+ case (val, isSecure) of
1270+ (Just _, Just _) -> (localMinioCred, "https://localhost:9000")
1271+ (Just _, Nothing) -> (localMinioCred, "http://localhost:9000")
1272+ (Nothing, _) -> (playCreds, "https://play.min.io:9000")
1273+ cv <- maybe (assertFailure "bad creds") return cvMay
1274+ let assumeRole =
1275+ STSAssumeRole cv $
1276+ defaultSTSAssumeRoleOptions
1277+ { saroLocation = Just "us-east-1",
1278+ saroEndpoint = Just loc
1279+ }
1280+ step "AssumeRole request"
1281+ res <- requestSTSCredential assumeRole
1282+ let v = credentialValueText $ fst res
1283+ print (v, snd res)
test/Network/Minio/API/Test.hs+75−61
@@ -15,88 +15,102 @@
15--
15--
16
16
17module Network.Minio.API.Test
17module Network.Minio.API.Test
18− ( bucketNameValidityTests
18+ ( bucketNameValidityTests,
19− , objectNameValidityTests
19+ objectNameValidityTests,
20− , parseServerInfoJSONTest
20+ parseServerInfoJSONTest,
21− , parseHealStatusTest
21+ parseHealStatusTest,
22− , parseHealStartRespTest
22+ parseHealStartRespTest,
23+ )
23− ) where
24+where
24−
25+
25−import Data.Aeson (eitherDecode)
26+import Data.Aeson (eitherDecode)
26−import Test.Tasty
27+import Network.Minio.API
27−import Test.Tasty.HUnit
28−
29−import Lib.Prelude
30−
31−import Network.Minio.AdminAPI
28+import Network.Minio.AdminAPI
32−import Network.Minio.API
29+import Test.Tasty
30+import Test.Tasty.HUnit
33
31
34assertBool' :: Bool -> Assertion
32assertBool' :: Bool -> Assertion
35assertBool' = assertBool "Test failed!"
33assertBool' = assertBool "Test failed!"
36
34
37bucketNameValidityTests :: TestTree
35bucketNameValidityTests :: TestTree
38−bucketNameValidityTests = testGroup "Bucket Name Validity Tests"
36+bucketNameValidityTests =
37+ testGroup
38+ "Bucket Name Validity Tests"
39− [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName ""
39+ [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName "",
40− , testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab"
40+ testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab",
41− , testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
41+ testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
42− , testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD"
42+ testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD",
43− , testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2"
43+ testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2",
44− , testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-"
44+ testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-",
45− , testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg"
45+ testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg",
46− , testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1"
46+ testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1",
47− , testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea"
47+ testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea",
48− , testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d"
48+ testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d",
49− , testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
49+ testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
50− ]
50+ ]
51
51
52objectNameValidityTests :: TestTree
52objectNameValidityTests :: TestTree
53−objectNameValidityTests = testGroup "Object Name Validity Tests"
53+objectNameValidityTests =
54+ testGroup
55+ "Object Name Validity Tests"
54− [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""
56+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName "",
55− , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
57+ testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
56− ]
58+ ]
57
59
58parseServerInfoJSONTest :: TestTree
60parseServerInfoJSONTest :: TestTree
61+parseServerInfoJSONTest =
59−parseServerInfoJSONTest = testGroup "Parse MinIO Admin API ServerInfo JSON test" $
62+ testGroup "Parse MinIO Admin API ServerInfo JSON test" $
63+ map
60− map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
64+ ( \(tName, tDesc, tfn, tVal) ->
65+ testCase tName $
66+ assertBool tDesc $
61− tfn (eitherDecode tVal :: Either [Char] [ServerInfo])) testCases
67+ tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
68+ )
69+ testCases
62 where
70 where
71+ testCases =
63− testCases = [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON)
72+ [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON),
64− , ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON)
73+ ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON),
65− , ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
74+ ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
66− ]
75+ ]
67 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\":[]}}}]"
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\":[]}}}]"
68−
69 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\":[]}}}]"
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\":[]}}}]"
70−
71 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\":[]}}}]"
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\":[]}}}]"
72
79
73parseHealStatusTest :: TestTree
80parseHealStatusTest :: TestTree
81+parseHealStatusTest =
74−parseHealStatusTest = testGroup "Parse MinIO Admin API HealStatus JSON test" $
82+ testGroup "Parse MinIO Admin API HealStatus JSON test" $
83+ map
75− map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
84+ ( \(tName, tDesc, tfn, tVal) ->
85+ testCase tName $
86+ assertBool tDesc $
76− tfn (eitherDecode tVal :: Either [Char] HealStatus)) testCases
87+ tfn (eitherDecode tVal :: Either [Char] HealStatus)
77−
88+ )
89+ testCases
78 where
90 where
91+ testCases =
79− testCases = [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON')
92+ [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON'),
80− , ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON')
93+ ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON'),
81− , ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
94+ ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
82− ]
95+ ]
83−
84 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}]}"
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}]}"
85−
86 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}]"
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}]"
87−
88 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}]}"
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}]}"
89
99
90parseHealStartRespTest :: TestTree
100parseHealStartRespTest :: TestTree
101+parseHealStartRespTest =
91−parseHealStartRespTest = testGroup "Parse MinIO Admin API HealStartResp JSON test" $
102+ testGroup "Parse MinIO Admin API HealStartResp JSON test" $
103+ map
92− map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
104+ ( \(tName, tDesc, tfn, tVal) ->
105+ testCase tName $
106+ assertBool tDesc $
93− tfn (eitherDecode tVal :: Either [Char] HealStartResp)) testCases
107+ tfn (eitherDecode tVal :: Either [Char] HealStartResp)
94−
108+ )
109+ testCases
95 where
110 where
111+ testCases =
96− testCases = [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON)
112+ [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON),
97− , ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
113+ ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
98− ]
114+ ]
99−
100 hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
115 hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
101−
102 missingTokenJSON = "{\"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.hs+26−27
@@ -15,26 +15,26 @@
15--
15--
16
16
17module Network.Minio.JsonParser.Test
17module Network.Minio.JsonParser.Test
18− (
19− jsonParserTests
18+ ( jsonParserTests,
19+ )
20− ) where
20+where
21
21
22+import Lib.Prelude
23+import Network.Minio.Errors
24+import Network.Minio.JsonParser
22−import Test.Tasty
25+import Test.Tasty
23−import Test.Tasty.HUnit
26+import Test.Tasty.HUnit
24−import UnliftIO (MonadUnliftIO)
27+import UnliftIO (MonadUnliftIO)
25−
26−import Lib.Prelude
27−
28−import Network.Minio.Errors
29−import Network.Minio.JsonParser
30
28
31jsonParserTests :: TestTree
29jsonParserTests :: TestTree
32−jsonParserTests = testGroup "JSON Parser Tests"
30+jsonParserTests =
31+ testGroup
32+ "JSON Parser Tests"
33− [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
33+ [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34− ]
34+ ]
35
35
36tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
36tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
37−tryValidationErr act = try act
37+tryValidationErr = try
38
38
39assertValidationErr :: MErrV -> Assertion
39assertValidationErr :: MErrV -> Assertion
40assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
40assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
@@ -44,21 +44,20 @@ testParseErrResponseJSON = do
44 -- 1. Test parsing of an invalid error json.
44 -- 1. Test parsing of an invalid error json.
45 parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
45 parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
46 when (isRight parseResE) $
46 when (isRight parseResE) $
47+ assertFailure $
47− assertFailure $ "Parsing should have failed => " ++ show parseResE
48+ "Parsing should have failed => " ++ show parseResE
48
49
49 forM_ cases $ \(jsondata, sErr) -> do
50 forM_ cases $ \(jsondata, sErr) -> do
50 parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
51 parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
51 either assertValidationErr (@?= sErr) parseErr
52 either assertValidationErr (@?= sErr) parseErr
52−
53 where
53 where
54− cases = [
54+ cases =
55− -- 2. Test parsing of a valid error json.
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\"}",
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."
57+ ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58− )
58+ ),
59− ,
60− -- 3. Test parsing of a valid, empty Resource.
59+ -- 3. Test parsing of a valid, empty Resource.
61− ("{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
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\"}",
62− ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
61+ ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
63− )
62+ )
64 ]
63 ]
test/Network/Minio/TestHelpers.hs+8−8
@@ -15,18 +15,18 @@
15--
15--
16
16
17module Network.Minio.TestHelpers
17module Network.Minio.TestHelpers
18− ( runTestNS
18+ ( runTestNS,
19+ )
19− ) where
20+where
20
21
21−import Network.Minio.Data
22+import Network.Minio.Data
22
23
23−import Lib.Prelude
24−
25−newtype TestNS = TestNS { testNamespace :: Text }
24+newtype TestNS = TestNS {testNamespace :: Text}
26
25
27instance HasSvcNamespace TestNS where
26instance HasSvcNamespace TestNS where
28 getSvcNamespace = testNamespace
27 getSvcNamespace = testNamespace
29
28
30runTestNS :: ReaderT TestNS m a -> m a
29runTestNS :: ReaderT TestNS m a -> m a
30+runTestNS =
31−runTestNS = flip runReaderT $
31+ flip runReaderT $
32− TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
32+ TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs+15−17
@@ -15,33 +15,31 @@
15--
15--
16
16
17module Network.Minio.Utils.Test
17module Network.Minio.Utils.Test
18− (
19− limitedMapConcurrentlyTests
18+ ( limitedMapConcurrentlyTests,
19+ )
20− ) where
20+where
21
21
22−import Test.Tasty
23−import Test.Tasty.HUnit
24−
25−import Lib.Prelude
26−
27−import Network.Minio.Utils
22+import Network.Minio.Utils
23+import Test.Tasty
24+import Test.Tasty.HUnit
28
25
29limitedMapConcurrentlyTests :: TestTree
26limitedMapConcurrentlyTests :: TestTree
30−limitedMapConcurrentlyTests = testGroup "limitedMapConcurrently Tests"
27+limitedMapConcurrentlyTests =
28+ testGroup
29+ "limitedMapConcurrently Tests"
31− [ testCase "Test with various thread counts" testLMC
30+ [ testCase "Test with various thread counts" testLMC
32− ]
31+ ]
33
32
34testLMC :: Assertion
33testLMC :: Assertion
35testLMC = do
34testLMC = do
36 let maxNum = 50
35 let maxNum = 50
37 -- test with thread count of 1 to 2*maxNum
36 -- test with thread count of 1 to 2*maxNum
38− forM_ [1..(2*maxNum)] $ \threads -> do
37+ forM_ [1 .. (2 * maxNum)] $ \threads -> do
39− res <- limitedMapConcurrently threads compute [1..maxNum]
38+ res <- limitedMapConcurrently threads compute [1 .. maxNum]
40 sum res @?= overallResultCheck maxNum
39 sum res @?= overallResultCheck maxNum
41 where
40 where
42 -- simple function to run in each thread
41 -- simple function to run in each thread
43 compute :: Int -> IO Int
42 compute :: Int -> IO Int
44− compute n = return $ sum [1..n]
43+ compute n = return $ sum [1 .. n]
45−
46 -- function to check overall result
44 -- function to check overall result
47− overallResultCheck n = sum $ map (\t -> (t * (t+1)) `div` 2) [1..n]
45+ overallResultCheck n = sum $ map (\t -> (t * (t + 1)) `div` 2) [1 .. n]
test/Network/Minio/XmlGenerator/Test.hs+137−98
@@ -13,30 +13,33 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
17{-# LANGUAGE QuasiQuotes #-}
16{-# LANGUAGE QuasiQuotes #-}
18−module Network.Minio.XmlGenerator.Test
19− ( xmlGeneratorTests
20− ) where
21−
22−import Test.Tasty
23−import Test.Tasty.HUnit
24−import Text.RawString.QQ (r)
25
17
26−import Lib.Prelude
18+module Network.Minio.XmlGenerator.Test
19+ ( xmlGeneratorTests,
20+ )
21+where
27
22
23+import qualified Data.ByteString.Lazy as LBS
24+import Lib.Prelude
28−import Network.Minio.Data
25+import Network.Minio.Data
29−import Network.Minio.TestHelpers
26+import Network.Minio.TestHelpers
30−import Network.Minio.XmlGenerator
27+import Network.Minio.XmlGenerator
31−import Network.Minio.XmlParser (parseNotification)
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)
32
33
33xmlGeneratorTests :: TestTree
34xmlGeneratorTests :: TestTree
34−xmlGeneratorTests = testGroup "XML Generator Tests"
35+xmlGeneratorTests =
36+ testGroup
37+ "XML Generator Tests"
35− [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig
38+ [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig,
36− , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest
39+ testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest,
37− , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest
40+ testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest,
38− , testCase "Test mkSelectRequest" testMkSelectRequest
41+ testCase "Test mkSelectRequest" testMkSelectRequest
39− ]
42+ ]
40
43
41testMkCreateBucketConfig :: Assertion
44testMkCreateBucketConfig :: Assertion
42testMkCreateBucketConfig = do
45testMkCreateBucketConfig = do
@@ -44,100 +47,136 @@ testMkCreateBucketConfig = do
44 assertEqual "CreateBucketConfiguration xml should match: " expected $
47 assertEqual "CreateBucketConfiguration xml should match: " expected $
45 mkCreateBucketConfig ns "EU"
48 mkCreateBucketConfig ns "EU"
46 where
49 where
50+ expected =
47− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
51+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
48− \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
52+ \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
49− \<LocationConstraint>EU</LocationConstraint>\
53+ \<LocationConstraint>EU</LocationConstraint>\
50− \</CreateBucketConfiguration>"
54+ \</CreateBucketConfiguration>"
51
55
52testMkCompleteMultipartUploadRequest :: Assertion
56testMkCompleteMultipartUploadRequest :: Assertion
53testMkCompleteMultipartUploadRequest =
57testMkCompleteMultipartUploadRequest =
54 assertEqual "completeMultipartUpload xml should match: " expected $
58 assertEqual "completeMultipartUpload xml should match: " expected $
55− mkCompleteMultipartUploadRequest [(1, "abc")]
59+ mkCompleteMultipartUploadRequest [(1, "abc")]
56 where
60 where
61+ expected =
57− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
62+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
58− \<CompleteMultipartUpload>\
63+ \<CompleteMultipartUpload>\
59− \<Part>\
64+ \<Part>\
60− \<PartNumber>1</PartNumber><ETag>abc</ETag>\
65+ \<PartNumber>1</PartNumber><ETag>abc</ETag>\
61− \</Part>\
66+ \</Part>\
62− \</CompleteMultipartUpload>"
67+ \</CompleteMultipartUpload>"
63−
64
68
65testMkPutNotificationRequest :: Assertion
69testMkPutNotificationRequest :: Assertion
66testMkPutNotificationRequest =
70testMkPutNotificationRequest =
67 forM_ cases $ \val -> do
71 forM_ cases $ \val -> do
68 let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
72 let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
69− result = toS $ mkPutNotificationRequest ns val
73+ result = fromStrictBS $ mkPutNotificationRequest ns val
70 ntf <- runExceptT $ runTestNS $ parseNotification result
74 ntf <- runExceptT $ runTestNS $ parseNotification result
75+ either
71− either (\_ -> assertFailure "XML Parse Error!")
76+ (\_ -> assertFailure "XML Parse Error!")
72− (@?= val) ntf
77+ (@?= val)
78+ ntf
73 where
79 where
80+ cases =
74− cases = [ Notification []
81+ [ Notification
82+ []
75− [ NotificationConfig
83+ [ NotificationConfig
76− "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
84+ "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
77− "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
85+ "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
78− [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
86+ [ReducedRedundancyLostObject, ObjectCreated]
87+ defaultFilter
79− ]
88+ ]
80− []
89+ [],
81− , Notification
90+ Notification
82− [ NotificationConfig
91+ [ NotificationConfig
92+ "1"
83− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
93+ "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
84− [ObjectCreatedPut]
94+ [ObjectCreatedPut]
95+ ( Filter $
96+ FilterKey $
85− (Filter $ FilterKey $ FilterRules
97+ FilterRules
86− [ FilterRule "prefix" "images/"
98+ [ FilterRule "prefix" "images/",
87− , FilterRule "suffix" ".jpg"])
99+ FilterRule "suffix" ".jpg"
100+ ]
101+ ),
88− , NotificationConfig
102+ NotificationConfig
103+ ""
89− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
104+ "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
90− [ObjectCreated] defaultFilter
105+ [ObjectCreated]
106+ defaultFilter
91− ]
107+ ]
92− [ NotificationConfig
108+ [ NotificationConfig
109+ ""
93− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
110+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
94− [ReducedRedundancyLostObject] defaultFilter
111+ [ReducedRedundancyLostObject]
112+ defaultFilter
95− ]
113+ ]
96− [ NotificationConfig
114+ [ NotificationConfig
115+ "ObjectCreatedEvents"
97− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
116+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
98− [ObjectCreated] defaultFilter
117+ [ObjectCreated]
118+ defaultFilter
99− ]
119+ ]
100− ]
120+ ]
101
121
102testMkSelectRequest :: Assertion
122testMkSelectRequest :: Assertion
103testMkSelectRequest = mapM_ assertFn cases
123testMkSelectRequest = mapM_ assertFn cases
104 where
124 where
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
105− assertFn (a, b) = assertEqual "selectRequest XML should match: " b $ mkSelectRequest a
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
106− cases = [ ( SelectRequest "Select * from S3Object" SQL
134+ "Select * from S3Object"
135+ SQL
136+ ( InputSerialization
107− (InputSerialization (Just CompressionTypeGzip)
137+ (Just CompressionTypeGzip)
138+ ( InputFormatCSV $
108− (InputFormatCSV $ fileHeaderInfo FileHeaderIgnore
139+ fileHeaderInfo FileHeaderIgnore
109− <> recordDelimiter "\n"
110− <> fieldDelimiter ","
111− <> quoteCharacter "\""
112− <> quoteEscapeCharacter "\""
113− ))
114− (OutputSerializationCSV $ quoteFields QuoteFieldsAsNeeded
115− <> recordDelimiter "\n"
140+ <> recordDelimiter "\n"
116− <> fieldDelimiter ","
141+ <> fieldDelimiter ","
117− <> quoteCharacter "\""
142+ <> quoteCharacter "\""
118− <> quoteEscapeCharacter "\""
143+ <> quoteEscapeCharacter "\""
119 )
144 )
145+ )
146+ ( OutputSerializationCSV $
147+ quoteFields QuoteFieldsAsNeeded
148+ <> recordDelimiter "\n"
149+ <> fieldDelimiter ","
150+ <> quoteCharacter "\""
151+ <> quoteEscapeCharacter "\""
152+ )
120− (Just False)
153+ (Just False),
121− , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><QuoteCharacter>&#34;</QuoteCharacter><RecordDelimiter>
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>
122−</RecordDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
155+</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
123−</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
156+</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
124− )
157+ ),
125− , ( setRequestProgressEnabled False $
158+ ( setRequestProgressEnabled False $
126− setInputCompressionType CompressionTypeGzip $
159+ setInputCompressionType CompressionTypeGzip $
160+ selectRequest
127− selectRequest "Select * from S3Object" documentJsonInput
161+ "Select * from S3Object"
162+ documentJsonInput
128− (outputJSONFromRecordDelimiter "\n")
163+ (outputJSONFromRecordDelimiter "\n"),
129− , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter>
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>
130</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
165</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
131− )
166+ ),
132− , ( setRequestProgressEnabled False $
167+ ( setRequestProgressEnabled False $
133− setInputCompressionType CompressionTypeNone $
168+ setInputCompressionType CompressionTypeNone $
169+ selectRequest
170+ "Select * from S3Object"
134− selectRequest "Select * from S3Object" defaultParquetInput
171+ defaultParquetInput
172+ ( outputCSVFromProps $
135− (outputCSVFromProps $ quoteFields QuoteFieldsAsNeeded
173+ quoteFields QuoteFieldsAsNeeded
136− <> recordDelimiter "\n"
174+ <> recordDelimiter "\n"
137− <> fieldDelimiter ","
175+ <> fieldDelimiter ","
138− <> quoteCharacter "\""
176+ <> quoteCharacter "\""
139− <> quoteEscapeCharacter "\"")
177+ <> quoteEscapeCharacter "\""
178+ ),
140− , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
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>
141−</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
180+</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
142− )
181+ )
143− ]
182+ ]
test/Network/Minio/XmlParser/Test.hs+298−276
@@ -13,48 +13,49 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
17{-# LANGUAGE QuasiQuotes #-}
16{-# LANGUAGE QuasiQuotes #-}
18−module Network.Minio.XmlParser.Test
19− ( xmlParserTests
20− ) where
21−
22−import qualified Data.HashMap.Strict as H
23−import Data.Time (fromGregorian)
24−import Test.Tasty
25−import Test.Tasty.HUnit
26−import Text.RawString.QQ (r)
27−import UnliftIO (MonadUnliftIO)
28
17
18+module Network.Minio.XmlParser.Test
19+ ( xmlParserTests,
20+ )
21+where
22+
23+import qualified Data.HashMap.Strict as H
24+import Data.Time (fromGregorian)
29−import Lib.Prelude
25+import Lib.Prelude
30−
31−import Network.Minio.Data
26+import Network.Minio.Data
32−import Network.Minio.Errors
27+import Network.Minio.Errors
33−import Network.Minio.TestHelpers
28+import Network.Minio.TestHelpers
34−import Network.Minio.XmlParser
29+import Network.Minio.XmlParser
30+import Test.Tasty
31+import Test.Tasty.HUnit
32+import Text.RawString.QQ (r)
33+import UnliftIO (MonadUnliftIO)
35
34
36xmlParserTests :: TestTree
35xmlParserTests :: TestTree
37−xmlParserTests = testGroup "XML Parser Tests"
36+xmlParserTests =
37+ testGroup
38+ "XML Parser Tests"
38− [ testCase "Test parseLocation" testParseLocation
39+ [ testCase "Test parseLocation" testParseLocation,
39− , testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload
40+ testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload,
40− , testCase "Test parseListObjectsResponse" testParseListObjectsResult
41+ testCase "Test parseListObjectsResponse" testParseListObjectsResult,
41− , testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result
42+ testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result,
42− , testCase "Test parseListUploadsresponse" testParseListIncompleteUploads
43+ testCase "Test parseListUploadsresponse" testParseListIncompleteUploads,
43− , testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse
44+ testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse,
44− , testCase "Test parseListPartsResponse" testParseListPartsResponse
45+ testCase "Test parseListPartsResponse" testParseListPartsResponse,
45− , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse
46+ testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse,
46− , testCase "Test parseNotification" testParseNotification
47+ testCase "Test parseNotification" testParseNotification,
47− , testCase "Test parseSelectProgress" testParseSelectProgress
48+ testCase "Test parseSelectProgress" testParseSelectProgress
48− ]
49+ ]
49
50
50tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
51tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
51−tryValidationErr act = try act
52+tryValidationErr = try
52
53
53assertValidtionErr :: MErrV -> Assertion
54assertValidtionErr :: MErrV -> Assertion
54assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
55assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
55
56
56eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
57eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
57−eitherValidationErr (Left e) _ = assertValidtionErr e
58+eitherValidationErr (Left e) _ = assertValidtionErr e
58eitherValidationErr (Right a) f = f a
59eitherValidationErr (Right a) f = f a
59
60
60testParseLocation :: Assertion
61testParseLocation :: Assertion
@@ -62,224 +63,224 @@ testParseLocation = do
62 -- 1. Test parsing of an invalid location constraint xml.
63 -- 1. Test parsing of an invalid location constraint xml.
63 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
64 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
64 when (isRight parseResE) $
65 when (isRight parseResE) $
66+ assertFailure $
65− assertFailure $ "Parsing should have failed => " ++ show parseResE
67+ "Parsing should have failed => " ++ show parseResE
66
68
67 forM_ cases $ \(xmldata, expectedLocation) -> do
69 forM_ cases $ \(xmldata, expectedLocation) -> do
68 parseLocE <- tryValidationErr $ parseLocation xmldata
70 parseLocE <- tryValidationErr $ parseLocation xmldata
69 either assertValidtionErr (@?= expectedLocation) parseLocE
71 either assertValidtionErr (@?= expectedLocation) parseLocE
70 where
72 where
71− cases = [
73+ cases =
72− -- 2. Test parsing of a valid location xml.
74+ [ -- 2. Test parsing of a valid location xml.
73− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
75+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
74− \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
76+ \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
75− "EU"
77+ "EU"
76− )
78+ ),
77− ,
78− -- 3. Test parsing of a valid, empty location xml.
79+ -- 3. Test parsing of a valid, empty location xml.
79− ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
80+ ( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
80− "us-east-1"
81+ "us-east-1"
81− )
82+ )
82 ]
83 ]
83
84
84−
85testParseNewMultipartUpload :: Assertion
85testParseNewMultipartUpload :: Assertion
86testParseNewMultipartUpload = do
86testParseNewMultipartUpload = do
87 forM_ cases $ \(xmldata, expectedUploadId) -> do
87 forM_ cases $ \(xmldata, expectedUploadId) -> do
88 parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
88 parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
89 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
89 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
90 where
90 where
91− cases = [
91+ cases =
92− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
92+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
93− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
93+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
94− \ <Bucket>example-bucket</Bucket>\
94+ \ <Bucket>example-bucket</Bucket>\
95− \ <Key>example-object</Key>\
95+ \ <Key>example-object</Key>\
96− \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
96+ \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
97− \</InitiateMultipartUploadResult>",
97+ \</InitiateMultipartUploadResult>",
98− "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
98+ "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
99− ),
99+ ),
100− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
100+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
101− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
101+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
102− \ <Bucket>example-bucket</Bucket>\
102+ \ <Bucket>example-bucket</Bucket>\
103− \ <Key>example-object</Key>\
103+ \ <Key>example-object</Key>\
104− \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
104+ \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
105− \</InitiateMultipartUploadResult>",
105+ \</InitiateMultipartUploadResult>",
106− "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
106+ "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
107− )
107+ )
108 ]
108 ]
109
109
110testParseListObjectsResult :: Assertion
110testParseListObjectsResult :: Assertion
111testParseListObjectsResult = do
111testParseListObjectsResult = do
112− let
112+ let xmldata =
113− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
113+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
114− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
114+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
115− \<Name>bucket</Name>\
115+ \<Name>bucket</Name>\
116− \<Prefix/>\
116+ \<Prefix/>\
117− \<NextContinuationToken>opaque</NextContinuationToken>\
117+ \<NextContinuationToken>opaque</NextContinuationToken>\
118− \<KeyCount>1000</KeyCount>\
118+ \<KeyCount>1000</KeyCount>\
119− \<MaxKeys>1000</MaxKeys>\
119+ \<MaxKeys>1000</MaxKeys>\
120− \<IsTruncated>true</IsTruncated>\
120+ \<IsTruncated>true</IsTruncated>\
121− \<Contents>\
121+ \<Contents>\
122− \<Key>my-image.jpg</Key>\
122+ \<Key>my-image.jpg</Key>\
123− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
123+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
124− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
124+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
125− \<Size>434234</Size>\
125+ \<Size>434234</Size>\
126− \<StorageClass>STANDARD</StorageClass>\
126+ \<StorageClass>STANDARD</StorageClass>\
127− \</Contents>\
127+ \</Contents>\
128− \</ListBucketResult>"
128+ \</ListBucketResult>"
129−
130− expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
129+ expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
131− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
130+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
132− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
131+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
133
132
134 parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
133 parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
135 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
134 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
136
135
137testParseListObjectsV1Result :: Assertion
136testParseListObjectsV1Result :: Assertion
138testParseListObjectsV1Result = do
137testParseListObjectsV1Result = do
139− let
138+ let xmldata =
140− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
139+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
141− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
140+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
142− \<Name>bucket</Name>\
141+ \<Name>bucket</Name>\
143− \<Prefix/>\
142+ \<Prefix/>\
144− \<NextMarker>my-image1.jpg</NextMarker>\
143+ \<NextMarker>my-image1.jpg</NextMarker>\
145− \<KeyCount>1000</KeyCount>\
144+ \<KeyCount>1000</KeyCount>\
146− \<MaxKeys>1000</MaxKeys>\
145+ \<MaxKeys>1000</MaxKeys>\
147− \<IsTruncated>true</IsTruncated>\
146+ \<IsTruncated>true</IsTruncated>\
148− \<Contents>\
147+ \<Contents>\
149− \<Key>my-image.jpg</Key>\
148+ \<Key>my-image.jpg</Key>\
150− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
149+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
151− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
150+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
152− \<Size>434234</Size>\
151+ \<Size>434234</Size>\
153− \<StorageClass>STANDARD</StorageClass>\
152+ \<StorageClass>STANDARD</StorageClass>\
154− \</Contents>\
153+ \</Contents>\
155− \</ListBucketResult>"
154+ \</ListBucketResult>"
156−
157− expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
155+ expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
158− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
156+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
159− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
157+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
160
158
161 parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
159 parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
162 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
160 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
163
161
164testParseListIncompleteUploads :: Assertion
162testParseListIncompleteUploads :: Assertion
165testParseListIncompleteUploads = do
163testParseListIncompleteUploads = do
166− let
164+ let xmldata =
167− xmldata = "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
165+ "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
168− \<Bucket>example-bucket</Bucket>\
166+ \<Bucket>example-bucket</Bucket>\
169− \<KeyMarker/>\
167+ \<KeyMarker/>\
170− \<UploadIdMarker/>\
168+ \<UploadIdMarker/>\
171− \<NextKeyMarker>sample.jpg</NextKeyMarker>\
169+ \<NextKeyMarker>sample.jpg</NextKeyMarker>\
172− \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
170+ \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
173− \<Delimiter>/</Delimiter>\
171+ \<Delimiter>/</Delimiter>\
174− \<Prefix/>\
172+ \<Prefix/>\
175− \<MaxUploads>1000</MaxUploads>\
173+ \<MaxUploads>1000</MaxUploads>\
176− \<IsTruncated>false</IsTruncated>\
174+ \<IsTruncated>false</IsTruncated>\
177− \<Upload>\
175+ \<Upload>\
178− \<Key>sample.jpg</Key>\
176+ \<Key>sample.jpg</Key>\
179− \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
177+ \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
180− \<Initiator>\
178+ \<Initiator>\
181− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
179+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
182− \<DisplayName>s3-nickname</DisplayName>\
180+ \<DisplayName>s3-nickname</DisplayName>\
183− \</Initiator>\
181+ \</Initiator>\
184− \<Owner>\
182+ \<Owner>\
185− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
183+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
186− \<DisplayName>s3-nickname</DisplayName>\
184+ \<DisplayName>s3-nickname</DisplayName>\
187− \</Owner>\
185+ \</Owner>\
188− \<StorageClass>STANDARD</StorageClass>\
186+ \<StorageClass>STANDARD</StorageClass>\
189− \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
187+ \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
190− \</Upload>\
188+ \</Upload>\
191− \<CommonPrefixes>\
189+ \<CommonPrefixes>\
192− \<Prefix>photos/</Prefix>\
190+ \<Prefix>photos/</Prefix>\
193− \</CommonPrefixes>\
191+ \</CommonPrefixes>\
194− \<CommonPrefixes>\
192+ \<CommonPrefixes>\
195− \<Prefix>videos/</Prefix>\
193+ \<Prefix>videos/</Prefix>\
196− \</CommonPrefixes>\
194+ \</CommonPrefixes>\
197− \</ListMultipartUploadsResult>"
195+ \</ListMultipartUploadsResult>"
198− expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
196+ expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
199− uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
197+ uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
200− initTime = UTCTime (fromGregorian 2010 11 26) 69857
198+ initTime = UTCTime (fromGregorian 2010 11 26) 69857
201− prefixes = ["photos/", "videos/"]
199+ prefixes = ["photos/", "videos/"]
202
200
203 parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
201 parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
204 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
202 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
205
203
206−
207testParseCompleteMultipartUploadResponse :: Assertion
204testParseCompleteMultipartUploadResponse :: Assertion
208testParseCompleteMultipartUploadResponse = do
205testParseCompleteMultipartUploadResponse = do
209− let
206+ let xmldata =
210− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
207+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
211−\<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
208+ \<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
212− \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
209+ \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
213− \<Bucket>Example-Bucket</Bucket>\
210+ \<Bucket>Example-Bucket</Bucket>\
214− \<Key>Example-Object</Key>\
211+ \<Key>Example-Object</Key>\
215− \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
212+ \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
216−\</CompleteMultipartUploadResult>"
213+ \</CompleteMultipartUploadResult>"
217− expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
214+ expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
218
215
219 parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
216 parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
220 eitherValidationErr parsedETagE (@?= expectedETag)
217 eitherValidationErr parsedETagE (@?= expectedETag)
221
218
222testParseListPartsResponse :: Assertion
219testParseListPartsResponse :: Assertion
223testParseListPartsResponse = do
220testParseListPartsResponse = do
224− let
221+ let xmldata =
225− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
222+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
226−\<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
223+ \<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
227− \<Bucket>example-bucket</Bucket>\
224+ \<Bucket>example-bucket</Bucket>\
228− \<Key>example-object</Key>\
225+ \<Key>example-object</Key>\
229− \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
226+ \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
230− \<Initiator>\
227+ \<Initiator>\
231− \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
228+ \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
232− \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
229+ \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
233− \</Initiator>\
230+ \</Initiator>\
234− \<Owner>\
231+ \<Owner>\
235− \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
232+ \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
236− \<DisplayName>someName</DisplayName>\
233+ \<DisplayName>someName</DisplayName>\
237− \</Owner>\
234+ \</Owner>\
238− \<StorageClass>STANDARD</StorageClass>\
235+ \<StorageClass>STANDARD</StorageClass>\
239− \<PartNumberMarker>1</PartNumberMarker>\
236+ \<PartNumberMarker>1</PartNumberMarker>\
240− \<NextPartNumberMarker>3</NextPartNumberMarker>\
237+ \<NextPartNumberMarker>3</NextPartNumberMarker>\
241− \<MaxParts>2</MaxParts>\
238+ \<MaxParts>2</MaxParts>\
242− \<IsTruncated>true</IsTruncated>\
239+ \<IsTruncated>true</IsTruncated>\
243− \<Part>\
240+ \<Part>\
244− \<PartNumber>2</PartNumber>\
241+ \<PartNumber>2</PartNumber>\
245− \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
242+ \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
246− \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
243+ \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
247− \<Size>10485760</Size>\
244+ \<Size>10485760</Size>\
248− \</Part>\
245+ \</Part>\
249− \<Part>\
246+ \<Part>\
250− \<PartNumber>3</PartNumber>\
247+ \<PartNumber>3</PartNumber>\
251− \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
248+ \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
252− \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
249+ \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
253− \<Size>10485760</Size>\
250+ \<Size>10485760</Size>\
254− \</Part>\
251+ \</Part>\
255−\</ListPartsResult>"
252+ \</ListPartsResult>"
256−
257− expectedListResult = ListPartsResult True (Just 3) [part1, part2]
253+ expectedListResult = ListPartsResult True (Just 3) [part1, part2]
258− part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
254+ part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
259− modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
255+ modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
260− part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
256+ part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
261− modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
257+ modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
262
258
263 parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
259 parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
264 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
260 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
265
261
266testParseCopyObjectResponse :: Assertion
262testParseCopyObjectResponse :: Assertion
267testParseCopyObjectResponse = do
263testParseCopyObjectResponse = do
268− let
264+ let cases =
269− cases = [ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
265+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
270−\<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
266+ \<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
271− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
267+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
272− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
268+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
273−\</CopyObjectResult>",
269+ \</CopyObjectResult>",
274− ("\"9b2cf535f27731c974343645a3985328\"",
270+ ( "\"9b2cf535f27731c974343645a3985328\"",
275− UTCTime (fromGregorian 2009 10 28) 81120))
271+ UTCTime (fromGregorian 2009 10 28) 81120
272+ )
273+ ),
276− , ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
274+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
277−\<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
275+ \<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
278− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
276+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
279− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
277+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
280−\</CopyPartResult>",
278+ \</CopyPartResult>",
281− ("\"9b2cf535f27731c974343645a3985328\"",
279+ ( "\"9b2cf535f27731c974343645a3985328\"",
282− UTCTime (fromGregorian 2009 10 28) 81120))]
280+ UTCTime (fromGregorian 2009 10 28) 81120
281+ )
282+ )
283+ ]
283
284
284 forM_ cases $ \(xmldata, (etag, modTime)) -> do
285 forM_ cases $ \(xmldata, (etag, modTime)) -> do
285 parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
286 parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
@@ -287,73 +288,89 @@ testParseCopyObjectResponse = do
287
288
288testParseNotification :: Assertion
289testParseNotification :: Assertion
289testParseNotification = do
290testParseNotification = do
290− let
291+ let cases =
291− cases = [ ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
292+ [ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
292−\ <TopicConfiguration>\
293+ \ <TopicConfiguration>\
293−\ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
294+ \ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
294−\ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
295+ \ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
295−\ <Event>s3:ReducedRedundancyLostObject</Event>\
296+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
296−\ <Event>s3:ObjectCreated:*</Event>\
297+ \ <Event>s3:ObjectCreated:*</Event>\
297−\ </TopicConfiguration>\
298+ \ </TopicConfiguration>\
298−\</NotificationConfiguration>",
299+ \</NotificationConfiguration>",
299− Notification []
300+ Notification
301+ []
300− [ NotificationConfig
302+ [ NotificationConfig
301 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
303 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
302 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
304 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
303− [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
305+ [ReducedRedundancyLostObject, ObjectCreated]
306+ defaultFilter
304− ]
307+ ]
305− [])
308+ []
309+ ),
306− , ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
310+ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
307−\ <CloudFunctionConfiguration>\
311+ \ <CloudFunctionConfiguration>\
308−\ <Id>ObjectCreatedEvents</Id>\
312+ \ <Id>ObjectCreatedEvents</Id>\
309−\ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
313+ \ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
310−\ <Event>s3:ObjectCreated:*</Event>\
314+ \ <Event>s3:ObjectCreated:*</Event>\
311−\ </CloudFunctionConfiguration>\
315+ \ </CloudFunctionConfiguration>\
312−\ <QueueConfiguration>\
316+ \ <QueueConfiguration>\
313−\ <Id>1</Id>\
317+ \ <Id>1</Id>\
314−\ <Filter>\
318+ \ <Filter>\
315−\ <S3Key>\
319+ \ <S3Key>\
316−\ <FilterRule>\
320+ \ <FilterRule>\
317−\ <Name>prefix</Name>\
321+ \ <Name>prefix</Name>\
318−\ <Value>images/</Value>\
322+ \ <Value>images/</Value>\
319−\ </FilterRule>\
323+ \ </FilterRule>\
320−\ <FilterRule>\
324+ \ <FilterRule>\
321−\ <Name>suffix</Name>\
325+ \ <Name>suffix</Name>\
322−\ <Value>.jpg</Value>\
326+ \ <Value>.jpg</Value>\
323−\ </FilterRule>\
327+ \ </FilterRule>\
324−\ </S3Key>\
328+ \ </S3Key>\
325−\ </Filter>\
329+ \ </Filter>\
326−\ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
330+ \ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
327−\ <Event>s3:ObjectCreated:Put</Event>\
331+ \ <Event>s3:ObjectCreated:Put</Event>\
328−\ </QueueConfiguration>\
332+ \ </QueueConfiguration>\
329−\ <TopicConfiguration>\
333+ \ <TopicConfiguration>\
330−\ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
334+ \ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
331−\ <Event>s3:ReducedRedundancyLostObject</Event>\
335+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
332−\ </TopicConfiguration>\
336+ \ </TopicConfiguration>\
333−\ <QueueConfiguration>\
337+ \ <QueueConfiguration>\
334−\ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
338+ \ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
335−\ <Event>s3:ObjectCreated:*</Event>\
339+ \ <Event>s3:ObjectCreated:*</Event>\
336−\ </QueueConfiguration>)\
340+ \ </QueueConfiguration>)\
337−\</NotificationConfiguration>",
341+ \</NotificationConfiguration>",
342+ Notification
338− Notification [ NotificationConfig
343+ [ NotificationConfig
344+ "1"
339− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
345+ "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
340− [ObjectCreatedPut]
346+ [ObjectCreatedPut]
347+ ( Filter $
348+ FilterKey $
341− (Filter $ FilterKey $ FilterRules
349+ FilterRules
342− [FilterRule "prefix" "images/",
350+ [ FilterRule "prefix" "images/",
343− FilterRule "suffix" ".jpg"])
351+ FilterRule "suffix" ".jpg"
352+ ]
353+ ),
344− , NotificationConfig
354+ NotificationConfig
355+ ""
345− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
356+ "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
346− [ObjectCreated] defaultFilter
357+ [ObjectCreated]
358+ defaultFilter
347− ]
359+ ]
348− [ NotificationConfig
360+ [ NotificationConfig
361+ ""
349− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
362+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
350− [ReducedRedundancyLostObject] defaultFilter
363+ [ReducedRedundancyLostObject]
364+ defaultFilter
351− ]
365+ ]
352− [ NotificationConfig
366+ [ NotificationConfig
367+ "ObjectCreatedEvents"
353− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
368+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
354− [ObjectCreated] defaultFilter
369+ [ObjectCreated]
370+ defaultFilter
355− ])
371+ ]
372+ )
356− ]
373+ ]
357
374
358 forM_ cases $ \(xmldata, val) -> do
375 forM_ cases $ \(xmldata, val) -> do
359 result <- runExceptT $ runTestNS $ parseNotification xmldata
376 result <- runExceptT $ runTestNS $ parseNotification xmldata
@@ -362,20 +379,25 @@ testParseNotification = do
362-- | Tests parsing of both progress and stats
379-- | Tests parsing of both progress and stats
363testParseSelectProgress :: Assertion
380testParseSelectProgress :: Assertion
364testParseSelectProgress = do
381testParseSelectProgress = do
382+ let cases =
365− let cases = [ ([r|<?xml version="1.0" encoding="UTF-8"?>
383+ [ ( [r|<?xml version="1.0" encoding="UTF-8"?>
366<Progress>
384<Progress>
367 <BytesScanned>512</BytesScanned>
385 <BytesScanned>512</BytesScanned>
368 <BytesProcessed>1024</BytesProcessed>
386 <BytesProcessed>1024</BytesProcessed>
369 <BytesReturned>1024</BytesReturned>
387 <BytesReturned>1024</BytesReturned>
388+</Progress>|],
370−</Progress>|] , Progress 512 1024 1024)
389+ Progress 512 1024 1024
390+ ),
371− , ([r|<?xml version="1.0" encoding="UTF-8"?>
391+ ( [r|<?xml version="1.0" encoding="UTF-8"?>
372<Stats>
392<Stats>
373 <BytesScanned>512</BytesScanned>
393 <BytesScanned>512</BytesScanned>
374 <BytesProcessed>1024</BytesProcessed>
394 <BytesProcessed>1024</BytesProcessed>
375 <BytesReturned>1024</BytesReturned>
395 <BytesReturned>1024</BytesReturned>
396+</Stats>|],
376−</Stats>|], Progress 512 1024 1024)
397+ Progress 512 1024 1024
398+ )
377− ]
399+ ]
378−
400+
379− forM_ cases $ \(xmldata, progress) -> do
401+ forM_ cases $ \(xmldata, progress) -> do
380− result <- runExceptT $ parseSelectProgress xmldata
402+ result <- runExceptT $ parseSelectProgress xmldata
381− eitherValidationErr result (@?= progress)
403+ eitherValidationErr result (@?= progress)
test/Spec.hs+83−82
@@ -1,5 +1,5 @@
1--
1--
2−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,21 +14,17 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17−import Test.Tasty
18−import Test.Tasty.QuickCheck as QC
19−
20−import qualified Data.ByteString as B
17+import qualified Data.ByteString as B
21−import qualified Data.List as L
18+import qualified Data.List as L
22−
23−import Lib.Prelude
19+import Lib.Prelude
24−
25−import Network.Minio.API.Test
20+import Network.Minio.API.Test
26−import Network.Minio.CopyObject
21+import Network.Minio.CopyObject
27−import Network.Minio.Data
22+import Network.Minio.Data
28−import Network.Minio.PutObject
29−import Network.Minio.Utils.Test
23+import Network.Minio.Utils.Test
30−import Network.Minio.XmlGenerator.Test
24+import Network.Minio.XmlGenerator.Test
31−import Network.Minio.XmlParser.Test
25+import Network.Minio.XmlParser.Test
26+import Test.Tasty
27+import Test.Tasty.QuickCheck as QC
32
28
33main :: IO ()
29main :: IO ()
34main = defaultMain tests
30main = defaultMain tests
@@ -51,82 +47,87 @@ properties = testGroup "Properties" [qcProps] -- [scProps]
51-- ]
47-- ]
52
48
53qcProps :: TestTree
49qcProps :: TestTree
50+qcProps =
51+ testGroup
54−qcProps = testGroup "(checked by QuickCheck)"
52+ "(checked by QuickCheck)"
55− [ QC.testProperty "selectPartSizes:" $
53+ [ QC.testProperty "selectPartSizes:" $
54+ \n ->
56− \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
55+ let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
57−
58 -- check that pns increments from 1.
56 -- check that pns increments from 1.
59− isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1..]
57+ isPNumsAscendingFrom1 = all (uncurry (==)) $ zip pns [1 ..]
60−
61− consPairs [] = []
58+ consPairs [] = []
62− consPairs [_] = []
59+ consPairs [_] = []
63− consPairs (a:(b:c)) = (a, b):(consPairs (b:c))
60+ consPairs (a : (b : c)) = (a, b) : consPairs (b : c)
64−
65 -- check `offs` is monotonically increasing.
61 -- check `offs` is monotonically increasing.
66− isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs
62+ isOffsetsAsc = all (uncurry (<)) $ consPairs offs
67−
68 -- check sizes sums to n.
63 -- check sizes sums to n.
69 isSumSizeOk = sum sizes == n
64 isSumSizeOk = sum sizes == n
70−
71 -- check sizes are constant except last
65 -- check sizes are constant except last
72 isSizesConstantExceptLast =
66 isSizesConstantExceptLast =
73− all (\(a, b) -> a == b) (consPairs $ L.init sizes)
67+ all (uncurry (==)) (consPairs $ L.init sizes)
74−
75 -- check each part except last is at least minPartSize;
68 -- check each part except last is at least minPartSize;
76 -- last part may be 0 only if it is the only part.
69 -- last part may be 0 only if it is the only part.
77 nparts = length sizes
70 nparts = length sizes
78 isMinPartSizeOk =
71 isMinPartSizeOk =
72+ if
79− if | nparts > 1 -> -- last part can be smaller but > 0
73+ | nparts > 1 -> -- last part can be smaller but > 0
80− all (>= minPartSize) (take (nparts - 1) sizes) &&
74+ all (>= minPartSize) (take (nparts - 1) sizes)
81− all (\s -> s > 0) (drop (nparts - 1) sizes)
75+ && all (> 0) (drop (nparts - 1) sizes)
82− | nparts == 1 -> -- size may be 0 here.
76+ | nparts == 1 -> -- size may be 0 here.
83− maybe True (\x -> x >= 0 && x <= minPartSize) $
77+ maybe True (\x -> x >= 0 && x <= minPartSize) $
84− headMay sizes
78+ listToMaybe sizes
85− | otherwise -> False
79+ | otherwise -> False
86−
87− in n < 0 ||
80+ in n < 0
88− (isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk &&
81+ || ( isPNumsAscendingFrom1
82+ && isOffsetsAsc
83+ && isSumSizeOk
89− isSizesConstantExceptLast && isMinPartSizeOk)
84+ && isSizesConstantExceptLast
90−
85+ && isMinPartSizeOk
86+ ),
91− , QC.testProperty "selectCopyRanges:" $
87+ QC.testProperty "selectCopyRanges:" $
92− \(start, end) ->
88+ \(start, end) ->
93− let (_, pairs) = L.unzip (selectCopyRanges (start, end))
89+ let (_, pairs) = L.unzip (selectCopyRanges (start, end))
94−
95− -- is last part's snd offset end?
90+ -- is last part's snd offset end?
96− isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
91+ isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
97− -- is first part's fst offset start
92+ -- is first part's fst offset start
98− isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
93+ isFirstPartOk = maybe False ((start ==) . fst) $ listToMaybe pairs
99−
100− -- each pair is >=64MiB except last, and all those parts
94+ -- each pair is >=64MiB except last, and all those parts
101− -- have same size.
95+ -- have same size.
102− initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs
96+ initSizes = maybe [] (map (\(a, b) -> b - a + 1) . init) (nonEmpty pairs)
97+ isPartSizesOk =
103− isPartSizesOk = all (>= minPartSize) initSizes &&
98+ all (>= minPartSize) initSizes
99+ && maybe
100+ True
104− maybe True (\k -> all (== k) initSizes)
101+ (\k -> all (== k) initSizes)
105− (headMay initSizes)
102+ (listToMaybe initSizes)
106−
107− -- returned offsets are contiguous.
103+ -- returned offsets are contiguous.
108− fsts = drop 1 $ map fst pairs
104+ fsts = drop 1 $ map fst pairs
109− snds = take (length pairs - 1) $ map snd pairs
105+ snds = take (length pairs - 1) $ map snd pairs
106+ isContParts =
110− isContParts = length fsts == length snds &&
107+ length fsts == length snds
111− and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
108+ && all (\(a, b) -> a == b + 1) (zip fsts snds)
112−
113− in start < 0 || start > end ||
109+ in start < 0
110+ || start > end
114− (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts)
111+ || (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts),
115−
116− , QC.testProperty "mkSSECKey:" $
112+ QC.testProperty "mkSSECKey:" $
113+ \w8s ->
117− \w8s -> let bs = B.pack w8s
114+ let bs = B.pack w8s
118− r = mkSSECKey bs
115+ r = mkSSECKey bs
119− in case r of
116+ in case r of
120− Just _ -> B.length bs == 32
117+ Just _ -> B.length bs == 32
121 Nothing -> B.length bs /= 32
118 Nothing -> B.length bs /= 32
122− ]
119+ ]
123
120
124unitTests :: TestTree
121unitTests :: TestTree
122+unitTests =
123+ testGroup
124+ "Unit tests"
125−unitTests = testGroup "Unit tests" [ xmlGeneratorTests, xmlParserTests
125+ [ xmlGeneratorTests,
126+ xmlParserTests,
126− , bucketNameValidityTests
127+ bucketNameValidityTests,
127− , objectNameValidityTests
128+ objectNameValidityTests,
128− , parseServerInfoJSONTest
129+ parseServerInfoJSONTest,
129− , parseHealStatusTest
130+ parseHealStatusTest,
130− , parseHealStartRespTest
131+ parseHealStartRespTest,
131− , limitedMapConcurrentlyTests
132+ limitedMapConcurrentlyTests
132− ]
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-----