git.iko.soy/typeable/minio-hs
on ae141fd help sign in
70 files changed+4776−6684
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml−266 .stylish-haskell.yaml+233 .travis.yml+61 CHANGELOG.md−31 CNAME−1 README.md+29−50 Setup.hs+18 examples/AssumeRole.hs−47 examples/BucketExists.hs+7−4 examples/CopyObject.hs+14−12 examples/FileUploader.hs+24−23 examples/GetConfig.hs+6−6 examples/GetObject.hs+10−5 examples/HeadObject.hs+11−8 examples/Heal.hs+8−11 examples/ListBuckets.hs+5−3 examples/ListIncompleteUploads.hs+19−17 examples/ListObjects.hs+15−13 examples/MakeBucket.hs+9−6 examples/PresignedGetObject.hs+28−32 examples/PresignedPostPolicy.hs+39−42 examples/PresignedPutObject.hs+21−20 examples/PutObject.hs+14−11 examples/RemoveBucket.hs+8−3 examples/RemoveIncompleteUpload.hs+10−7 examples/RemoveObject.hs+8−7 examples/SelectObject.hs+26−24 examples/ServerInfo.hs+7−7 examples/ServiceSendRestart.hs+7−6 examples/ServiceSendStop.hs+7−6 examples/ServiceStatus.hs+7−7 examples/SetConfig.hs+4−3 minio-hs.cabal+17−73 src/Lib/Prelude.hs+11−33 src/Network/Minio.hs+218−249 src/Network/Minio/API.hs+159−259 src/Network/Minio/APICommon.hs+24−36 src/Network/Minio/AdminAPI.hs+391−543 src/Network/Minio/CopyObject.hs+34−48 src/Network/Minio/Credentials.hs−77 src/Network/Minio/Credentials/AssumeRole.hs−266 src/Network/Minio/Credentials/Types.hs−90 src/Network/Minio/Data.hs+559−697 src/Network/Minio/Data/ByteString.hs+18−19 src/Network/Minio/Data/Crypto.hs+33−32 src/Network/Minio/Data/Time.hs+13−17 src/Network/Minio/Errors.hs+52−59 src/Network/Minio/JsonParser.hs+16−23 src/Network/Minio/ListOps.hs+38−98 src/Network/Minio/PresignedOperations.hs+152−215 src/Network/Minio/PutObject.hs+69−85 src/Network/Minio/S3API.hs+319−481 src/Network/Minio/SelectAPI.hs+238−233 src/Network/Minio/Sign/V4.hs+327−430 src/Network/Minio/Utils.hs+127−140 src/Network/Minio/XmlCommon.hs−65 src/Network/Minio/XmlGenerator.hs+99−165 src/Network/Minio/XmlParser.hs+154−107 stack.yaml+4−4 stack.yaml.lock−19 test/LiveServer.hs+480−754 test/Network/Minio/API/Test.hs+61−75 test/Network/Minio/JsonParser/Test.hs+27−26 test/Network/Minio/TestHelpers.hs+8−8 test/Network/Minio/Utils/Test.hs+17−15 test/Network/Minio/XmlGenerator/Test.hs+98−137 test/Network/Minio/XmlParser/Test.hs+276−298 test/Spec.hs+82−83 test/cert/private.key−28 test/cert/public.crt−19
.github/workflows/ci.ymldeleted−266
@@ -1,266 +0,0 @@
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.yamlnew file+233
@@ -0,0 +1,233 @@
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.ymlnew file+61
@@ -0,0 +1,61 @@
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,37 +1,6 @@
11Changelog
22==========
33
4−## Version 1.7.0 -- Unreleased
5−
6−* Fix data type `EventMessage` to not export partial fields (#179)
7−* Bump up min bound on time dep and fix deprecation warnings (#181)
8−* Add `dev` flag to cabal for building with warnings as errors (#182)
9−* Fix AWS region map (#185)
10−* Fix XML generator tests (#187)
11−* Add support for STS Assume Role API (#188)
12−
13−### Breaking changes in 1.7.0
14−
15−* `Credentials` type has been removed. Use `CredentialValue` instead.
16−* `Provider` type has been replaced with `CredentialLoader`.
17−* `EventMessage` data type is updated.
18−
19−## Version 1.6.0
20−
21−* HLint fixes - some types were changed to newtype (#173)
22−* Fix XML generation test for S3 SELECT (#161)
23−* Use region specific endpoints for AWS S3 in presigned Urls (#164)
24−* Replace protolude with relude and build with GHC 9.0.2 (#168)
25−* Support aeson 2 (#169)
26−* CI updates and code formatting changes with ormolu 0.5.0.0
27−
28−## Version 1.5.3
29−
30−* Fix windows build
31−* Fix support for Yandex Storage (#147)
32−* Fix for HEAD requests to S3/Minio (#155)
33−* Bump up some dependencies, new code formatting, Github CI, example fixes and other minor improvements.
34−
354## Version 1.5.2
365
376* Fix region `us-west-2` for AWS S3 (#139)
CNAMEdeleted−1
@@ -1 +0,0 @@
1−minio-hs.min.io
README.md+29−50
@@ -1,8 +1,10 @@
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)
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)
22
3−The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
3+The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and Amazon S3 compatible object storage server.
44
5−This guide assumes that you have a working [Haskell development environment](https://www.haskell.org/downloads/).
5+## Minimum Requirements
6+
7+- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
68
79## Installation
810
@@ -10,35 +12,20 @@ This guide assumes that you have a working [Haskell development environment](htt
1012
1113Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
1214
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
15+### Try it out directly with `ghci`
3216
3317From your home folder or any non-haskell project directory, just run:
3418
3519```sh
20+
3621stack install minio-hs
22+
3723```
3824
3925Then start an interpreter session and browse the available APIs with:
4026
4127```sh
28+
4229$ stack ghci
4330> :browse Network.Minio
4431```
@@ -147,52 +134,44 @@ main = do
147134
148135### Development
149136
150−#### Download the source
137+To setup:
151138
152139```sh
153−$ git clone https://github.com/minio/minio-hs.git
154−$ cd minio-hs/
155−```
140+git clone https://github.com/minio/minio-hs.git
141+
142+cd minio-hs/
156143
157−#### Build the package:
144+stack install
145+```
158146
159−With `cabal`:
147+Tests can be run with:
160148
161149```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−```
166150
167−With `stack`:
151+stack test
168152
169−``` sh
170−$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
171153```
172−#### Running tests:
173154
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).
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).
175156
176−With `cabal`:
157+To run the live server tests, set a build flag as shown below:
177158
178159```sh
179−$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
180−$ cabal test
181−```
182160
183−With `stack`:
161+stack test --flag minio-hs:live-test
162+
163+# OR against a local MinIO server with:
164+
165+MINIO_LOCAL=1 stack test --flag minio-hs:live-test
184166
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
188167```
189168
190−This will run all the test suites.
169+The configured CI system always runs both test-suites for every change.
191170
192−#### Building documentation:
171+Documentation can be locally built with:
193172
194173```sh
195−$ cabal haddock
196−$ # OR
197−$ stack haddock
174+
175+stack haddock
176+
198177```
Setup.hsnew file+18
@@ -0,0 +1,18 @@
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.hsdeleted−47
@@ -1,47 +0,0 @@
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+7−4
@@ -16,17 +16,20 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Control.Monad.IO.Class (liftIO)
22−import Network.Minio
23−import Prelude
23+import Control.Monad.IO.Class (liftIO)
24+import Prelude
2425
2526-- | The following example uses minio's play server at
2627-- https://play.min.io. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
32+
3033main :: IO ()
3134main = do
3235 let bucket = "missingbucket"
@@ -36,5 +39,5 @@ main = do
3639 liftIO $ putStrLn $ "Does " ++ show bucket ++ " exist? - " ++ show foundBucket
3740
3841 case res1 of
39− Left e -> putStrLn $ "bucketExists failed." ++ show e
42+ Left e -> putStrLn $ "bucketExists failed." ++ show e
4043 Right () -> return ()
examples/CopyObject.hs+14−12
@@ -16,40 +16,42 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Network.Minio
22−import UnliftIO.Exception (catch, throwIO)
23+import UnliftIO.Exception (catch, throwIO)
2324
2425-- | The following example uses minio's play server at
2526-- https://play.min.io. The endpoint and associated
2627-- credentials are provided via the libary constant,
2728--
2829-- > minioPlayCI :: ConnectInfo
30+--
31+
2932main :: IO ()
3033main = do
31− let bucket = "test"
34+ let
35+ bucket = "test"
3236 object = "obj"
3337 objectCopy = "obj-copy"
3438 localFile = "/etc/lsb-release"
3539
3640 res1 <- runMinio minioPlayCI $ do
3741 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
38− catch
39− (makeBucket bucket Nothing)
40− ( \e -> case e of
41− BucketAlreadyOwnedByYou -> return ()
42− _ -> throwIO e
42+ catch (makeBucket bucket Nothing) (
43+ \e -> case e of
44+ BucketAlreadyOwnedByYou -> return ()
45+ _ -> throwIO e
4346 )
4447
4548 -- 2. Upload a file to bucket/object.
4649 fPutObject bucket object localFile defaultPutObjectOptions
4750
4851 -- 3. Copy bucket/object to bucket/objectCopy.
49− copyObject
50− defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
51− defaultSourceInfo {srcBucket = bucket, srcObject = object}
52+ copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
53+ defaultSourceInfo { srcBucket = bucket , srcObject = object }
5254
5355 case res1 of
54− Left e -> putStrLn $ "copyObject failed." ++ show e
56+ Left e -> putStrLn $ "copyObject failed." ++ show e
5557 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+24−23
@@ -16,39 +16,40 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−{-# LANGUAGE OverloadedStrings #-}
19+
20+
21+{-# LANGUAGE OverloadedStrings #-}
2022{-# LANGUAGE ScopedTypeVariables #-}
23+import Network.Minio
2124
22−import Data.Text (pack)
23−import Network.Minio
24−import Options.Applicative
25−import System.FilePath.Posix
26−import UnliftIO (throwIO, try)
27−import Prelude
25+import Data.Monoid ((<>))
26+import Data.Text (pack)
27+import Options.Applicative
28+import System.FilePath.Posix
29+import UnliftIO (throwIO, try)
30+
31+import Prelude
2832
2933-- | The following example uses minio's play server at
3034-- https://play.min.io. The endpoint and associated
3135-- credentials are provided via the libary constant,
3236--
3337-- > minioPlayCI :: ConnectInfo
38+--
3439
3540-- optparse-applicative package based command-line parsing.
3641fileNameArgs :: Parser FilePath
37−fileNameArgs =
38− strArgument
39− ( metavar "FILENAME"
40− <> help "Name of file to upload to AWS S3 or a MinIO server"
41− )
42+fileNameArgs = strArgument
43+ (metavar "FILENAME"
44+ <> help "Name of file to upload to AWS S3 or a MinIO server")
4245
4346cmdParser :: ParserInfo FilePath
44−cmdParser =
45− info
46− (helper <*> fileNameArgs)
47− ( fullDesc
48− <> progDesc "FileUploader"
49− <> header
50− "FileUploader - a simple file-uploader program using minio-hs"
51− )
47+cmdParser = info
48+ (helper <*> fileNameArgs)
49+ (fullDesc
50+ <> progDesc "FileUploader"
51+ <> header
52+ "FileUploader - a simple file-uploader program using minio-hs")
5253
5354main :: IO ()
5455main = do
@@ -63,12 +64,12 @@ main = do
6364 bErr <- try $ makeBucket bucket Nothing
6465 case bErr of
6566 Left BucketAlreadyOwnedByYou -> return ()
66− Left e -> throwIO e
67− Right _ -> return ()
67+ Left e -> throwIO e
68+ Right _ -> return ()
6869
6970 -- Upload filepath to bucket; object is derived from filepath.
7071 fPutObject bucket object filepath defaultPutObjectOptions
7172
7273 case res of
73− Left e -> putStrLn $ "file upload failed due to " ++ show e
74+ Left e -> putStrLn $ "file upload failed due to " ++ (show e)
7475 Right () -> putStrLn "file upload succeeded."
examples/GetConfig.hs+6−6
@@ -17,14 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio
28− minioPlayCI
28+ res <- runMinio minioPlayCI $
2929 getConfig
3030 print res
examples/GetObject.hs+10−5
@@ -16,26 +16,31 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import qualified Data.Conduit as C
23+import qualified Data.Conduit as C
2224import qualified Data.Conduit.Binary as CB
23−import Network.Minio
24−import Prelude
25+
26+import Prelude
2527
2628-- | The following example uses minio's play server at
2729-- https://play.min.io. The endpoint and associated
2830-- credentials are provided via the libary constant,
2931--
3032-- > minioPlayCI :: ConnectInfo
33+--
34+
3135main :: IO ()
3236main = do
33− let bucket = "my-bucket"
37+ let
38+ bucket = "my-bucket"
3439 object = "my-object"
3540 res <- runMinio minioPlayCI $ do
3641 src <- getObject bucket object defaultGetObjectOptions
3742 C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
3843
3944 case res of
40− Left e -> putStrLn $ "getObject failed." ++ show e
45+ Left e -> putStrLn $ "getObject failed." ++ (show e)
4146 Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs+11−8
@@ -16,25 +16,28 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.S3API
2023
21−import Network.Minio
22−import Network.Minio.S3API
23−import Prelude
24+import Prelude
2425
2526-- | The following example uses minio's play server at
2627-- https://play.min.io. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
32+
3033main :: IO ()
3134main = do
32− let bucket = "test"
35+ let
36+ bucket = "test"
3337 object = "passwd"
34− res <-
35− runMinio minioPlayCI $
36− headObject bucket object []
38+ res <- runMinio minioPlayCI $
39+ headObject bucket object []
3740
3841 case res of
39− Left e -> putStrLn $ "headObject failed." ++ show e
42+ Left e -> putStrLn $ "headObject failed." ++ show e
4043 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
examples/Heal.hs+8−11
@@ -17,21 +17,18 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
2628 res <- runMinio minioPlayCI $
2729 do
28− hsr <-
29− startHeal
30− Nothing
31− Nothing
32− HealOpts
33− { hoRecursive = True,
34− hoDryRun = False
35− }
30+ hsr <- startHeal Nothing Nothing HealOpts { hoRecursive = True
31+ , hoDryRun = False
32+ }
3633 getHealStatus Nothing Nothing (hsrClientToken hsr)
3734 print res
examples/ListBuckets.hs+5−3
@@ -16,17 +16,19 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Control.Monad.IO.Class (liftIO)
22−import Network.Minio
23−import Prelude
23+import Control.Monad.IO.Class (liftIO)
24+import Prelude
2425
2526-- | The following example uses minio's play server at
2627-- https://play.min.io. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
3032
3133-- This example list buckets that belongs to the user and returns
3234-- region of the first bucket returned.
examples/ListIncompleteUploads.hs+19−17
@@ -16,36 +16,38 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Conduit
22−import Network.Minio
23−import Prelude
23+import Conduit
24+import Prelude
2425
2526-- | The following example uses minio's play server at
2627-- https://play.min.io. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
32+
3033main :: IO ()
3134main = do
32− let bucket = "test"
35+ let
36+ bucket = "test"
3337
3438 -- Performs a recursive listing of incomplete uploads under bucket "test"
3539 -- on a local minio server.
36− res <-
37− runMinio minioPlayCI $
38− runConduit $
39− listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
40+ res <- runMinio minioPlayCI $
41+ runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
4042 print res
4143
42−{-
43− Following is the output of the above program on a local MinIO server.
44+ {-
45+ Following is the output of the above program on a local MinIO server.
4446
45− Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
46− , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
47− , uiInitTime = 2017-03-01 10:16:25.698 UTC
48− , uiSize = 17731794
49− }
50− ]
51−-}
47+ Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
48+ , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
49+ , uiInitTime = 2017-03-01 10:16:25.698 UTC
50+ , uiSize = 17731794
51+ }
52+ ]
53+ -}
examples/ListObjects.hs+15−13
@@ -16,31 +16,33 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+
23+import Conduit
24+import Prelude
2025
21−import Conduit
22−import Network.Minio
23−import Prelude
2426
2527-- | The following example uses minio's play server at
2628-- https://play.min.io. The endpoint and associated
2729-- credentials are provided via the libary constant,
2830--
2931-- > minioPlayCI :: ConnectInfo
32+--
33+
3034main :: IO ()
3135main = do
32− let bucket = "test"
36+ let
37+ bucket = "test"
3338
3439 -- Performs a recursive listing of all objects under bucket "test"
3540 -- on play.min.io.
36− res <-
37− runMinio minioPlayCI $
38− runConduit $
39− listObjects bucket Nothing True .| mapM_C (liftIO . print)
41+ res <- runMinio minioPlayCI $
42+ runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
4043 print res
44+ {-
45+ Following is the output of the above program on a local MinIO server.
4146
42−{-
43− Following is the output of the above program on a local MinIO server.
44−
45− Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]
46−-}
47+ Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]
48+ -}
examples/MakeBucket.hs+9−6
@@ -16,21 +16,24 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
20+
1921{-# LANGUAGE OverloadedStrings #-}
22+import Network.Minio
2023
21−import Network.Minio
22−import Prelude
24+import Prelude
2325
2426-- | The following example uses minio's play server at
2527-- https://play.min.io. The endpoint and associated
2628-- credentials are provided via the libary constant,
2729--
2830-- > minioPlayCI :: ConnectInfo
31+--
32+
2933main :: IO ()
3034main = do
3135 let bucket = "my-bucket"
32− res <-
33− runMinio minioPlayCI $
34− -- N B the region provided for makeBucket is optional.
35− makeBucket bucket (Just "us-east-1")
36+ res <- runMinio minioPlayCI $
37+ -- N B the region provided for makeBucket is optional.
38+ makeBucket bucket (Just "us-east-1")
3639 print res
examples/PresignedGetObject.hs+28−32
@@ -16,37 +16,39 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Control.Monad.IO.Class (liftIO)
22−import qualified Data.ByteString.Char8 as B
23−import Data.CaseInsensitive (original)
23+import Control.Monad.IO.Class (liftIO)
24+import qualified Data.ByteString.Char8 as B
25+import Data.CaseInsensitive (original)
2426import qualified Data.Conduit.Combinators as CC
25−import qualified Data.Text.Encoding as E
26−import Network.Minio
27+import qualified Data.Text.Encoding as E
2728
2829-- | The following example uses minio's play server at
2930-- https://play.min.io. The endpoint and associated
3031-- credentials are provided via the libary constant,
3132--
3233-- > minioPlayCI :: ConnectInfo
34+--
35+
3336main :: IO ()
3437main = do
35− let bucket = "my-bucket"
36− object = "my-object"
37− kb15 = 15 * 1024
38− -- Set query parameter to modify content disposition response
39− -- header
40− queryParam =
41− [ ( "response-content-disposition",
42− Just "attachment; filename=\"your-filename.txt\""
43− )
44− ]
38+ let
39+ bucket = "my-bucket"
40+ object = "my-object"
41+ kb15 = 15*1024
42+
43+ -- Set query parameter to modify content disposition response
44+ -- header
45+ queryParam = [("response-content-disposition",
46+ Just "attachment; filename=\"your-filename.txt\"")]
4547
4648 res <- runMinio minioPlayCI $ do
4749 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
4850 putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
49− liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
51+ liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
5052
5153 -- Extract Etag of uploaded object
5254 oi <- statObject bucket object defaultGetObjectOptions
@@ -59,29 +61,23 @@ main = do
5961 -- Generate a URL with 7 days expiry time - note that the headers
6062 -- used above must be added to the request with the signed URL
6163 -- generated.
62− url <-
63− presignedGetObjectUrl
64− "my-bucket"
65− "my-object"
66− (7 * 24 * 3600)
67− queryParam
68− headers
64+ url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)
65+ queryParam headers
6966
7067 return (headers, etag, url)
7168
7269 case res of
7370 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
7471 Right (headers, _, url) -> do
72+
7573 -- We generate a curl command to demonstrate usage of the signed
7674 -- URL.
77− let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
78− curlCmd =
79− B.intercalate " " $
80− ["curl --fail"]
81− ++ map hdrOpt headers
82− ++ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
75+ let
76+ hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
77+ curlCmd = B.intercalate " " $
78+ ["curl --fail"] ++ map hdrOpt headers ++
79+ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
8380
84− putStrLn $
85− "The following curl command would use the presigned "
86− ++ "URL to fetch the object and write it to \"/tmp/myfile\":"
81+ putStrLn $ "The following curl command would use the presigned " ++
82+ "URL to fetch the object and write it to \"/tmp/myfile\":"
8783 B.putStrLn curlCmd
examples/PresignedPostPolicy.hs+39−42
@@ -16,72 +16,69 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import qualified Data.ByteString as B
23+import qualified Data.ByteString as B
2224import qualified Data.ByteString.Char8 as Char8
23−import qualified Data.HashMap.Strict as H
24−import qualified Data.Text.Encoding as Enc
25−import qualified Data.Time as Time
26−import Network.Minio
25+import qualified Data.HashMap.Strict as H
26+import qualified Data.Text.Encoding as Enc
27+import qualified Data.Time as Time
2728
2829-- | The following example uses minio's play server at
2930-- https://play.min.io. The endpoint and associated
3031-- credentials are provided via the libary constant,
3132--
3233-- > minioPlayCI :: ConnectInfo
34+--
35+
3336main :: IO ()
3437main = do
3538 now <- Time.getCurrentTime
36− let bucket = "my-bucket"
37− object = "photos/my-object"
38− -- set an expiration time of 10 days
39− expireTime = Time.addUTCTime (3600 * 24 * 10) now
40− -- create a policy with expiration time and conditions - since the
41− -- conditions are validated, newPostPolicy returns an Either value
42− policyE =
43− newPostPolicy
44− expireTime
45− [ -- set the object name condition
46− ppCondKey object,
47− -- set the bucket name condition
48− ppCondBucket bucket,
49− -- set the size range of object as 1B to 10MiB
50− ppCondContentLengthRange 1 (10 * 1024 * 1024),
51− -- set content type as jpg image
52− ppCondContentType "image/jpeg",
53− -- on success set the server response code to 200
54− ppCondSuccessActionStatus 200
55− ]
39+ let
40+ bucket = "my-bucket"
41+ object = "photos/my-object"
42+
43+ -- set an expiration time of 10 days
44+ expireTime = Time.addUTCTime (3600 * 24 * 10) now
45+
46+ -- create a policy with expiration time and conditions - since the
47+ -- conditions are validated, newPostPolicy returns an Either value
48+ policyE = newPostPolicy expireTime
49+ [ -- set the object name condition
50+ ppCondKey object
51+ -- set the bucket name condition
52+ , ppCondBucket bucket
53+ -- set the size range of object as 1B to 10MiB
54+ , ppCondContentLengthRange 1 (10*1024*1024)
55+ -- set content type as jpg image
56+ , ppCondContentType "image/jpeg"
57+ -- on success set the server response code to 200
58+ , ppCondSuccessActionStatus 200
59+ ]
5660
5761 case policyE of
58− Left err -> print err
62+ Left err -> putStrLn $ show err
5963 Right policy -> do
6064 res <- runMinio minioPlayCI $ do
6165 (url, formData) <- presignedPostPolicy policy
6266
6367 -- a curl command is output to demonstrate using the generated
6468 -- URL and form-data
65− let formFn (k, v) =
66− B.concat
67− [ "-F ",
68− Enc.encodeUtf8 k,
69− "=",
70− "'",
71− v,
72− "'"
73− ]
74− formOptions = B.intercalate " " $ map formFn $ H.toList formData
69+ let
70+ formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
71+ "'", v, "'"]
72+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
73+
7574
76− return $
77− B.intercalate
78− " "
79− ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
75+ return $ B.intercalate " " $
76+ ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
8077
8178 case res of
82− Left e -> putStrLn $ "post-policy error: " ++ show e
79+ Left e -> putStrLn $ "post-policy error: " ++ (show e)
8380 Right cmd -> do
84− putStrLn "Put a photo at /tmp/photo.jpg and run command:\n"
81+ putStrLn $ "Put a photo at /tmp/photo.jpg and run command:\n"
8582
8683 -- print the generated curl command
8784 Char8.putStrLn cmd
examples/PresignedPutObject.hs+21−20
@@ -16,43 +16,44 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
2123import qualified Data.ByteString.Char8 as B
22−import Data.CaseInsensitive (original)
23−import Network.Minio
24+import Data.CaseInsensitive (original)
2425
2526-- | The following example uses minio's play server at
2627-- https://play.min.io. The endpoint and associated
2728-- credentials are provided via the libary constant,
2829--
2930-- > minioPlayCI :: ConnectInfo
31+--
32+
3033main :: IO ()
3134main = do
32− let -- Use headers to set user-metadata - note that this header will
33− -- need to be set when the URL is used to make an upload.
34− headers =
35− [ ( "x-amz-meta-url-creator",
36− "minio-hs-presigned-put-example"
37− )
38− ]
35+ let
36+ -- Use headers to set user-metadata - note that this header will
37+ -- need to be set when the URL is used to make an upload.
38+ headers = [("x-amz-meta-url-creator",
39+ "minio-hs-presigned-put-example")]
3940 res <- runMinio minioPlayCI $ do
41+
4042 -- generate a URL with 7 days expiry time
41− presignedPutObjectUrl "my-bucket" "my-object" (7 * 24 * 3600) headers
43+ presignedPutObjectUrl "my-bucket" "my-object" (7*24*3600) headers
4244
4345 case res of
4446 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
4547 Right url -> do
48+
4649 -- We generate a curl command to demonstrate usage of the signed
4750 -- URL.
48− let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
49− curlCmd =
50− B.intercalate " " $
51− ["curl "]
52− ++ map hdrOpt headers
53− ++ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
54−
55− putStrLn $
56− "The following curl command would use the presigned "
57− ++ "URL to upload the file at \"/tmp/myfile\":"
51+ let
52+ hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
53+ curlCmd = B.intercalate " " $
54+ ["curl "] ++ map hdrOpt headers ++
55+ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
56+
57+ putStrLn $ "The following curl command would use the presigned " ++
58+ "URL to upload the file at \"/tmp/myfile\":"
5859 B.putStrLn curlCmd
examples/PutObject.hs+14−11
@@ -16,36 +16,39 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
2123import qualified Data.Conduit.Combinators as CC
22−import Network.Minio
23−import Prelude
24+
25+import Prelude
2426
2527-- | The following example uses minio's play server at
2628-- https://play.min.io. The endpoint and associated
2729-- credentials are provided via the libary constant,
2830--
2931-- > minioPlayCI :: ConnectInfo
32+--
33+
3034main :: IO ()
3135main = do
32− let bucket = "test"
36+ let
37+ bucket = "test"
3338 object = "obj"
3439 localFile = "/etc/lsb-release"
3540 kb15 = 15 * 1024
3641
3742 -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
38− res1 <-
39− runMinio minioPlayCI $
40− putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
43+ res1 <- runMinio minioPlayCI $
44+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
4145 case res1 of
42− Left e -> putStrLn $ "putObject failed." ++ show e
46+ Left e -> putStrLn $ "putObject failed." ++ show e
4347 Right () -> putStrLn "putObject succeeded."
4448
4549 -- Eg 2. Upload a file using fPutObject with default options.
46− res2 <-
47− runMinio minioPlayCI $
48− fPutObject bucket object localFile defaultPutObjectOptions
50+ res2 <- runMinio minioPlayCI $
51+ fPutObject bucket object localFile defaultPutObjectOptions
4952 case res2 of
50− Left e -> putStrLn $ "fPutObject failed." ++ show e
53+ Left e -> putStrLn $ "fPutObject failed." ++ show e
5154 Right () -> putStrLn "fPutObject succeeded."
examples/RemoveBucket.hs+8−3
@@ -16,18 +16,23 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
20+
1921{-# LANGUAGE OverloadedStrings #-}
22+import Network.Minio
2023
21−import Network.Minio
22−import Prelude
24+import Prelude
2325
2426-- | The following example uses minio's play server at
2527-- https://play.min.io. The endpoint and associated
2628-- credentials are provided via the libary constant,
2729--
2830-- > minioPlayCI :: ConnectInfo
31+--
32+
2933main :: IO ()
3034main = do
31− let bucket = "my-bucket"
35+ let
36+ bucket = "my-bucket"
3237 res <- runMinio minioPlayCI $ removeBucket bucket
3338 print res
examples/RemoveIncompleteUpload.hs+10−7
@@ -16,24 +16,27 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
2022
21−import Network.Minio
22−import Prelude
23+import Prelude
2324
2425-- | The following example uses minio's play server at
2526-- https://play.min.io. The endpoint and associated
2627-- credentials are provided via the libary constant,
2728--
2829-- > minioPlayCI :: ConnectInfo
30+--
31+
2932main :: IO ()
3033main = do
31− let bucket = "mybucket"
32− object = "myobject"
34+ let
35+ bucket = "mybucket"
36+ object = "myobject"
3337
34− res <-
35− runMinio minioPlayCI $
36− removeIncompleteUpload bucket object
38+ res <- runMinio minioPlayCI $
39+ removeIncompleteUpload bucket object
3740
3841 case res of
3942 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/RemoveObject.hs+8−7
@@ -16,19 +16,20 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
2021
21−import Network.Minio
22−import Prelude
22+import Network.Minio
23+import Prelude
2324
2425main :: IO ()
2526main = do
26− let bucket = "mybucket"
27− object = "myobject"
27+ let
28+ bucket = "mybucket"
29+ object = "myobject"
2830
29− res <-
30− runMinio minioPlayCI $
31− removeObject bucket object
31+ res <- runMinio minioPlayCI $
32+ removeObject bucket object
3233
3334 case res of
3435 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/SelectObject.hs+26−24
@@ -16,32 +16,34 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+
23+import qualified Conduit as C
24+import Control.Monad (when)
2025
21−import qualified Conduit as C
22−import Control.Monad (unless)
23−import Network.Minio
24−import Prelude
26+import Prelude
2527
2628main :: IO ()
2729main = do
28− let bucket = "selectbucket"
29− object = "1.csv"
30− content =
31− "Name,Place,Temperature\n"
32− <> "James,San Jose,76\n"
33− <> "Alicia,San Leandro,88\n"
34− <> "Mark,San Carlos,90\n"
35−
36− res <- runMinio minioPlayCI $ do
37− exists <- bucketExists bucket
38− unless exists $
39− makeBucket bucket Nothing
40−
41− C.liftIO $ putStrLn "Uploading csv object"
42− putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
43−
44− let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
45− res <- selectObjectContent bucket object sr
46− C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
47− print res
30+ let bucket = "selectbucket"
31+ object = "1.csv"
32+ content = "Name,Place,Temperature\n"
33+ <> "James,San Jose,76\n"
34+ <> "Alicia,San Leandro,88\n"
35+ <> "Mark,San Carlos,90\n"
36+
37+ res <- runMinio minioPlayCI $ do
38+
39+ exists <- bucketExists bucket
40+ when (not exists) $
41+ makeBucket bucket Nothing
42+
43+ C.liftIO $ putStrLn "Uploading csv object"
44+ putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
45+
46+ let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
47+ res <- selectObjectContent bucket object sr
48+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
49+ print res
examples/ServerInfo.hs+7−7
@@ -17,14 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio
28− minioPlayCI
29− getServerInfo
28+ res <- runMinio minioPlayCI $
29+ getServerInfo
3030 print res
examples/ServiceSendRestart.hs+7−6
@@ -17,13 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio minioPlayCI $
28− serviceSendAction ServiceActionRestart
28+ res <- runMinio minioPlayCI $
29+ serviceSendAction ServiceActionRestart
2930 print res
examples/ServiceSendStop.hs+7−6
@@ -17,13 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio minioPlayCI $
28− serviceSendAction ServiceActionStop
28+ res <- runMinio minioPlayCI $
29+ serviceSendAction ServiceActionStop
2930 print res
examples/ServiceStatus.hs+7−7
@@ -17,14 +17,14 @@
1717-- limitations under the License.
1818--
1919
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+
24+import Prelude
2325
2426main :: IO ()
2527main = do
26− res <-
27− runMinio
28− minioPlayCI
29− serviceStatus
28+ res <- runMinio minioPlayCI $
29+ serviceStatus
3030 print res
examples/SetConfig.hs+4−3
@@ -16,11 +16,12 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19+
1920{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+import Network.Minio.AdminAPI
2023
21−import Network.Minio
22−import Network.Minio.AdminAPI
23−import Prelude
24+import Prelude
2425
2526main :: IO ()
2627main = do
minio-hs.cabal+17−73
@@ -1,6 +1,6 @@
1−cabal-version: 2.4
1+cabal-version: 2.2
22name: minio-hs
3−version: 1.7.0
3+version: 1.5.2
44synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
55 storage.
66description: The MinIO Haskell client library provides simple APIs to
@@ -14,70 +14,29 @@ maintainer: dev@min.io
1414category: Network, AWS, Object Storage
1515build-type: Simple
1616stability: Experimental
17−extra-doc-files:
17+extra-source-files:
1818 CHANGELOG.md
1919 CONTRIBUTING.md
2020 docs/API.md
21− README.md
22−extra-source-files:
2321 examples/*.hs
22+ README.md
2423 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
3124
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
4025
4126common base-settings
4227 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−
6728 default-language: Haskell2010
68−
6929 default-extensions: BangPatterns
70− , DerivingStrategies
7130 , FlexibleContexts
7231 , FlexibleInstances
73− , LambdaCase
7432 , MultiParamTypeClasses
7533 , MultiWayIf
34+ , NoImplicitPrelude
7635 , OverloadedStrings
7736 , RankNTypes
7837 , ScopedTypeVariables
38+ , TypeFamilies
7939 , TupleSections
80−
8140 other-modules: Lib.Prelude
8241 , Network.Minio.API
8342 , Network.Minio.APICommon
@@ -95,31 +54,22 @@ common base-settings
9554 , Network.Minio.Utils
9655 , Network.Minio.XmlGenerator
9756 , Network.Minio.XmlParser
98− , Network.Minio.XmlCommon
9957 , 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−
10858 build-depends: base >= 4.7 && < 5
109− , relude >= 0.7 && < 2
110− , aeson >= 1.2 && < 3
59+ , protolude >= 0.2 && < 0.3
60+ , aeson >= 1.2
11161 , base64-bytestring >= 1.0
11262 , binary >= 0.8.5.0
11363 , bytestring >= 0.10
11464 , case-insensitive >= 1.2
11565 , conduit >= 1.3
11666 , conduit-extra >= 1.3
117− , crypton-connection
67+ , connection
11868 , cryptonite >= 0.25
11969 , cryptonite-conduit >= 0.2
120− , data-default-class
12170 , digest >= 0.0.1
12271 , directory
72+ , exceptions
12373 , filepath >= 1.4
12474 , http-client >= 0.5
12575 , http-client-tls
@@ -127,15 +77,14 @@ common base-settings
12777 , http-types >= 0.12
12878 , ini
12979 , memory >= 0.14
130− , network-uri
80+ , raw-strings-qq >= 1
13181 , resourcet >= 1.2
13282 , retry
13383 , text >= 1.2
134− , time >= 1.9
135− , time-units ^>= 1.0.0
84+ , time >= 1.8
13685 , transformers >= 0.5
137− , unliftio >= 0.2 && < 0.3
138− , unliftio-core >= 0.2 && < 0.3
86+ , unliftio >= 0.2
87+ , unliftio-core >= 0.1 && < 0.2
13988 , unordered-containers >= 0.2
14089 , xml-conduit >= 1.8
14190
@@ -166,9 +115,7 @@ test-suite minio-hs-live-server-test
166115 , Network.Minio.Utils.Test
167116 , Network.Minio.XmlGenerator.Test
168117 , Network.Minio.XmlParser.Test
169− , Network.Minio.Credentials
170118 build-depends: minio-hs
171− , raw-strings-qq
172119 , tasty
173120 , tasty-hunit
174121 , tasty-quickcheck
@@ -183,7 +130,6 @@ test-suite minio-hs-test
183130 hs-source-dirs: test, src
184131 main-is: Spec.hs
185132 build-depends: minio-hs
186− , raw-strings-qq
187133 , QuickCheck
188134 , tasty
189135 , tasty-hunit
@@ -200,7 +146,6 @@ test-suite minio-hs-test
200146 , Network.Minio.Utils.Test
201147 , Network.Minio.XmlGenerator.Test
202148 , Network.Minio.XmlParser.Test
203− , Network.Minio.Credentials
204149
205150Flag examples
206151 Description: Build the examples
@@ -347,7 +292,6 @@ executable SetConfig
347292 scope: private
348293 main-is: SetConfig.hs
349294
350−executable AssumeRole
351− import: examples-settings
352− scope: private
353− main-is: AssumeRole.hs
295+source-repository head
296+ type: git
297+ location: https://github.com/minio/minio-hs
src/Lib/Prelude.hs+11−33
@@ -15,41 +15,19 @@
1515--
1616
1717module Lib.Prelude
18− ( module Exports,
19− both,
20− showBS,
21− toStrictBS,
22− fromStrictBS,
23− lastMay,
24− )
25−where
18+ ( module Exports
19+ , both
20+ ) where
2621
27−import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
28−import qualified Data.ByteString.Lazy as LB
29−import Data.Time as Exports
30− ( UTCTime (..),
31− diffUTCTime,
32− )
33−import UnliftIO as Exports
34− ( Handler,
35− catch,
36− catches,
37− throwIO,
38− try,
39− )
22+import Protolude as Exports hiding (catch, catches,
23+ throwIO, try)
24+
25+import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
26+import Data.Time as Exports (UTCTime (..),
27+ diffUTCTime)
28+import UnliftIO as Exports (catch, catches, throwIO,
29+ try)
4030
4131-- | Apply a function on both elements of a pair
4232both :: (a -> b) -> (a, a) -> (b, b)
4333both 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+218−249
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -16,237 +16,224 @@
1616
1717-- |
1818-- Module: Network.Minio
19−-- Copyright: (c) 2017-2023 MinIO Dev Team
19+-- Copyright: (c) 2017-2019 MinIO Dev Team
2020-- License: Apache 2.0
2121-- Maintainer: MinIO Dev Team <dev@min.io>
2222--
2323-- Types and functions to conveniently access S3 compatible object
2424-- storage servers like MinIO.
25+
2526module Network.Minio
26− ( -- * Credentials
27− CredentialValue (..),
28− credentialValueText,
29− AccessKey (..),
30− SecretKey (..),
31− SessionToken (..),
32−
33− -- ** Credential Loaders
34−
35− -- | Run actions that retrieve 'CredentialValue's from the environment or
36− -- files or other custom sources.
37− CredentialLoader,
38− fromAWSConfigFile,
39− fromAWSEnv,
40− fromMinioEnv,
41− findFirst,
42−
43− -- * Connecting to object storage
44− ConnectInfo,
45− setRegion,
46− setCreds,
47− setCredsFrom,
48− isConnectInfoSecure,
49− disableTLSCertValidation,
50− MinioConn,
51− mkMinioConn,
52−
53− -- ** Connection helpers
54−
55− -- | These are helpers to construct 'ConnectInfo' values for common
56− -- cases.
57− minioPlayCI,
58− awsCI,
59− gcsCI,
60−
61− -- ** STS Credential types
62− STSAssumeRole (..),
63− STSAssumeRoleOptions (..),
64− defaultSTSAssumeRoleOptions,
65− requestSTSCredential,
66− setSTSCredential,
67− ExpiryTime (..),
68− STSCredentialProvider,
69−
70− -- * Minio Monad
71−
72− ----------------
73−
74− -- | The Minio Monad provides connection-reuse, bucket-location
75− -- caching, resource management and simpler error handling
76− -- functionality. All actions on object storage are performed within
77− -- this Monad.
78− Minio,
79− runMinioWith,
80− runMinio,
81− runMinioResWith,
82− runMinioRes,
83−
84− -- * Bucket Operations
85−
86− -- ** Creation, removal and querying
87− Bucket,
88− makeBucket,
89− removeBucket,
90− bucketExists,
91− Region,
92− getLocation,
93−
94− -- ** Listing buckets
95− BucketInfo (..),
96− listBuckets,
97−
98− -- ** Listing objects
99− listObjects,
100− listObjectsV1,
101− ListItem (..),
102− ObjectInfo,
103− oiObject,
104− oiModTime,
105− oiETag,
106− oiSize,
107− oiUserMetadata,
108− oiMetadata,
109−
110− -- ** Listing incomplete uploads
111− listIncompleteUploads,
112− UploadId,
113− UploadInfo (..),
114− listIncompleteParts,
115− ObjectPartInfo (..),
116−
117− -- ** Bucket Notifications
118− getBucketNotification,
119− putBucketNotification,
120− removeAllBucketNotification,
121− Notification (..),
122− defaultNotification,
123− NotificationConfig (..),
124− Arn,
125− Event (..),
126− Filter (..),
127− defaultFilter,
128− FilterKey (..),
129− defaultFilterKey,
130− FilterRules (..),
131− defaultFilterRules,
132− FilterRule (..),
133−
134− -- * Object Operations
135− Object,
136−
137− -- ** File-based operations
138− fGetObject,
139− fPutObject,
140−
141− -- ** Conduit-based streaming operations
142− putObject,
143− PutObjectOptions,
144− defaultPutObjectOptions,
145− pooContentType,
146− pooContentEncoding,
147− pooContentDisposition,
148− pooContentLanguage,
149− pooCacheControl,
150− pooStorageClass,
151− pooUserMetadata,
152− pooNumThreads,
153− pooSSE,
154− getObject,
155− GetObjectOptions,
156− defaultGetObjectOptions,
157− gooRange,
158− gooIfMatch,
159− gooIfNoneMatch,
160− gooIfModifiedSince,
161− gooIfUnmodifiedSince,
162− gooSSECKey,
163− GetObjectResponse,
164− gorObjectInfo,
165− gorObjectStream,
166−
167− -- ** Server-side object copying
168− copyObject,
169− SourceInfo,
170− defaultSourceInfo,
171− srcBucket,
172− srcObject,
173− srcRange,
174− srcIfMatch,
175− srcIfNoneMatch,
176− srcIfModifiedSince,
177− srcIfUnmodifiedSince,
178− DestinationInfo,
179− defaultDestinationInfo,
180− dstBucket,
181− dstObject,
182−
183− -- ** Querying object info
184− statObject,
185−
186− -- ** Object removal operations
187− removeObject,
188− removeIncompleteUpload,
189−
190− -- ** Select Object Content with SQL
191− module Network.Minio.SelectAPI,
192−
193− -- * Server-Side Encryption Helpers
194− mkSSECKey,
195− SSECKey,
196− SSE (..),
197−
198− -- * Presigned Operations
199− presignedPutObjectUrl,
200− presignedGetObjectUrl,
201− presignedHeadObjectUrl,
202− UrlExpiry,
203−
204− -- ** POST (browser) upload helpers
205−
206− -- | Please see
207− -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
208− -- for detailed information.
209− newPostPolicy,
210− presignedPostPolicy,
211− showPostPolicy,
212− PostPolicy,
213− PostPolicyError (..),
214−
215− -- *** Post Policy condition helpers
216− PostPolicyCondition,
217− ppCondBucket,
218− ppCondContentLengthRange,
219− ppCondContentType,
220− ppCondKey,
221− ppCondKeyStartsWith,
222− ppCondSuccessActionStatus,
223−
224− -- * Error handling
225−
226− -- | Data types representing various errors that may occur while
227− -- working with an object storage service.
228− MinioErr (..),
229− MErrV (..),
230− ServiceErr (..),
231− )
232−where
27+ (
28+ -- * Credentials
29+ Credentials (..)
30+
31+ -- ** Credential providers
32+ -- | Run actions that retrieve 'Credentials' from the environment or
33+ -- files or other custom sources.
34+ , Provider
35+ , fromAWSConfigFile
36+ , fromAWSEnv
37+ , fromMinioEnv
38+ , findFirst
39+
40+ -- * Connecting to object storage
41+ , ConnectInfo
42+ , setRegion
43+ , setCreds
44+ , setCredsFrom
45+ , isConnectInfoSecure
46+ , disableTLSCertValidation
47+ , MinioConn
48+ , mkMinioConn
49+
50+ -- ** Connection helpers
51+ -- | These are helpers to construct 'ConnectInfo' values for common
52+ -- cases.
53+ , minioPlayCI
54+ , awsCI
55+ , gcsCI
56+
57+ -- * Minio Monad
58+ ----------------
59+ -- | The Minio Monad provides connection-reuse, bucket-location
60+ -- caching, resource management and simpler error handling
61+ -- functionality. All actions on object storage are performed within
62+ -- this Monad.
63+ , Minio
64+ , runMinioWith
65+ , runMinio
66+ , runMinioResWith
67+ , runMinioRes
68+
69+ -- * Bucket Operations
70+
71+ -- ** Creation, removal and querying
72+ , Bucket
73+ , makeBucket
74+ , removeBucket
75+ , bucketExists
76+ , Region
77+ , getLocation
78+
79+ -- ** Listing buckets
80+ , BucketInfo(..)
81+ , listBuckets
82+
83+ -- ** Listing objects
84+ , listObjects
85+ , listObjectsV1
86+ , ListItem(..)
87+
88+ , ObjectInfo
89+ , oiObject
90+ , oiModTime
91+ , oiETag
92+ , oiSize
93+ , oiUserMetadata
94+ , oiMetadata
95+
96+ -- ** Listing incomplete uploads
97+ , listIncompleteUploads
98+ , UploadId
99+ , UploadInfo(..)
100+ , listIncompleteParts
101+ , ObjectPartInfo(..)
102+
103+ -- ** Bucket Notifications
104+ , getBucketNotification
105+ , putBucketNotification
106+ , removeAllBucketNotification
107+ , Notification(..)
108+ , defaultNotification
109+ , NotificationConfig(..)
110+ , Arn
111+ , Event(..)
112+ , Filter(..)
113+ , defaultFilter
114+ , FilterKey(..)
115+ , defaultFilterKey
116+ , FilterRules(..)
117+ , defaultFilterRules
118+ , FilterRule(..)
119+
120+ -- * Object Operations
121+ , Object
122+
123+ -- ** File-based operations
124+ , fGetObject
125+ , fPutObject
126+
127+ -- ** Conduit-based streaming operations
128+ , putObject
129+ , PutObjectOptions
130+ , defaultPutObjectOptions
131+ , pooContentType
132+ , pooContentEncoding
133+ , pooContentDisposition
134+ , pooContentLanguage
135+ , pooCacheControl
136+ , pooStorageClass
137+ , pooUserMetadata
138+ , pooNumThreads
139+ , pooSSE
140+
141+ , getObject
142+ , GetObjectOptions
143+ , defaultGetObjectOptions
144+ , gooRange
145+ , gooIfMatch
146+ , gooIfNoneMatch
147+ , gooIfModifiedSince
148+ , gooIfUnmodifiedSince
149+ , gooSSECKey
150+ , GetObjectResponse
151+ , gorObjectInfo
152+ , gorObjectStream
153+
154+ -- ** Server-side object copying
155+ , copyObject
156+ , SourceInfo
157+ , defaultSourceInfo
158+ , srcBucket
159+ , srcObject
160+ , srcRange
161+ , srcIfMatch
162+ , srcIfNoneMatch
163+ , srcIfModifiedSince
164+ , srcIfUnmodifiedSince
165+ , DestinationInfo
166+ , defaultDestinationInfo
167+ , dstBucket
168+ , dstObject
169+
170+ -- ** Querying object info
171+ , statObject
172+
173+ -- ** Object removal operations
174+ , removeObject
175+ , removeIncompleteUpload
176+
177+ -- ** Select Object Content with SQL
178+ , module Network.Minio.SelectAPI
179+
180+ -- * Server-Side Encryption Helpers
181+ , mkSSECKey
182+ , SSECKey
183+ , SSE(..)
184+
185+ -- * Presigned Operations
186+ , presignedPutObjectUrl
187+ , presignedGetObjectUrl
188+ , presignedHeadObjectUrl
189+ , UrlExpiry
190+
191+ -- ** POST (browser) upload helpers
192+ -- | Please see
193+ -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
194+ -- for detailed information.
195+ , newPostPolicy
196+ , presignedPostPolicy
197+ , showPostPolicy
198+ , PostPolicy
199+ , PostPolicyError(..)
200+
201+ -- *** Post Policy condition helpers
202+ , PostPolicyCondition
203+ , ppCondBucket
204+ , ppCondContentLengthRange
205+ , ppCondContentType
206+ , ppCondKey
207+ , ppCondKeyStartsWith
208+ , ppCondSuccessActionStatus
209+
210+ -- * Error handling
211+ -- | Data types representing various errors that may occur while
212+ -- working with an object storage service.
213+ , MinioErr(..)
214+ , MErrV(..)
215+ , ServiceErr(..)
216+
217+) where
233218
234219{-
235220This module exports the high-level MinIO API for object storage.
236221-}
237222
238−import qualified Data.Conduit as C
239−import qualified Data.Conduit.Binary as CB
223+import qualified Data.Conduit as C
224+import qualified Data.Conduit.Binary as CB
240225import qualified Data.Conduit.Combinators as CC
241−import Network.Minio.API
242−import Network.Minio.CopyObject
243−import Network.Minio.Credentials
244−import Network.Minio.Data
245−import Network.Minio.Errors
246−import Network.Minio.ListOps
247−import Network.Minio.PutObject
248−import Network.Minio.S3API
249−import Network.Minio.SelectAPI
226+
227+import Lib.Prelude
228+
229+import Network.Minio.CopyObject
230+import Network.Minio.Data
231+import Network.Minio.Errors
232+import Network.Minio.ListOps
233+import Network.Minio.PutObject
234+import Network.Minio.S3API
235+import Network.Minio.SelectAPI
236+import Network.Minio.Utils
250237
251238-- | Lists buckets.
252239listBuckets :: Minio [BucketInfo]
@@ -261,12 +248,8 @@ fGetObject bucket object fp opts = do
261248 C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
262249
263250-- | Upload the given file to the given object.
264−fPutObject ::
265− Bucket ->
266− Object ->
267− FilePath ->
268− PutObjectOptions ->
269− Minio ()
251+fPutObject :: Bucket -> Object -> FilePath
252+ -> PutObjectOptions -> Minio ()
270253fPutObject bucket object f opts =
271254 void $ putObjectInternal bucket object opts $ ODFile f Nothing
272255
@@ -274,13 +257,8 @@ fPutObject bucket object f opts =
274257-- known; this helps the library select optimal part sizes to perform
275258-- a multipart upload. If not specified, it is assumed that the object
276259-- can be potentially 5TiB and selects multipart sizes appropriately.
277−putObject ::
278− Bucket ->
279− Object ->
280− C.ConduitM () ByteString Minio () ->
281− Maybe Int64 ->
282− PutObjectOptions ->
283− Minio ()
260+putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio ()
261+ -> Maybe Int64 -> PutObjectOptions -> Minio ()
284262putObject bucket object src sizeMay opts =
285263 void $ putObjectInternal bucket object opts $ ODStream src sizeMay
286264
@@ -290,25 +268,18 @@ putObject bucket object src sizeMay opts =
290268-- copy operation if the new object is to be greater than 5GiB in
291269-- size.
292270copyObject :: DestinationInfo -> SourceInfo -> Minio ()
293−copyObject dstInfo srcInfo =
294− void $
295− copyObjectInternal
296− (dstBucket dstInfo)
297− (dstObject dstInfo)
298− srcInfo
271+copyObject dstInfo srcInfo = void $ copyObjectInternal (dstBucket dstInfo)
272+ (dstObject dstInfo) srcInfo
299273
300274-- | Remove an object from the object store.
301275removeObject :: Bucket -> Object -> Minio ()
302276removeObject = deleteObject
303277
304278-- | Get an object from the object store.
305−getObject ::
306− Bucket ->
307− Object ->
308− GetObjectOptions ->
309− Minio GetObjectResponse
279+getObject :: Bucket -> Object -> GetObjectOptions
280+ -> Minio GetObjectResponse
310281getObject bucket object opts =
311− getObject' bucket object [] $ gooToHeaders opts
282+ getObject' bucket object [] $ gooToHeaders opts
312283
313284-- | Get an object's metadata from the object store. It accepts the
314285-- same options as GetObject.
@@ -338,8 +309,6 @@ bucketExists = headBucket
338309-- | Removes an ongoing multipart upload of an object.
339310removeIncompleteUpload :: Bucket -> Object -> Minio ()
340311removeIncompleteUpload bucket object = do
341− uploads <-
342− C.runConduit $
343− listIncompleteUploads bucket (Just object) False
344− C..| CC.sinkList
312+ uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
313+ C..| CC.sinkList
345314 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs+159−259
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,260 +15,169 @@
1515--
1616
1717module Network.Minio.API
18− ( connect,
19− S3ReqInfo (..),
20− runMinio,
21− executeRequest,
22− buildRequest,
23− mkStreamRequest,
24− getLocation,
25− isValidBucketName,
26− checkBucketNameValidity,
27− isValidObjectName,
28− checkObjectNameValidity,
29− requestSTSCredential,
30− )
31−where
32−
33−import Control.Retry
34− ( fullJitterBackoff,
35− limitRetriesByCumulativeDelay,
36− retrying,
37− )
38−import qualified Data.ByteString as B
39−import qualified Data.Char as C
40−import qualified Data.Conduit as C
41−import qualified Data.HashMap.Strict as H
42−import qualified Data.Text as T
43−import qualified Data.Time.Clock as Time
44−import Lib.Prelude
45−import Network.HTTP.Client (defaultManagerSettings)
46−import qualified Network.HTTP.Client as NClient
47−import Network.HTTP.Conduit (Response)
48−import qualified Network.HTTP.Conduit as NC
49−import Network.HTTP.Types (simpleQueryToQuery)
50−import qualified Network.HTTP.Types as HT
51−import Network.HTTP.Types.Header (hHost)
52−import Network.Minio.APICommon
53−import Network.Minio.Credentials
54−import Network.Minio.Data
55−import Network.Minio.Errors
56−import Network.Minio.Sign.V4
57−import Network.Minio.Utils
58−import Network.Minio.XmlParser
18+ ( connect
19+ , S3ReqInfo(..)
20+ , runMinio
21+ , executeRequest
22+ , mkStreamRequest
23+ , getLocation
24+
25+ , isValidBucketName
26+ , checkBucketNameValidity
27+ , isValidObjectName
28+ , checkObjectNameValidity
29+ ) where
30+
31+import Control.Retry (fullJitterBackoff,
32+ limitRetriesByCumulativeDelay,
33+ retrying)
34+import qualified Data.ByteString as B
35+import qualified Data.Char as C
36+import qualified Data.Conduit as C
37+import qualified Data.HashMap.Strict as H
38+import qualified Data.Text as T
39+import qualified Data.Time.Clock as Time
40+import Network.HTTP.Conduit (Response)
41+import qualified Network.HTTP.Conduit as NC
42+import qualified Network.HTTP.Types as HT
43+import Network.HTTP.Types.Header (hHost)
44+
45+import Lib.Prelude
46+
47+import Network.Minio.APICommon
48+import Network.Minio.Data
49+import Network.Minio.Errors
50+import Network.Minio.Sign.V4
51+import Network.Minio.Utils
52+import Network.Minio.XmlParser
5953
6054-- | Fetch bucket location (region)
6155getLocation :: Bucket -> Minio Region
6256getLocation bucket = do
63− resp <-
64− executeRequest $
65− defaultS3ReqInfo
66− { riBucket = Just bucket,
67− riQueryParams = [("location", Nothing)],
68− riNeedsLocation = False
69− }
57+ resp <- executeRequest $ defaultS3ReqInfo {
58+ riBucket = Just bucket
59+ , riQueryParams = [("location", Nothing)]
60+ , riNeedsLocation = False
61+ }
7062 parseLocation $ NC.responseBody resp
7163
64+
7265-- | Looks for region in RegionMap and updates it using getLocation if
7366-- absent.
7467discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
7568discoverRegion ri = runMaybeT $ do
7669 bucket <- MaybeT $ return $ riBucket ri
7770 regionMay <- lift $ lookupRegionCache bucket
78− maybe
79− ( do
80− l <- lift $ getLocation bucket
81− lift $ addToRegionCache bucket l
82− return l
83− )
84− return
85− regionMay
86−
87−-- | Returns the region to be used for the request.
71+ maybe (do
72+ l <- lift $ getLocation bucket
73+ lift $ addToRegionCache bucket l
74+ return l
75+ ) return regionMay
76+
8877getRegion :: S3ReqInfo -> Minio (Maybe Region)
8978getRegion ri = do
90− ci <- asks mcConnInfo
91−
92− -- getService/makeBucket/getLocation -- don't need location
93− if
94− | not $ riNeedsLocation ri ->
95− return $ Just $ connectRegion ci
96− -- if autodiscovery of location is disabled by user
97− | not $ connectAutoDiscoverRegion ci ->
98− return $ Just $ connectRegion ci
99− -- discover the region for the request
100− | otherwise -> discoverRegion ri
79+ ci <- asks mcConnInfo
80+
81+ -- getService/makeBucket/getLocation -- don't need location
82+ if | not $ riNeedsLocation ri ->
83+ return $ Just $ connectRegion ci
84+
85+ -- if autodiscovery of location is disabled by user
86+ | not $ connectAutoDiscoverRegion ci ->
87+ return $ Just $ connectRegion ci
88+
89+ -- discover the region for the request
90+ | otherwise -> discoverRegion ri
10191
10292getRegionHost :: Region -> Minio Text
10393getRegionHost r = do
104− ci <- asks mcConnInfo
105−
106− if "amazonaws.com" `T.isSuffixOf` connectHost ci
107− then
108− maybe
109− (throwIO $ MErrVRegionNotSupported r)
110− return
111− (H.lookup r awsRegionMap)
112− else return $ connectHost ci
94+ ci <- asks mcConnInfo
11395
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
96+ if "amazonaws.com" `T.isSuffixOf` connectHost ci
97+ then maybe (throwIO $ MErrVRegionNotSupported r)
98+ return (H.lookup r awsRegionMap)
99+ else return $ connectHost ci
163100
164101buildRequest :: S3ReqInfo -> Minio NC.Request
165102buildRequest ri = do
166− maybe (return ()) checkBucketNameValidity $ riBucket ri
167− maybe (return ()) checkObjectNameValidity $ riObject ri
168−
169− ci <- asks mcConnInfo
170−
171− (host, path, regionMay) <- getHostPathRegion ri
172−
173− let ci' = ci {connectHost = host}
174− hostHeader = (hHost, getHostAddr ci')
175− ri' =
176− ri
177− { riHeaders = hostHeader : riHeaders ri,
178− riRegion = regionMay
179− }
180− -- Does not contain body and auth info.
181− baseRequest =
182− NC.defaultRequest
183− { NC.method = riMethod ri',
184− NC.secure = connectIsSecure ci',
185− NC.host = encodeUtf8 $ connectHost ci',
186− NC.port = connectPort ci',
187− NC.path = path,
188− NC.requestHeaders = riHeaders ri',
189− NC.queryString = HT.renderQuery False $ riQueryParams ri'
190− }
191−
192− timeStamp <- liftIO Time.getCurrentTime
103+ maybe (return ()) checkBucketNameValidity $ riBucket ri
104+ maybe (return ()) checkObjectNameValidity $ riObject ri
105+
106+ ci <- asks mcConnInfo
107+
108+ regionMay <- getRegion ri
109+
110+ regionHost <- maybe (return $ connectHost ci) getRegionHost regionMay
111+
112+ let ri' = ri { riHeaders = hostHeader : riHeaders ri
113+ , riRegion = regionMay
114+ }
115+ ci' = ci { connectHost = regionHost }
116+ hostHeader = (hHost, getHostAddr ci')
117+
118+ -- Does not contain body and auth info.
119+ baseRequest = NC.defaultRequest
120+ { NC.method = riMethod ri'
121+ , NC.secure = connectIsSecure ci'
122+ , NC.host = encodeUtf8 $ connectHost ci'
123+ , NC.port = connectPort ci'
124+ , NC.path = getS3Path (riBucket ri') (riObject ri')
125+ , NC.requestHeaders = riHeaders ri'
126+ , NC.queryString = HT.renderQuery False $ riQueryParams ri'
127+ }
128+
129+ timeStamp <- liftIO Time.getCurrentTime
130+
131+ let sp = SignParams (connectAccessKey ci') (connectSecretKey ci')
132+ timeStamp (riRegion ri') Nothing Nothing
133+
134+ -- Cases to handle:
135+ --
136+ -- 1. Connection is secure: use unsigned payload
137+ --
138+ -- 2. Insecure connection, streaming signature is enabled via use of
139+ -- conduit payload: use streaming signature for request.
140+ --
141+ -- 3. Insecure connection, non-conduit payload: compute payload
142+ -- sha256hash, buffer request in memory and perform request.
143+
144+ -- case 2 from above.
145+ if | isStreamingPayload (riPayload ri') &&
146+ (not $ connectIsSecure ci') -> do
147+ (pLen, pSrc) <- case riPayload ri of
148+ PayloadC l src -> return (l, src)
149+ _ -> throwIO MErrVUnexpectedPayload
150+ let reqFn = signV4Stream pLen sp baseRequest
151+ return $ reqFn pSrc
152+
153+ | otherwise -> do
154+ -- case 1 described above.
155+ sp' <- if | connectIsSecure ci' -> return sp
156+ -- case 3 described above.
157+ | otherwise -> do
158+ pHash <- getPayloadSHA256Hash $ riPayload ri'
159+ return $ sp { spPayloadHash = Just pHash }
160+
161+ let signHeaders = signV4 sp' baseRequest
162+ return $ baseRequest
163+ { NC.requestHeaders =
164+ NC.requestHeaders baseRequest ++
165+ mkHeaderFromPairs signHeaders
166+ , NC.requestBody = getRequestBody (riPayload ri')
167+ }
193168
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− }
258169
259170retryAPIRequest :: Minio a -> Minio a
260171retryAPIRequest apiCall = do
261− resE <-
262− retrying retryPolicy (const shouldRetry) $
263− const $
264− try apiCall
172+ resE <- retrying retryPolicy (const shouldRetry) $
173+ const $ try apiCall
265174 either throwIO return resE
266175 where
267176 -- Retry using the full-jitter backoff method for up to 10 mins
268177 -- total
269− retryPolicy =
270− limitRetriesByCumulativeDelay tenMins $
271− fullJitterBackoff oneMilliSecond
178+ retryPolicy = limitRetriesByCumulativeDelay tenMins
179+ $ fullJitterBackoff oneMilliSecond
180+
272181 oneMilliSecond = 1000 -- in microseconds
273182 tenMins = 10 * 60 * 1000000 -- in microseconds
274183 -- retry on connection related failure
@@ -280,23 +189,23 @@ retryAPIRequest apiCall = do
280189 -- API request failed with a retryable exception
281190 Left httpExn@(NC.HttpExceptionRequest _ exn) ->
282191 case (exn :: NC.HttpExceptionContent) of
283− NC.ResponseTimeout -> return True
284− NC.ConnectionTimeout -> return True
192+ NC.ResponseTimeout -> return True
193+ NC.ConnectionTimeout -> return True
285194 NC.ConnectionFailure _ -> return True
286195 -- We received an unexpected exception
287− _ -> throwIO httpExn
196+ _ -> throwIO httpExn
288197 -- We received an unexpected exception
289198 Left someOtherExn -> throwIO someOtherExn
290199
200+
291201executeRequest :: S3ReqInfo -> Minio (Response LByteString)
292202executeRequest ri = do
293203 req <- buildRequest ri
294204 mgr <- asks mcConnManager
295205 retryAPIRequest $ httpLbs req mgr
296206
297−mkStreamRequest ::
298− S3ReqInfo ->
299− Minio (Response (C.ConduitM () ByteString Minio ()))
207+mkStreamRequest :: S3ReqInfo
208+ -> Minio (Response (C.ConduitM () ByteString Minio ()))
300209mkStreamRequest ri = do
301210 req <- buildRequest ri
302211 mgr <- asks mcConnManager
@@ -305,50 +214,41 @@ mkStreamRequest ri = do
305214-- Bucket name validity check according to AWS rules.
306215isValidBucketName :: Bucket -> Bool
307216isValidBucketName bucket =
308− not
309− ( or
310− [ len < 3 || len > 63,
311− any labelCheck labels,
312− any labelCharsCheck labels,
313− isIPCheck
314− ]
315− )
217+ not (or [ len < 3 || len > 63
218+ , or (map labelCheck labels)
219+ , or (map labelCharsCheck labels)
220+ , isIPCheck
221+ ])
316222 where
317223 len = T.length bucket
318224 labels = T.splitOn "." bucket
225+
319226 -- does label `l` fail basic checks of length and start/end?
320227 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
228+
321229 -- does label `l` have non-allowed characters?
322− labelCharsCheck l =
323− isJust $
324− T.find
325− ( \x ->
326− not
327− ( C.isAsciiLower x
328− || x == '-'
329− || C.isDigit x
330− )
331− )
332− l
230+ labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
231+ x == '-' ||
232+ C.isDigit x)) l
233+
333234 -- does label `l` have non-digit characters?
334235 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
335236 labelAsNums = map (not . labelNonDigits) labels
237+
336238 -- check if bucket name looks like an IP
337239 isIPCheck = and labelAsNums && length labelAsNums == 4
338240
339241-- Throws exception iff bucket name is invalid according to AWS rules.
340−checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
242+checkBucketNameValidity :: MonadIO m => Bucket -> m ()
341243checkBucketNameValidity bucket =
342− unless (isValidBucketName bucket) $
343− throwIO $
344− MErrVInvalidBucketName bucket
244+ when (not $ isValidBucketName bucket) $
245+ throwIO $ MErrVInvalidBucketName bucket
345246
346247isValidObjectName :: Object -> Bool
347248isValidObjectName object =
348249 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
349250
350−checkObjectNameValidity :: (MonadIO m) => Object -> m ()
251+checkObjectNameValidity :: MonadIO m => Object -> m ()
351252checkObjectNameValidity object =
352− unless (isValidObjectName object) $
353− throwIO $
354− MErrVInvalidObjectName object
253+ when (not $ isValidObjectName object) $
254+ throwIO $ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hs+24−36
@@ -16,39 +16,37 @@
1616
1717module Network.Minio.APICommon where
1818
19−import qualified Conduit as C
20−import qualified Data.ByteString as BS
21−import qualified Data.ByteString.Lazy as LB
22−import Data.Conduit.Binary (sourceHandleRange)
23−import qualified Data.Text as T
24−import Lib.Prelude
25−import qualified Network.HTTP.Conduit as NC
26−import qualified Network.HTTP.Types as HT
27−import Network.Minio.Data
28−import Network.Minio.Data.Crypto
29−import Network.Minio.Errors
19+import qualified Conduit as C
20+import qualified Data.ByteString as BS
21+import qualified Data.ByteString.Lazy as LB
22+import Data.Conduit.Binary (sourceHandleRange)
23+import qualified Network.HTTP.Conduit as NC
24+import qualified Network.HTTP.Types as HT
25+
26+import Lib.Prelude
27+
28+import Network.Minio.Data
29+import Network.Minio.Data.Crypto
30+import Network.Minio.Errors
3031
3132sha256Header :: ByteString -> HT.Header
32−sha256Header = ("x-amz-content-sha256",)
33+sha256Header = ("x-amz-content-sha256", )
3334
3435-- | This function throws an error if the payload is a conduit (as it
3536-- will not be possible to re-read the conduit after it is consumed).
3637getPayloadSHA256Hash :: Payload -> Minio ByteString
3738getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
38−getPayloadSHA256Hash (PayloadH h off size) =
39− hashSHA256FromSource $
40− sourceHandleRange
41− h
42− (return . fromIntegral $ off)
43− (return . fromIntegral $ size)
39+getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
40+ sourceHandleRange h
41+ (return . fromIntegral $ off)
42+ (return . fromIntegral $ size)
4443getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
4544
4645getRequestBody :: Payload -> NC.RequestBody
4746getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
4847getRequestBody (PayloadH h off size) =
49− NC.requestBodySource size $
50− sourceHandleRange
51− h
48+ NC.requestBodySource (fromIntegral size) $
49+ sourceHandleRange h
5250 (return . fromIntegral $ off)
5351 (return . fromIntegral $ size)
5452getRequestBody (PayloadC n src) = NC.requestBodySource n src
@@ -57,24 +55,14 @@ mkStreamingPayload :: Payload -> Payload
5755mkStreamingPayload payload =
5856 case payload of
5957 PayloadBS bs ->
60− PayloadC
61− (fromIntegral $ BS.length bs)
58+ PayloadC (fromIntegral $ BS.length bs)
6259 (C.sourceLazy $ LB.fromStrict bs)
6360 PayloadH h off len ->
64− PayloadC len $
65− sourceHandleRange
66− h
67− (return . fromIntegral $ off)
68− (return . fromIntegral $ len)
61+ PayloadC len $ sourceHandleRange h
62+ (return . fromIntegral $ off)
63+ (return . fromIntegral $ len)
6964 _ -> payload
7065
7166isStreamingPayload :: Payload -> Bool
7267isStreamingPayload (PayloadC _ _) = True
73−isStreamingPayload _ = False
74−
75−-- | Checks if the connect info is for Amazon S3.
76−isAWSConnectInfo :: ConnectInfo -> Bool
77−isAWSConnectInfo ci = ".amazonaws.com" `T.isSuffixOf` connectHost ci
78−
79−bucketHasPeriods :: Bucket -> Bool
80−bucketHasPeriods b = isJust $ T.find (== '.') b
68+isStreamingPayload _ = False
src/Network/Minio/AdminAPI.hs+391−543
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2018-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -16,341 +16,270 @@
1616
1717module Network.Minio.AdminAPI
1818 ( -- * MinIO Admin API
19−
2019 --------------------
21−
2220 -- | Provides MinIO admin API and related types. It is in
2321 -- experimental state.
24− DriveInfo (..),
25− ErasureInfo (..),
26− Backend (..),
27− ConnStats (..),
28− HttpStats (..),
29− ServerProps (..),
30− CountNAvgTime (..),
31− StorageClass (..),
32− StorageInfo (..),
33− SIData (..),
34− ServerInfo (..),
35− getServerInfo,
36− HealOpts (..),
37− HealResultItem (..),
38− HealStatus (..),
39− HealStartResp (..),
40− startHeal,
41− forceStartHeal,
42− getHealStatus,
43− SetConfigResult (..),
44− NodeSummary (..),
45− setConfig,
46− getConfig,
47− ServerVersion (..),
48− ServiceStatus (..),
49− serviceStatus,
50− ServiceAction (..),
51− serviceSendAction,
52− )
53−where
54−
55−import Data.Aeson
56− ( FromJSON,
57− ToJSON,
58− Value (Object),
59− eitherDecode,
60− object,
61− pairs,
62− parseJSON,
63− toEncoding,
64− toJSON,
65− withObject,
66− withText,
67− (.:),
68− (.:?),
69− (.=),
70− )
71−import qualified Data.Aeson as A
72−import Data.Aeson.Types (typeMismatch)
73−import qualified Data.ByteString as B
74−import qualified Data.ByteString.Lazy as LBS
75−import qualified Data.Text as T
76−import Data.Time (NominalDiffTime, getCurrentTime)
77−import Lib.Prelude
78−import Network.HTTP.Conduit (Response)
79−import qualified Network.HTTP.Conduit as NC
80−import qualified Network.HTTP.Types as HT
81−import Network.HTTP.Types.Header (hHost)
82−import Network.Minio.APICommon
83−import Network.Minio.Credentials
84−import Network.Minio.Data
85−import Network.Minio.Errors
86−import Network.Minio.Sign.V4
87−import Network.Minio.Utils
22+ DriveInfo(..)
23+ , ErasureInfo(..)
24+ , Backend(..)
25+ , ConnStats(..)
26+ , HttpStats(..)
27+ , ServerProps(..)
28+ , CountNAvgTime(..)
29+ , StorageClass(..)
30+ , StorageInfo(..)
31+ , SIData(..)
32+ , ServerInfo(..)
33+ , getServerInfo
34+
35+ , HealOpts(..)
36+ , HealResultItem(..)
37+ , HealStatus(..)
38+ , HealStartResp(..)
39+ , startHeal
40+ , forceStartHeal
41+ , getHealStatus
42+
43+ , SetConfigResult(..)
44+ , NodeSummary(..)
45+ , setConfig
46+ , getConfig
47+
48+ , ServerVersion(..)
49+ , ServiceStatus(..)
50+ , serviceStatus
51+
52+ , ServiceAction(..)
53+ , serviceSendAction
54+ ) where
55+
56+import Data.Aeson (FromJSON, ToJSON, Value (Object),
57+ eitherDecode, object, pairs,
58+ parseJSON, toEncoding, toJSON,
59+ withObject, withText, (.:), (.:?),
60+ (.=))
61+import qualified Data.Aeson as A
62+import Data.Aeson.Types (typeMismatch)
63+import qualified Data.ByteString as B
64+import qualified Data.ByteString.Lazy as LBS
65+import qualified Data.Text as T
66+import Data.Time (NominalDiffTime, getCurrentTime)
67+import Network.HTTP.Conduit (Response)
68+import qualified Network.HTTP.Conduit as NC
69+import qualified Network.HTTP.Types as HT
70+import Network.HTTP.Types.Header (hHost)
71+
72+import Lib.Prelude
73+
74+import Network.Minio.APICommon
75+import Network.Minio.Data
76+import Network.Minio.Errors
77+import Network.Minio.Sign.V4
78+import Network.Minio.Utils
8879
8980data DriveInfo = DriveInfo
90− { diUuid :: Text,
91− diEndpoint :: Text,
92− diState :: Text
93− }
94− deriving stock (Show, Eq)
81+ { diUuid :: Text
82+ , diEndpoint :: Text
83+ , diState :: Text
84+ } deriving (Eq, Show)
9585
9686instance FromJSON DriveInfo where
97− parseJSON = withObject "DriveInfo" $ \v ->
98− DriveInfo
99− <$> v
100− .: "uuid"
101− <*> v
102− .: "endpoint"
103− <*> v
104− .: "state"
87+ parseJSON = withObject "DriveInfo" $ \v -> DriveInfo
88+ <$> v .: "uuid"
89+ <*> v .: "endpoint"
90+ <*> v .: "state"
10591
10692data StorageClass = StorageClass
107− { scParity :: Int,
108− scData :: Int
109− }
110− deriving stock (Show, Eq)
93+ { scParity :: Int
94+ , scData :: Int
95+ } deriving (Eq, Show)
11196
11297data ErasureInfo = ErasureInfo
113− { eiOnlineDisks :: Int,
114− eiOfflineDisks :: Int,
115− eiStandard :: StorageClass,
116− eiReducedRedundancy :: StorageClass,
117− eiSets :: [[DriveInfo]]
118− }
119− deriving stock (Show, Eq)
98+ { eiOnlineDisks :: Int
99+ , eiOfflineDisks :: Int
100+ , eiStandard :: StorageClass
101+ , eiReducedRedundancy :: StorageClass
102+ , eiSets :: [[DriveInfo]]
103+ } deriving (Eq, Show)
120104
121105instance FromJSON ErasureInfo where
122− parseJSON = withObject "ErasureInfo" $ \v -> do
123− onlineDisks <- v .: "OnlineDisks"
124− offlineDisks <- v .: "OfflineDisks"
125− stdClass <-
126− StorageClass
127− <$> v
128− .: "StandardSCData"
129− <*> v
130− .: "StandardSCParity"
131− rrClass <-
132− StorageClass
133− <$> v
134− .: "RRSCData"
135− <*> v
136− .: "RRSCParity"
137− sets <- v .: "Sets"
138− return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
139−
140−data Backend
141− = BackendFS
142− | BackendErasure ErasureInfo
143− deriving stock (Show, Eq)
106+ parseJSON = withObject "ErasureInfo" $ \v -> do
107+ onlineDisks <- v .: "OnlineDisks"
108+ offlineDisks <- v .: "OfflineDisks"
109+ stdClass <- StorageClass
110+ <$> v .: "StandardSCData"
111+ <*> v .: "StandardSCParity"
112+ rrClass <- StorageClass
113+ <$> v .: "RRSCData"
114+ <*> v .: "RRSCParity"
115+ sets <- v .: "Sets"
116+ return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
117+
118+data Backend = BackendFS
119+ | BackendErasure ErasureInfo
120+ deriving (Eq, Show)
144121
145122instance FromJSON Backend where
146− parseJSON = withObject "Backend" $ \v -> do
147− typ <- v .: "Type"
148− case typ :: Int of
149− 1 -> return BackendFS
150− 2 -> BackendErasure <$> parseJSON (Object v)
151− _ -> typeMismatch "BackendType" (Object v)
123+ parseJSON = withObject "Backend" $ \v -> do
124+ typ <- v .: "Type"
125+ case typ :: Int of
126+ 1 -> return BackendFS
127+ 2 -> BackendErasure <$> parseJSON (Object v)
128+ _ -> typeMismatch "BackendType" (Object v)
152129
153130data ConnStats = ConnStats
154− { csTransferred :: Int64,
155− csReceived :: Int64
156− }
157− deriving stock (Show, Eq)
131+ { csTransferred :: Int64
132+ , csReceived :: Int64
133+ } deriving (Eq, Show)
158134
159135instance FromJSON ConnStats where
160− parseJSON = withObject "ConnStats" $ \v ->
161− ConnStats
162− <$> v
163− .: "transferred"
164− <*> v
165− .: "received"
136+ parseJSON = withObject "ConnStats" $ \v -> ConnStats
137+ <$> v .: "transferred"
138+ <*> v .: "received"
166139
167140data ServerProps = ServerProps
168− { spUptime :: NominalDiffTime,
169− spVersion :: Text,
170− spCommitId :: Text,
171− spRegion :: Text,
172− spSqsArns :: [Text]
173− }
174− deriving stock (Show, Eq)
141+ { spUptime :: NominalDiffTime
142+ , spVersion :: Text
143+ , spCommitId :: Text
144+ , spRegion :: Text
145+ , spSqsArns :: [Text]
146+ } deriving (Eq, Show)
175147
176148instance FromJSON ServerProps where
177− parseJSON = withObject "SIServer" $ \v -> do
178− uptimeNs <- v .: "uptime"
179− let uptime = uptimeNs / 1e9
180− ver <- v .: "version"
181− commitId <- v .: "commitID"
182− region <- v .: "region"
183− arn <- v .: "sqsARN"
184− return $ ServerProps uptime ver commitId region arn
149+ parseJSON = withObject "SIServer" $ \v -> do
150+ uptimeNs <- v .: "uptime"
151+ let uptime = uptimeNs / 1e9
152+ ver <- v .: "version"
153+ commitId <- v .: "commitID"
154+ region <- v .: "region"
155+ arn <- v .: "sqsARN"
156+ return $ ServerProps uptime ver commitId region arn
185157
186158data StorageInfo = StorageInfo
187− { siUsed :: Int64,
188− siBackend :: Backend
189− }
190− deriving stock (Show, Eq)
159+ { siUsed :: Int64
160+ , siBackend :: Backend
161+ } deriving (Eq, Show)
191162
192163instance FromJSON StorageInfo where
193− parseJSON = withObject "StorageInfo" $ \v ->
194− StorageInfo
195− <$> v
196− .: "Used"
197− <*> v
198− .: "Backend"
164+ parseJSON = withObject "StorageInfo" $ \v -> StorageInfo
165+ <$> v .: "Used"
166+ <*> v .: "Backend"
199167
200168data CountNAvgTime = CountNAvgTime
201− { caCount :: Int64,
202− caAvgDuration :: Text
203− }
204− deriving stock (Show, Eq)
169+ { caCount :: Int64
170+ , caAvgDuration :: Text
171+ } deriving (Eq, Show)
205172
206173instance FromJSON CountNAvgTime where
207− parseJSON = withObject "CountNAvgTime" $ \v ->
208− CountNAvgTime
209− <$> v
210− .: "count"
211− <*> v
212− .: "avgDuration"
174+ parseJSON = withObject "CountNAvgTime" $ \v -> CountNAvgTime
175+ <$> v .: "count"
176+ <*> v .: "avgDuration"
213177
214178data HttpStats = HttpStats
215− { hsTotalHeads :: CountNAvgTime,
216− hsSuccessHeads :: CountNAvgTime,
217− hsTotalGets :: CountNAvgTime,
218− hsSuccessGets :: CountNAvgTime,
219− hsTotalPuts :: CountNAvgTime,
220− hsSuccessPuts :: CountNAvgTime,
221− hsTotalPosts :: CountNAvgTime,
222− hsSuccessPosts :: CountNAvgTime,
223− hsTotalDeletes :: CountNAvgTime,
224− hsSuccessDeletes :: CountNAvgTime
225− }
226− deriving stock (Show, Eq)
179+ { hsTotalHeads :: CountNAvgTime
180+ , hsSuccessHeads :: CountNAvgTime
181+ , hsTotalGets :: CountNAvgTime
182+ , hsSuccessGets :: CountNAvgTime
183+ , hsTotalPuts :: CountNAvgTime
184+ , hsSuccessPuts :: CountNAvgTime
185+ , hsTotalPosts :: CountNAvgTime
186+ , hsSuccessPosts :: CountNAvgTime
187+ , hsTotalDeletes :: CountNAvgTime
188+ , hsSuccessDeletes :: CountNAvgTime
189+ } deriving (Eq, Show)
227190
228191instance FromJSON HttpStats where
229− parseJSON = withObject "HttpStats" $ \v ->
230− HttpStats
231− <$> v
232− .: "totalHEADs"
233− <*> v
234− .: "successHEADs"
235− <*> v
236− .: "totalGETs"
237− <*> v
238− .: "successGETs"
239− <*> v
240− .: "totalPUTs"
241− <*> v
242− .: "successPUTs"
243− <*> v
244− .: "totalPOSTs"
245− <*> v
246− .: "successPOSTs"
247− <*> v
248− .: "totalDELETEs"
249− <*> v
250− .: "successDELETEs"
192+ parseJSON = withObject "HttpStats" $ \v -> HttpStats
193+ <$> v .: "totalHEADs"
194+ <*> v .: "successHEADs"
195+ <*> v .: "totalGETs"
196+ <*> v .: "successGETs"
197+ <*> v .: "totalPUTs"
198+ <*> v .: "successPUTs"
199+ <*> v .: "totalPOSTs"
200+ <*> v .: "successPOSTs"
201+ <*> v .: "totalDELETEs"
202+ <*> v .: "successDELETEs"
251203
252204data SIData = SIData
253− { sdStorage :: StorageInfo,
254− sdConnStats :: ConnStats,
255− sdHttpStats :: HttpStats,
256− sdProps :: ServerProps
257− }
258− deriving stock (Show, Eq)
205+ { sdStorage :: StorageInfo
206+ , sdConnStats :: ConnStats
207+ , sdHttpStats :: HttpStats
208+ , sdProps :: ServerProps
209+ } deriving (Eq, Show)
259210
260211instance FromJSON SIData where
261− parseJSON = withObject "SIData" $ \v ->
262− SIData
263− <$> v
264− .: "storage"
265− <*> v
266− .: "network"
267− <*> v
268− .: "http"
269− <*> v
270− .: "server"
212+ parseJSON = withObject "SIData" $ \v -> SIData
213+ <$> v .: "storage"
214+ <*> v .: "network"
215+ <*> v .: "http"
216+ <*> v .: "server"
271217
272218data ServerInfo = ServerInfo
273− { siError :: Text,
274− siAddr :: Text,
275− siData :: SIData
276− }
277− deriving stock (Show, Eq)
219+ { siError :: Text
220+ , siAddr :: Text
221+ , siData :: SIData
222+ } deriving (Eq, Show)
278223
279224instance FromJSON ServerInfo where
280− parseJSON = withObject "ServerInfo" $ \v ->
281− ServerInfo
282− <$> v
283− .: "error"
284− <*> v
285− .: "addr"
286− <*> v
287− .: "data"
225+ parseJSON = withObject "ServerInfo" $ \v -> ServerInfo
226+ <$> v .: "error"
227+ <*> v .: "addr"
228+ <*> v .: "data"
288229
289230data ServerVersion = ServerVersion
290− { svVersion :: Text,
291− svCommitId :: Text
292− }
293− deriving stock (Show, Eq)
231+ { svVersion :: Text
232+ , svCommitId :: Text
233+ } deriving (Eq, Show)
294234
295235instance FromJSON ServerVersion where
296− parseJSON = withObject "ServerVersion" $ \v ->
297− ServerVersion
298− <$> v
299− .: "version"
300− <*> v
301− .: "commitID"
236+ parseJSON = withObject "ServerVersion" $ \v -> ServerVersion
237+ <$> v .: "version"
238+ <*> v .: "commitID"
302239
303240data ServiceStatus = ServiceStatus
304− { ssVersion :: ServerVersion,
305− ssUptime :: NominalDiffTime
306− }
307− deriving stock (Show, Eq)
241+ { ssVersion :: ServerVersion
242+ , ssUptime :: NominalDiffTime
243+ } deriving (Eq, Show)
308244
309245instance FromJSON ServiceStatus where
310− parseJSON = withObject "ServiceStatus" $ \v -> do
311− serverVersion <- v .: "serverVersion"
312− uptimeNs <- v .: "uptime"
313− let uptime = uptimeNs / 1e9
314− return $ ServiceStatus serverVersion uptime
246+ parseJSON = withObject "ServiceStatus" $ \v -> do
247+ serverVersion <- v .: "serverVersion"
248+ uptimeNs <- v .: "uptime"
249+ let uptime = uptimeNs / 1e9
250+ return $ ServiceStatus serverVersion uptime
315251
316−data ServiceAction
317− = ServiceActionRestart
318− | ServiceActionStop
319− deriving stock (Show, Eq)
252+data ServiceAction = ServiceActionRestart
253+ | ServiceActionStop
254+ deriving (Eq, Show)
320255
321256instance ToJSON ServiceAction where
322− toJSON a = object ["action" .= serviceActionToText a]
257+ toJSON a = object [ "action" .= serviceActionToText a ]
323258
324259serviceActionToText :: ServiceAction -> Text
325260serviceActionToText a = case a of
326261 ServiceActionRestart -> "restart"
327− ServiceActionStop -> "stop"
262+ ServiceActionStop -> "stop"
328263
329264adminPath :: ByteString
330265adminPath = "/minio/admin"
331266
332267data HealStartResp = HealStartResp
333− { hsrClientToken :: Text,
334− hsrClientAddr :: Text,
335− hsrStartTime :: UTCTime
336− }
337− deriving stock (Show, Eq)
268+ { hsrClientToken :: Text
269+ , hsrClientAddr :: Text
270+ , hsrStartTime :: UTCTime
271+ } deriving (Eq, Show)
338272
339273instance FromJSON HealStartResp where
340− parseJSON = withObject "HealStartResp" $ \v ->
341− HealStartResp
342− <$> v
343− .: "clientToken"
344− <*> v
345− .: "clientAddress"
346− <*> v
347− .: "startTime"
274+ parseJSON = withObject "HealStartResp" $ \v -> HealStartResp
275+ <$> v .: "clientToken"
276+ <*> v .: "clientAddress"
277+ <*> v .: "startTime"
348278
349279data HealOpts = HealOpts
350− { hoRecursive :: Bool,
351− hoDryRun :: Bool
352− }
353− deriving stock (Show, Eq)
280+ { hoRecursive :: Bool
281+ , hoDryRun :: Bool
282+ } deriving (Eq, Show)
354283
355284instance ToJSON HealOpts where
356285 toJSON (HealOpts r d) =
@@ -359,259 +288,197 @@ instance ToJSON HealOpts where
359288 pairs ("recursive" .= r <> "dryRun" .= d)
360289
361290instance FromJSON HealOpts where
362− parseJSON = withObject "HealOpts" $ \v ->
363− HealOpts
364− <$> v
365− .: "recursive"
366− <*> v
367− .: "dryRun"
368−
369−data HealItemType
370− = HealItemMetadata
371− | HealItemBucket
372− | HealItemBucketMetadata
373− | HealItemObject
374− deriving stock (Show, Eq)
291+ parseJSON = withObject "HealOpts" $ \v -> HealOpts
292+ <$> v .: "recursive"
293+ <*> v .: "dryRun"
294+
295+data HealItemType = HealItemMetadata
296+ | HealItemBucket
297+ | HealItemBucketMetadata
298+ | HealItemObject
299+ deriving (Eq, Show)
375300
376301instance FromJSON HealItemType where
377− parseJSON = withText "HealItemType" $ \v -> case v of
378− "metadata" -> return HealItemMetadata
379− "bucket" -> return HealItemBucket
380− "object" -> return HealItemObject
381− "bucket-metadata" -> return HealItemBucketMetadata
382− _ -> typeMismatch "HealItemType" (A.String v)
302+ parseJSON = withText "HealItemType" $ \v -> case v of
303+ "metadata" -> return HealItemMetadata
304+ "bucket" -> return HealItemBucket
305+ "object" -> return HealItemObject
306+ "bucket-metadata" -> return HealItemBucketMetadata
307+ _ -> typeMismatch "HealItemType" (A.String v)
383308
384309data NodeSummary = NodeSummary
385− { nsName :: Text,
386− nsErrSet :: Bool,
387− nsErrMessage :: Text
388− }
389− deriving stock (Show, Eq)
310+ { nsName :: Text
311+ , nsErrSet :: Bool
312+ , nsErrMessage :: Text
313+ } deriving (Eq, Show)
390314
391315instance FromJSON NodeSummary where
392− parseJSON = withObject "NodeSummary" $ \v ->
393− NodeSummary
394− <$> v
395− .: "name"
396− <*> v
397− .: "errSet"
398− <*> v
399− .: "errMsg"
316+ parseJSON = withObject "NodeSummary" $ \v -> NodeSummary
317+ <$> v .: "name"
318+ <*> v .: "errSet"
319+ <*> v .: "errMsg"
400320
401321data SetConfigResult = SetConfigResult
402− { scrStatus :: Bool,
403− scrNodeSummary :: [NodeSummary]
404− }
405− deriving stock (Show, Eq)
322+ { scrStatus :: Bool
323+ , scrNodeSummary :: [NodeSummary]
324+ } deriving (Eq, Show)
406325
407326instance FromJSON SetConfigResult where
408− parseJSON = withObject "SetConfigResult" $ \v ->
409− SetConfigResult
410− <$> v
411− .: "status"
412− <*> v
413− .: "nodeResults"
327+ parseJSON = withObject "SetConfigResult" $ \v -> SetConfigResult
328+ <$> v .: "status"
329+ <*> v .: "nodeResults"
414330
415331data HealResultItem = HealResultItem
416− { hriResultIdx :: Int,
417− hriType :: HealItemType,
418− hriBucket :: Bucket,
419− hriObject :: Object,
420− hriDetail :: Text,
421− hriParityBlocks :: Maybe Int,
422− hriDataBlocks :: Maybe Int,
423− hriDiskCount :: Int,
424− hriSetCount :: Int,
425− hriObjectSize :: Int,
426− hriBefore :: [DriveInfo],
427− hriAfter :: [DriveInfo]
428− }
429− deriving stock (Show, Eq)
332+ { hriResultIdx :: Int
333+ , hriType :: HealItemType
334+ , hriBucket :: Bucket
335+ , hriObject :: Object
336+ , hriDetail :: Text
337+ , hriParityBlocks :: Maybe Int
338+ , hriDataBlocks :: Maybe Int
339+ , hriDiskCount :: Int
340+ , hriSetCount :: Int
341+ , hriObjectSize :: Int
342+ , hriBefore :: [DriveInfo]
343+ , hriAfter :: [DriveInfo]
344+ } deriving (Eq, Show)
430345
431346instance FromJSON HealResultItem where
432− parseJSON = withObject "HealResultItem" $ \v ->
433− HealResultItem
434− <$> v
435− .: "resultId"
436− <*> v
437− .: "type"
438− <*> v
439− .: "bucket"
440− <*> v
441− .: "object"
442− <*> v
443− .: "detail"
444− <*> v
445− .:? "parityBlocks"
446− <*> v
447− .:? "dataBlocks"
448− <*> v
449− .: "diskCount"
450− <*> v
451− .: "setCount"
452− <*> v
453− .: "objectSize"
454− <*> ( do
455− before <- v .: "before"
456− before .: "drives"
457− )
458− <*> ( do
459− after <- v .: "after"
460− after .: "drives"
461− )
347+ parseJSON = withObject "HealResultItem" $ \v -> HealResultItem
348+ <$> v .: "resultId"
349+ <*> v .: "type"
350+ <*> v .: "bucket"
351+ <*> v .: "object"
352+ <*> v .: "detail"
353+ <*> v .:? "parityBlocks"
354+ <*> v .:? "dataBlocks"
355+ <*> v .: "diskCount"
356+ <*> v .: "setCount"
357+ <*> v .: "objectSize"
358+ <*> (do before <- v .: "before"
359+ before .: "drives")
360+ <*> (do after <- v .: "after"
361+ after .: "drives")
462362
463363data HealStatus = HealStatus
464− { hsSummary :: Text,
465− hsStartTime :: UTCTime,
466− hsSettings :: HealOpts,
467− hsNumDisks :: Int,
468− hsFailureDetail :: Maybe Text,
469− hsItems :: Maybe [HealResultItem]
470− }
471− deriving stock (Show, Eq)
364+ { hsSummary :: Text
365+ , hsStartTime :: UTCTime
366+ , hsSettings :: HealOpts
367+ , hsNumDisks :: Int
368+ , hsFailureDetail :: Maybe Text
369+ , hsItems :: Maybe [HealResultItem]
370+ } deriving (Eq, Show)
472371
473372instance FromJSON HealStatus where
474− parseJSON = withObject "HealStatus" $ \v ->
475− HealStatus
476− <$> v
477− .: "Summary"
478− <*> v
479− .: "StartTime"
480− <*> v
481− .: "Settings"
482− <*> v
483− .: "NumDisks"
484− <*> v
485− .:? "Detail"
486− <*> v
487− .: "Items"
373+ parseJSON = withObject "HealStatus" $ \v -> HealStatus
374+ <$> v .: "Summary"
375+ <*> v .: "StartTime"
376+ <*> v .: "Settings"
377+ <*> v .: "NumDisks"
378+ <*> v .:? "Detail"
379+ <*> v .: "Items"
488380
489381healPath :: Maybe Bucket -> Maybe Text -> ByteString
490382healPath bucket prefix = do
491− if isJust bucket
492− then
493− encodeUtf8 $
494− "v1/heal/"
495− <> fromMaybe "" bucket
496− <> "/"
497− <> fromMaybe "" prefix
498− else encodeUtf8 ("v1/heal/" :: Text)
383+ if (isJust bucket)
384+ then encodeUtf8 $ "v1/heal/" <> fromMaybe "" bucket <> "/"
385+ <> fromMaybe "" prefix
386+ else encodeUtf8 $ "v1/heal/"
499387
500388-- | Get server version and uptime.
501389serviceStatus :: Minio ServiceStatus
502390serviceStatus = do
503− rsp <-
504− executeAdminRequest
505− AdminReqInfo
506− { ariMethod = HT.methodGet,
507− ariPayload = PayloadBS B.empty,
508− ariPayloadHash = Nothing,
509− ariPath = "v1/service",
510− ariHeaders = [],
511− ariQueryParams = []
512− }
513−
514− let rspBS = NC.responseBody rsp
515− case eitherDecode rspBS of
516− Right ss -> return ss
517− Left err -> throwIO $ MErrVJsonParse $ T.pack err
391+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
392+ , ariPayload = PayloadBS B.empty
393+ , ariPayloadHash = Nothing
394+ , ariPath = "v1/service"
395+ , ariHeaders = []
396+ , ariQueryParams = []
397+ }
398+
399+ let rspBS = NC.responseBody rsp
400+ case eitherDecode rspBS of
401+ Right ss -> return ss
402+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
518403
519404-- | Send service restart or stop action to MinIO server.
520405serviceSendAction :: ServiceAction -> Minio ()
521406serviceSendAction action = do
522− let payload = PayloadBS $ LBS.toStrict $ A.encode action
523− void $
524− executeAdminRequest
525− AdminReqInfo
526− { ariMethod = HT.methodPost,
527− ariPayload = payload,
528− ariPayloadHash = Nothing,
529− ariPath = "v1/service",
530− ariHeaders = [],
531− ariQueryParams = []
532− }
407+ let payload = PayloadBS $ LBS.toStrict $ A.encode action
408+ void $ executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
409+ , ariPayload = payload
410+ , ariPayloadHash = Nothing
411+ , ariPath = "v1/service"
412+ , ariHeaders = []
413+ , ariQueryParams = []
414+ }
533415
534416-- | Get the current config file from server.
535417getConfig :: Minio ByteString
536418getConfig = do
537− rsp <-
538− executeAdminRequest
539− AdminReqInfo
540− { ariMethod = HT.methodGet,
541− ariPayload = PayloadBS B.empty,
542− ariPayloadHash = Nothing,
543− ariPath = "v1/config",
544− ariHeaders = [],
545− ariQueryParams = []
546− }
547− return $ LBS.toStrict $ NC.responseBody rsp
419+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
420+ , ariPayload = PayloadBS B.empty
421+ , ariPayloadHash = Nothing
422+ , ariPath = "v1/config"
423+ , ariHeaders = []
424+ , ariQueryParams = []
425+ }
426+ return $ LBS.toStrict $ NC.responseBody rsp
548427
549428-- | Set a new config to the server.
550429setConfig :: ByteString -> Minio SetConfigResult
551430setConfig config = do
552− rsp <-
553− executeAdminRequest
554− AdminReqInfo
555− { ariMethod = HT.methodPut,
556− ariPayload = PayloadBS config,
557− ariPayloadHash = Nothing,
558− ariPath = "v1/config",
559− ariHeaders = [],
560− ariQueryParams = []
561− }
562−
563− let rspBS = NC.responseBody rsp
564− case eitherDecode rspBS of
565− Right scr -> return scr
566− Left err -> throwIO $ MErrVJsonParse $ T.pack err
431+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPut
432+ , ariPayload = PayloadBS config
433+ , ariPayloadHash = Nothing
434+ , ariPath = "v1/config"
435+ , ariHeaders = []
436+ , ariQueryParams = []
437+ }
438+
439+ let rspBS = NC.responseBody rsp
440+ case eitherDecode rspBS of
441+ Right scr -> return scr
442+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
567443
568444-- | Get the progress of currently running heal task, this API should be
569445-- invoked right after `startHeal`. `token` is obtained after `startHeal`
570446-- which should be used to get the heal status.
571447getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
572448getHealStatus bucket prefix token = do
573− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
574− let qparams = HT.queryTextToQuery [("clientToken", Just token)]
575− rsp <-
576− executeAdminRequest
577− AdminReqInfo
578− { ariMethod = HT.methodPost,
579− ariPayload = PayloadBS B.empty,
580− ariPayloadHash = Nothing,
581− ariPath = healPath bucket prefix,
582− ariHeaders = [],
583− ariQueryParams = qparams
584− }
585− let rspBS = NC.responseBody rsp
586− case eitherDecode rspBS of
587− Right hs -> return hs
588− Left err -> throwIO $ MErrVJsonParse $ T.pack err
449+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
450+ let qparams = HT.queryTextToQuery [("clientToken", Just token)]
451+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
452+ , ariPayload = PayloadBS B.empty
453+ , ariPayloadHash = Nothing
454+ , ariPath = healPath bucket prefix
455+ , ariHeaders = []
456+ , ariQueryParams = qparams
457+ }
458+ let rspBS = NC.responseBody rsp
459+ case eitherDecode rspBS of
460+ Right hs -> return hs
461+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
589462
590463doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
591464doHeal bucket prefix opts forceStart = do
592− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
593− let payload = PayloadBS $ LBS.toStrict $ A.encode opts
594− let qparams =
595− bool
596− []
597− (HT.queryTextToQuery [("forceStart", Just "true")])
598− forceStart
599−
600− rsp <-
601− executeAdminRequest
602− AdminReqInfo
603− { ariMethod = HT.methodPost,
604− ariPayload = payload,
605− ariPayloadHash = Nothing,
606− ariPath = healPath bucket prefix,
607− ariHeaders = [],
608− ariQueryParams = qparams
609− }
610−
611− let rspBS = NC.responseBody rsp
612− case eitherDecode rspBS of
613− Right hsr -> return hsr
614− Left err -> throwIO $ MErrVJsonParse $ T.pack err
465+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
466+ let payload = PayloadBS $ LBS.toStrict $ A.encode opts
467+ let qparams = bool [] (HT.queryTextToQuery [("forceStart", Just "true")])
468+ forceStart
469+
470+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
471+ , ariPayload = payload
472+ , ariPayloadHash = Nothing
473+ , ariPath = healPath bucket prefix
474+ , ariHeaders = []
475+ , ariQueryParams = qparams
476+ }
477+
478+ let rspBS = NC.responseBody rsp
479+ case eitherDecode rspBS of
480+ Right hsr -> return hsr
481+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
615482
616483-- | Start a heal sequence that scans data under given (possible empty)
617484-- `bucket` and `prefix`. The `recursive` bool turns on recursive
@@ -633,79 +500,60 @@ forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
633500-- properties, storage information, network statistics, etc.
634501getServerInfo :: Minio [ServerInfo]
635502getServerInfo = do
636− rsp <-
637− executeAdminRequest
638− AdminReqInfo
639− { ariMethod = HT.methodGet,
640− ariPayload = PayloadBS B.empty,
641− ariPayloadHash = Nothing,
642− ariPath = "v1/info",
643− ariHeaders = [],
644− ariQueryParams = []
645− }
646− let rspBS = NC.responseBody rsp
647− case eitherDecode rspBS of
648− Right si -> return si
649− Left err -> throwIO $ MErrVJsonParse $ T.pack err
503+ rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
504+ , ariPayload = PayloadBS B.empty
505+ , ariPayloadHash = Nothing
506+ , ariPath = "v1/info"
507+ , ariHeaders = []
508+ , ariQueryParams = []
509+ }
510+ let rspBS = NC.responseBody rsp
511+ case eitherDecode rspBS of
512+ Right si -> return si
513+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
650514
651515executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
652516executeAdminRequest ari = do
653− req <- buildAdminRequest ari
654− mgr <- asks mcConnManager
655− httpLbs req mgr
517+ req <- buildAdminRequest ari
518+ mgr <- asks mcConnManager
519+ httpLbs req mgr
656520
657521buildAdminRequest :: AdminReqInfo -> Minio NC.Request
658522buildAdminRequest areq = do
659− ci <- asks mcConnInfo
660− sha256Hash <-
661− if connectIsSecure ci
662− then -- if secure connection
663− return "UNSIGNED-PAYLOAD"
664− else -- otherwise compute sha256
665− getPayloadSHA256Hash (ariPayload areq)
666−
667− timeStamp <- liftIO getCurrentTime
668−
669− mgr <- asks mcConnManager
670− cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671−
672− let hostHeader = (hHost, getHostAddr ci)
673− newAreq =
674− areq
675− { ariPayloadHash = Just sha256Hash,
676− ariHeaders =
677− hostHeader
678− : sha256Header sha256Hash
679− : ariHeaders areq
680− }
681− signReq = toRequest ci newAreq
682− sp =
683− SignParams
684− (coerce $ cvAccessKey cv)
685− (coerce $ cvSecretKey cv)
686− (coerce $ cvSessionToken cv)
687− ServiceS3
688− timeStamp
689− Nothing
690− Nothing
691− (ariPayloadHash newAreq)
692− signHeaders = signV4 sp signReq
693−
694− -- Update signReq with Authorization header containing v4 signature
695− return
696− signReq
697− { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
698− }
523+ ci <- asks mcConnInfo
524+ sha256Hash <- if | connectIsSecure ci ->
525+ -- if secure connection
526+ return "UNSIGNED-PAYLOAD"
527+
528+ -- otherwise compute sha256
529+ | otherwise -> getPayloadSHA256Hash (ariPayload areq)
530+
531+ timeStamp <- liftIO getCurrentTime
532+
533+ let hostHeader = (hHost, getHostAddr ci)
534+ newAreq = areq { ariPayloadHash = Just sha256Hash
535+ , ariHeaders = hostHeader
536+ : sha256Header sha256Hash
537+ : ariHeaders areq
538+ }
539+ signReq = toRequest ci newAreq
540+ sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
541+ timeStamp Nothing Nothing (ariPayloadHash newAreq)
542+ signHeaders = signV4 sp signReq
543+
544+ -- Update signReq with Authorization header containing v4 signature
545+ return signReq {
546+ NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
547+ }
699548 where
700549 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
701− toRequest ci aReq =
702− NC.defaultRequest
703− { NC.method = ariMethod aReq,
704− NC.secure = connectIsSecure ci,
705− NC.host = encodeUtf8 $ connectHost ci,
706− NC.port = connectPort ci,
707− NC.path = B.intercalate "/" [adminPath, ariPath aReq],
708− NC.requestHeaders = ariHeaders aReq,
709− NC.queryString = HT.renderQuery False $ ariQueryParams aReq,
710− NC.requestBody = getRequestBody (ariPayload aReq)
550+ toRequest ci aReq = NC.defaultRequest
551+ { NC.method = ariMethod aReq
552+ , NC.secure = connectIsSecure ci
553+ , NC.host = encodeUtf8 $ connectHost ci
554+ , NC.port = connectPort ci
555+ , NC.path = B.intercalate "/" [adminPath, ariPath aReq]
556+ , NC.requestHeaders = ariHeaders aReq
557+ , NC.queryString = HT.renderQuery False $ ariQueryParams aReq
558+ , NC.requestBody = getRequestBody (ariPayload aReq)
711559 }
src/Network/Minio/CopyObject.hs+34−48
@@ -16,19 +16,19 @@
1616
1717module Network.Minio.CopyObject where
1818
19−import qualified Data.List as List
20−import Lib.Prelude
21−import Network.Minio.Data
22−import Network.Minio.Errors
23−import Network.Minio.S3API
24−import Network.Minio.Utils
19+import qualified Data.List as List
20+
21+import Lib.Prelude
22+
23+import Network.Minio.Data
24+import Network.Minio.Errors
25+import Network.Minio.S3API
26+import Network.Minio.Utils
27+
2528
2629-- | Copy an object using single or multipart copy strategy.
27−copyObjectInternal ::
28− Bucket ->
29− Object ->
30− SourceInfo ->
31− Minio ETag
30+copyObjectInternal :: Bucket -> Object -> SourceInfo
31+ -> Minio ETag
3232copyObjectInternal b' o srcInfo = do
3333 let sBucket = srcBucket srcInfo
3434 sObject = srcObject srcInfo
@@ -43,33 +43,27 @@ copyObjectInternal b' o srcInfo = do
4343 startOffset = fst range
4444 endOffset = snd range
4545
46− when
47− ( isJust rangeMay
48− && ( (startOffset < 0)
49− || (endOffset < startOffset)
50− || (endOffset >= srcSize)
51− )
52− )
53− $ throwIO
54− $ MErrVInvalidSrcObjByteRange range
46+ when (isJust rangeMay &&
47+ or [startOffset < 0, endOffset < startOffset,
48+ endOffset >= fromIntegral srcSize]) $
49+ throwIO $ MErrVInvalidSrcObjByteRange range
5550
5651 -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
5752 -- 2. If startOffset /= 0 use multipart copy
58− let destSize =
59− (\(a, b) -> b - a + 1) $
60− maybe (0, srcSize - 1) identity rangeMay
53+ let destSize = (\(a, b) -> b - a + 1 ) $
54+ maybe (0, srcSize - 1) identity rangeMay
6155
6256 if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
6357 then multiPartCopyObject b' o srcInfo srcSize
64− else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} []
58+
59+ else fst <$> copyObjectSingle b' o srcInfo{srcRange = Nothing} []
6560
6661-- | Given the input byte range of the source object, compute the
6762-- splits for a multipart copy object procedure. Minimum part size
6863-- used is minPartSize.
6964selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
70−selectCopyRanges (st, end) =
71− zip pns $
72− zipWith (\x y -> (st + x, st + x + y - 1)) startOffsets partSizes
65+selectCopyRanges (st, end) = zip pns $
66+ map (\(x, y) -> (st + x, st + x + y - 1)) $ zip startOffsets partSizes
7367 where
7468 size = end - st + 1
7569 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
@@ -77,30 +71,22 @@ selectCopyRanges (st, end) =
7771-- | Perform a multipart copy object action. Since we cannot verify
7872-- existing parts based on the source object, there is no resuming
7973-- copy action support.
80−multiPartCopyObject ::
81− Bucket ->
82− Object ->
83− SourceInfo ->
84− Int64 ->
85− Minio ETag
74+multiPartCopyObject :: Bucket -> Object -> SourceInfo -> Int64
75+ -> Minio ETag
8676multiPartCopyObject b o cps srcSize = do
8777 uid <- newMultipartUpload b o []
8878
89− let byteRange = maybe (0, srcSize - 1) identity $ srcRange cps
79+ let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $ srcRange cps
9080 partRanges = selectCopyRanges byteRange
91− partSources =
92− map
93− (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end)}))
94− partRanges
95− dstInfo = defaultDestinationInfo {dstBucket = b, dstObject = o}
96−
97− copiedParts <-
98− limitedMapConcurrently
99− 10
100− ( \(pn, cps') -> do
101− (etag, _) <- copyObjectPart dstInfo cps' uid pn []
102− return (pn, etag)
103− )
104− partSources
81+ partSources = map (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end) }))
82+ partRanges
83+ dstInfo = defaultDestinationInfo { dstBucket = b, dstObject = o}
84+
85+ copiedParts <- limitedMapConcurrently 10
86+ (\(pn, cps') -> do
87+ (etag, _) <- copyObjectPart dstInfo cps' uid pn []
88+ return (pn, etag)
89+ )
90+ partSources
10591
10692 completeMultipartUpload b o uid copiedParts
src/Network/Minio/Credentials.hsdeleted−77
@@ -1,77 +0,0 @@
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.hsdeleted−266
@@ -1,266 +0,0 @@
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.hsdeleted−90
@@ -1,90 +0,0 @@
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+559−697
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,64 +13,45 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE CPP #-}
17−{-# LANGUAGE GADTs #-}
18−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
19−{-# LANGUAGE StrictData #-}
20−{-# LANGUAGE TypeFamilies #-}
2116
17+{-# LANGUAGE CPP #-}
18+{-# LANGUAGE GADTs #-}
19+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
20+{-# LANGUAGE TypeFamilies #-}
2221module Network.Minio.Data where
2322
24−import qualified Conduit as C
25−import qualified Control.Concurrent.MVar as M
26−import Control.Monad.Trans.Except (throwE)
27−import Control.Monad.Trans.Resource
28− ( MonadResource,
29− MonadThrow (..),
30− MonadUnliftIO,
31− ResourceT,
32− runResourceT,
33− )
34−import qualified Data.Aeson as A
35−import qualified Data.ByteArray as BA
36−import qualified Data.ByteString as B
37−import qualified Data.ByteString.Lazy as LB
38−import Data.Default.Class (def)
39−import qualified Data.HashMap.Strict as H
40−import qualified Data.Ini as Ini
41−import qualified Data.List as List
42−import qualified Data.Text as T
43−import qualified Data.Text.Encoding as TE
44−import Data.Time (defaultTimeLocale, formatTime)
45−import Lib.Prelude (UTCTime, throwIO)
46−import qualified Network.Connection as Conn
47−import Network.HTTP.Client (defaultManagerSettings)
48−import qualified Network.HTTP.Client.TLS as TLS
49−import qualified Network.HTTP.Conduit as NC
50−import Network.HTTP.Types
51− ( ByteRange,
52− Header,
53− Method,
54− Query,
55− hRange,
56− )
57−import qualified Network.HTTP.Types as HT
58−import Network.Minio.Credentials
59−import Network.Minio.Data.Crypto
60− ( encodeToBase64,
61− hashMD5ToBase64,
62− )
63−import Network.Minio.Data.Time (UrlExpiry)
64−import Network.Minio.Errors
65− ( MErrV (MErrVInvalidEncryptionKeyLength, MErrVMissingCredentials),
66− MinioErr (..),
67− )
68−import Network.Minio.Utils
69−import System.Directory (doesFileExist, getHomeDirectory)
70−import qualified System.Environment as Env
71−import System.FilePath.Posix (combine)
72−import qualified UnliftIO as U
73−import qualified UnliftIO.MVar as UM
23+import qualified Conduit as C
24+import qualified Control.Concurrent.MVar as M
25+import Control.Monad.IO.Unlift (UnliftIO (..), askUnliftIO,
26+ withUnliftIO)
27+import Control.Monad.Trans.Resource
28+import qualified Data.Aeson as A
29+import qualified Data.ByteArray as BA
30+import qualified Data.ByteString as B
31+import Data.CaseInsensitive (mk)
32+import qualified Data.HashMap.Strict as H
33+import qualified Data.Ini as Ini
34+import Data.String (IsString (..))
35+import qualified Data.Text as T
36+import qualified Data.Text.Encoding as TE
37+import Data.Time (defaultTimeLocale, formatTime)
38+import GHC.Show (Show (show))
39+import qualified Network.Connection as Conn
40+import Network.HTTP.Client (defaultManagerSettings)
41+import qualified Network.HTTP.Client.TLS as TLS
42+import qualified Network.HTTP.Conduit as NC
43+import Network.HTTP.Types (ByteRange, Header, Method, Query,
44+ hRange)
45+import qualified Network.HTTP.Types as HT
46+import Network.Minio.Errors
47+import System.Directory (doesFileExist, getHomeDirectory)
48+import qualified System.Environment as Env
49+import System.FilePath.Posix (combine)
50+import Text.XML
51+import qualified UnliftIO as U
52+
53+import Lib.Prelude
54+import Network.Minio.Data.Crypto
7455
7556-- | max obj size is 5TiB
7657maxObjectSize :: Int64
@@ -90,157 +71,125 @@ maxMultipartParts = 10000
9071-- TODO: Add a type which provides typed constants for region. this
9172-- type should have a IsString instance to infer the appropriate
9273-- constant.
93−
9474-- | awsRegionMap - library constant
9575awsRegionMap :: H.HashMap Text Text
96−awsRegionMap =
97− H.fromList
98− [ ("us-east-1", "s3.us-east-1.amazonaws.com"),
99− ("us-east-2", "s3.us-east-2.amazonaws.com"),
100− ("us-west-1", "s3.us-west-1.amazonaws.com"),
101− ("us-west-2", "s3.us-west-2.amazonaws.com"),
102− ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
103− ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
104− ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
105− ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
106− ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
107− ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
108− ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
109− ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
110− ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
111− ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
112− ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
113− ("eu-west-3", "s3.eu-west-3.amazonaws.com"),
114− ("eu-central-1", "s3.eu-central-1.amazonaws.com"),
115− ("eu-central-2", "s3.eu-central-2.amazonaws.com"),
116− ("eu-south-1", "s3.eu-south-1.amazonaws.com"),
117− ("eu-south-2", "s3.eu-south-2.amazonaws.com"),
118− ("af-south-1", "s3.af-south-1.amazonaws.com"),
119− ("ap-east-1", "s3.ap-east-1.amazonaws.com"),
120− ("cn-north-1", "s3.cn-north-1.amazonaws.com.cn"),
121− ("cn-northwest-1", "s3.cn-northwest-1.amazonaws.com.cn"),
122− ("eu-north-1", "s3.eu-north-1.amazonaws.com"),
123− ("me-south-1", "s3.me-south-1.amazonaws.com"),
124− ("me-central-1", "s3.me-central-1.amazonaws.com"),
125− ("us-gov-east-1", "s3.us-gov-east-1.amazonaws.com"),
126− ("us-gov-west-1", "s3.us-gov-west-1.amazonaws.com"),
127− ("sa-east-1", "s3.sa-east-1.amazonaws.com")
128− ]
76+awsRegionMap = H.fromList [
77+ ("us-east-1", "s3.amazonaws.com")
78+ , ("us-east-2", "s3-us-east-2.amazonaws.com")
79+ , ("us-west-1", "s3-us-west-1.amazonaws.com")
80+ , ("us-west-2", "s3-us-west-2.amazonaws.com")
81+ , ("ca-central-1", "s3-ca-central-1.amazonaws.com")
82+ , ("ap-south-1", "s3-ap-south-1.amazonaws.com")
83+ , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
84+ , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
85+ , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
86+ , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
87+ , ("eu-west-1", "s3-eu-west-1.amazonaws.com")
88+ , ("eu-west-2", "s3-eu-west-2.amazonaws.com")
89+ , ("eu-central-1", "s3-eu-central-1.amazonaws.com")
90+ , ("sa-east-1", "s3-sa-east-1.amazonaws.com")
91+ ]
12992
13093-- | Connection Info data type. To create a 'ConnectInfo' value,
13194-- enable the @OverloadedStrings@ language extension and use the
13295-- `IsString` instance to provide a URL, for example:
13396--
13497-- > let c :: ConnectInfo = "https://play.min.io"
135−data ConnectInfo = ConnectInfo
136− { connectHost :: Text,
137− connectPort :: Int,
138− connectCreds :: Creds,
139− connectIsSecure :: Bool,
140− connectRegion :: Region,
141− connectAutoDiscoverRegion :: Bool,
142− connectDisableTLSCertValidation :: Bool
143− }
144−
145−getEndpoint :: ConnectInfo -> Endpoint
146−getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
98+data ConnectInfo =
99+ ConnectInfo { connectHost :: Text
100+ , connectPort :: Int
101+ , connectAccessKey :: Text
102+ , connectSecretKey :: Text
103+ , connectIsSecure :: Bool
104+ , connectRegion :: Region
105+ , connectAutoDiscoverRegion :: Bool
106+ , connectDisableTLSCertValidation :: Bool
107+ } deriving (Eq, Show)
147108
148109instance IsString ConnectInfo where
149− fromString str =
150− let req = NC.parseRequest_ str
151− in ConnectInfo
152− { connectHost = TE.decodeUtf8 $ NC.host req,
153− connectPort = NC.port req,
154− connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
155− connectIsSecure = NC.secure req,
156− connectRegion = "",
157− connectAutoDiscoverRegion = True,
158− connectDisableTLSCertValidation = False
159− }
160−
161−-- | A 'CredentialLoader' is an action that may return a 'CredentialValue'.
162−-- Loaders may be chained together using 'findFirst'.
163−--
164−-- @since 1.7.0
165−type CredentialLoader = IO (Maybe CredentialValue)
166−
167−-- | Combines the given list of loaders, by calling each one in
168−-- order until a 'CredentialValue' is returned.
169−findFirst :: [CredentialLoader] -> IO (Maybe CredentialValue)
110+ fromString str =
111+ let req = NC.parseRequest_ str
112+ in ConnectInfo
113+ { connectHost = TE.decodeUtf8 $ NC.host req
114+ , connectPort = NC.port req
115+ , connectAccessKey = ""
116+ , connectSecretKey = ""
117+ , connectIsSecure = NC.secure req
118+ , connectRegion = ""
119+ , connectAutoDiscoverRegion = True
120+ , connectDisableTLSCertValidation = False
121+ }
122+
123+-- | Contains access key and secret key to access object storage.
124+data Credentials = Credentials { cAccessKey :: Text
125+ , cSecretKey :: Text
126+ } deriving (Eq, Show)
127+
128+-- | A Provider is an action that may return Credentials. Providers
129+-- may be chained together using 'findFirst'.
130+type Provider = IO (Maybe Credentials)
131+
132+-- | Combines the given list of providers, by calling each one in
133+-- order until Credentials are found.
134+findFirst :: [Provider] -> Provider
170135findFirst [] = return Nothing
171−findFirst (f : fs) = do
172− c <- f
173− maybe (findFirst fs) (return . Just) c
136+findFirst (f:fs) = do c <- f
137+ maybe (findFirst fs) (return . Just) c
174138
175−-- | This action returns a 'CredentialValue' populated from
176−-- @~\/.aws\/credentials@
177−fromAWSConfigFile :: CredentialLoader
139+-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
140+fromAWSConfigFile :: Provider
178141fromAWSConfigFile = do
179− credsE <- runExceptT $ do
180− homeDir <- lift getHomeDirectory
181− let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
182− fileExists <- lift $ doesFileExist awsCredsFile
183− bool (throwE "FileNotFound") (return ()) fileExists
184− ini <- ExceptT $ Ini.readIniFile awsCredsFile
185− akey <-
186− ExceptT $
187− return $
188− Ini.lookupValue "default" "aws_access_key_id" ini
189− skey <-
190− ExceptT $
191− return $
192− Ini.lookupValue "default" "aws_secret_access_key" ini
193− return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
194− return $ either (const Nothing) Just credsE
195−
196−-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
197−-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
198−fromAWSEnv :: CredentialLoader
142+ credsE <- runExceptT $ do
143+ homeDir <- lift $ getHomeDirectory
144+ let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
145+ fileExists <- lift $ doesFileExist awsCredsFile
146+ bool (throwE "FileNotFound") (return ()) fileExists
147+ ini <- ExceptT $ Ini.readIniFile awsCredsFile
148+ akey <- ExceptT $ return
149+ $ Ini.lookupValue "default" "aws_access_key_id" ini
150+ skey <- ExceptT $ return
151+ $ Ini.lookupValue "default" "aws_secret_access_key" ini
152+ return $ Credentials akey skey
153+ return $ hush credsE
154+
155+-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
156+-- @AWS_SECRET_ACCESS_KEY@ environment variables.
157+fromAWSEnv :: Provider
199158fromAWSEnv = runMaybeT $ do
200− akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
201− skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
202− return $ CredentialValue (fromString akey) (fromString skey) Nothing
159+ akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
160+ skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
161+ return $ Credentials (T.pack akey) (T.pack skey)
203162
204−-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
205−-- and @MINIO_SECRET_KEY@ environment variables.
206−fromMinioEnv :: CredentialLoader
163+-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
164+-- @MINIO_SECRET_KEY@ environment variables.
165+fromMinioEnv :: Provider
207166fromMinioEnv = runMaybeT $ do
208− akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
209− skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
210− return $ CredentialValue (fromString akey) (fromString skey) Nothing
211−
212−-- | setCredsFrom retrieves access credentials from the first action in the
213−-- given list that succeeds and sets it in the 'ConnectInfo'.
214−setCredsFrom :: [CredentialLoader] -> ConnectInfo -> IO ConnectInfo
215−setCredsFrom ps ci = do
216− pMay <- findFirst ps
217− maybe
218− (throwIO MErrVMissingCredentials)
219− (return . (`setCreds` ci))
220− pMay
221−
222−-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
223−setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
224−setCreds cv connInfo =
225− connInfo
226− { connectCreds = CredsStatic cv
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}
167+ akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
168+ skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
169+ return $ Credentials (T.pack akey) (T.pack skey)
170+
171+-- | setCredsFrom retrieves access credentials from the first
172+-- `Provider` form the given list that succeeds and sets it in the
173+-- `ConnectInfo`.
174+setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo
175+setCredsFrom ps ci = do pMay <- findFirst ps
176+ maybe
177+ (throwIO MErrVMissingCredentials)
178+ (return . (flip setCreds ci))
179+ pMay
180+
181+-- | setCreds sets the given `Credentials` in the `ConnectInfo`.
182+setCreds :: Credentials -> ConnectInfo -> ConnectInfo
183+setCreds (Credentials accessKey secretKey) connInfo =
184+ connInfo { connectAccessKey = accessKey
185+ , connectSecretKey = secretKey
186+ }
236187
237188-- | Set the S3 region parameter in the `ConnectInfo`
238189setRegion :: Region -> ConnectInfo -> ConnectInfo
239−setRegion r connInfo =
240− connInfo
241− { connectRegion = r,
242− connectAutoDiscoverRegion = False
243− }
190+setRegion r connInfo = connInfo { connectRegion = r
191+ , connectAutoDiscoverRegion = False
192+ }
244193
245194-- | Check if the connection to object storage server is secure
246195-- (i.e. uses TLS)
@@ -252,10 +201,16 @@ isConnectInfoSecure = connectIsSecure
252201-- certificates. Note that this option has no effect, if you provide
253202-- your own Manager in `mkMinioConn`.
254203disableTLSCertValidation :: ConnectInfo -> ConnectInfo
255−disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
204+disableTLSCertValidation c = c { connectDisableTLSCertValidation = True }
256205
257206getHostAddr :: ConnectInfo -> ByteString
258−getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
207+getHostAddr ci = if | port == 80 || port == 443 -> toS host
208+ | otherwise -> toS $
209+ T.concat [ host, ":" , Lib.Prelude.show port]
210+ where
211+ port = connectPort ci
212+ host = connectHost ci
213+
259214
260215-- | Default Google Compute Storage ConnectInfo. Works only for
261216-- "Simple Migration" use-case with interoperability mode enabled on
@@ -264,25 +219,23 @@ getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
264219--
265220-- Credentials should be supplied before use.
266221gcsCI :: ConnectInfo
267−gcsCI =
268− setRegion
269− "us"
270− "https://storage.googleapis.com"
222+gcsCI = setRegion "us"
223+ "https://storage.googleapis.com"
224+
271225
272226-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
273227-- should be supplied before use.
274228awsCI :: ConnectInfo
275229awsCI = "https://s3.amazonaws.com"
276230
231+
277232-- | <https://play.min.io MinIO Play Server>
278233-- ConnectInfo. Credentials are already filled in.
279234minioPlayCI :: ConnectInfo
280−minioPlayCI =
281− let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
282− in setCreds playCreds $
283− setRegion
284− "us-east-1"
285− "https://play.min.io"
235+minioPlayCI = let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
236+ in setCreds playCreds
237+ $ setRegion "us-east-1"
238+ "https://play.min.io"
286239
287240-- |
288241-- Represents a bucket in the object store
@@ -301,147 +254,135 @@ type ETag = Text
301254-- | Data type to represent an object encryption key. Create one using
302255-- the `mkSSECKey` function.
303256newtype SSECKey = SSECKey BA.ScrubbedBytes
304− deriving stock (Eq, Show)
257+ deriving (Eq, Show)
305258
306259-- | Validates that the given ByteString is 32 bytes long and creates
307260-- an encryption key.
308−mkSSECKey :: (MonadThrow m) => ByteString -> m SSECKey
309−mkSSECKey keyBytes
310− | B.length keyBytes /= 32 =
311− throwM MErrVInvalidEncryptionKeyLength
312− | otherwise =
313− return $ SSECKey $ BA.convert keyBytes
261+mkSSECKey :: MonadThrow m => ByteString -> m SSECKey
262+mkSSECKey keyBytes | B.length keyBytes /= 32 =
263+ throwM MErrVInvalidEncryptionKeyLength
264+ | otherwise =
265+ return $ SSECKey $ BA.convert keyBytes
314266
315267-- | Data type to represent Server-Side-Encryption settings
316268data SSE where
317− -- | Specifies SSE S3 encryption - server manages encryption keys
318− SSE :: SSE
319− -- | Specifies that KMS service should be used. The first argument
320− -- to the constructor is the Key Id to be used by the server (if
321− -- not specified, the default KMS key id is used). The second
322− -- argument is the optional KMS context that must have a
323− -- `A.ToJSON` instance - please refer to the AWS S3 documentation
324− -- for detailed information.
325− SSEKMS :: (A.ToJSON a) => Maybe ByteString -> Maybe a -> SSE
326− -- | Specifies server-side encryption with customer provided
327− -- key. The argument is the encryption key to be used.
328− SSEC :: SSECKey -> SSE
269+ -- | Specifies SSE S3 encryption - server manages encryption keys
270+ SSE :: SSE
271+ -- | Specifies that KMS service should be used. The first argument
272+ -- to the constructor is the Key Id to be used by the server (if
273+ -- not specified, the default KMS key id is used). The second
274+ -- argument is the optional KMS context that must have a
275+ -- `A.ToJSON` instance - please refer to the AWS S3 documentation
276+ -- for detailed information.
277+ SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
278+ -- | Specifies server-side encryption with customer provided
279+ -- key. The argument is the encryption key to be used.
280+ SSEC :: SSECKey -> SSE
329281
330282toPutObjectHeaders :: SSE -> [HT.Header]
331283toPutObjectHeaders sseArg =
332− let sseHeader = "x-amz-server-side-encryption"
333− sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
334− sseKmsContextHeader = sseHeader <> "-context"
335− ssecAlgo = sseHeader <> "-customer-algorithm"
336− ssecKey = sseHeader <> "-customer-key"
337− ssecKeyMD5 = ssecKey <> "-MD5"
338− in case sseArg of
339− SSE -> [(sseHeader, "AES256")]
340− SSEKMS keyIdMay ctxMay ->
341− [(sseHeader, "aws:kms")]
342− ++ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay
343− ++ maybe [] (\k -> [(sseKmsContextHeader, LB.toStrict $ A.encode k)]) ctxMay
344− SSEC (SSECKey sb) ->
345− [ (ssecAlgo, "AES256"),
346− (ssecKey, encodeToBase64 sb),
347− (ssecKeyMD5, hashMD5ToBase64 sb)
348− ]
284+ let sseHeader = "x-amz-server-side-encryption"
285+ sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
286+ sseKmsContextHeader = sseHeader <> "-context"
287+ ssecAlgo = sseHeader <> "-customer-algorithm"
288+ ssecKey = sseHeader <> "-customer-key"
289+ ssecKeyMD5 = ssecKey <> "-MD5"
290+
291+ in case sseArg of
292+ SSE -> [(sseHeader, "AES256")]
293+
294+ SSEKMS keyIdMay ctxMay ->
295+ [(sseHeader, "aws:kms")] ++
296+ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay ++
297+ maybe [] (\k -> [(sseKmsContextHeader, toS $ A.encode k)]) ctxMay
298+
299+ SSEC (SSECKey sb) ->
300+ [(ssecAlgo, "AES256"),
301+ (ssecKey, encodeToBase64 sb),
302+ (ssecKeyMD5, hashMD5ToBase64 sb)]
349303
350304-- | Data type for options in PutObject call. Start with the empty
351305-- `defaultPutObjectOptions` and use various the various poo*
352306-- accessors.
353−data PutObjectOptions = PutObjectOptions
354− { -- | Set a standard MIME type describing the format of the object.
355− pooContentType :: Maybe Text,
356− -- | Set what content encodings have been applied to the object and thus
357− -- what decoding mechanisms must be applied to obtain the media-type
358− -- referenced by the Content-Type header field.
359− pooContentEncoding :: Maybe Text,
360− -- | Set presentational information for the object.
361− pooContentDisposition :: Maybe Text,
362− -- | Set to specify caching behavior for the object along the
363− -- request/reply chain.
364− pooCacheControl :: Maybe Text,
365− -- | Set to describe the language(s) intended for the audience.
366− pooContentLanguage :: Maybe Text,
367− -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
368− -- performance needs, storage class is @STANDARD@ by default (i.e
369− -- when Nothing is passed).
370− pooStorageClass :: Maybe Text,
371− -- | Set user defined metadata to store with the object.
372− pooUserMetadata :: [(Text, Text)],
373− -- | Set number of worker threads used to upload an object.
374− pooNumThreads :: Maybe Word,
375− -- | Set object encryption parameters for the request.
376− pooSSE :: Maybe SSE
307+data PutObjectOptions = PutObjectOptions {
308+ -- | Set a standard MIME type describing the format of the object.
309+ pooContentType :: Maybe Text
310+ -- | Set what content encodings have been applied to the object and thus
311+ -- what decoding mechanisms must be applied to obtain the media-type
312+ -- referenced by the Content-Type header field.
313+ , pooContentEncoding :: Maybe Text
314+ -- | Set presentational information for the object.
315+ , pooContentDisposition :: Maybe Text
316+ -- | Set to specify caching behavior for the object along the
317+ -- request/reply chain.
318+ , pooCacheControl :: Maybe Text
319+ -- | Set to describe the language(s) intended for the audience.
320+ , pooContentLanguage :: Maybe Text
321+ -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
322+ -- performance needs, storage class is @STANDARD@ by default (i.e
323+ -- when Nothing is passed).
324+ , pooStorageClass :: Maybe Text
325+ -- | Set user defined metadata to store with the object.
326+ , pooUserMetadata :: [(Text, Text)]
327+ -- | Set number of worker threads used to upload an object.
328+ , pooNumThreads :: Maybe Word
329+ -- | Set object encryption parameters for the request.
330+ , pooSSE :: Maybe SSE
377331 }
378332
379333-- | Provide default `PutObjectOptions`.
380334defaultPutObjectOptions :: PutObjectOptions
381335defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
382336
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+
383354pooToHeaders :: PutObjectOptions -> [HT.Header]
384−pooToHeaders poo =
385− userMetadata
386− ++ mapMaybe tupToMaybe (zip names values)
387− ++ maybe [] toPutObjectHeaders (pooSSE poo)
355+pooToHeaders poo = userMetadata
356+ ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
357+ ++ maybe [] toPutObjectHeaders (pooSSE poo)
388358 where
389− tupToMaybe (k, Just v) = Just (k, v)
359+ tupToMaybe (k, Just v) = Just (k, v)
390360 tupToMaybe (_, Nothing) = Nothing
361+
391362 userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
392− names =
393− [ "content-type",
394− "content-encoding",
395− "content-disposition",
396− "content-language",
397− "cache-control",
398− "x-amz-storage-class"
399− ]
400− values =
401− map
402− (fmap encodeUtf8 . (poo &))
403− [ pooContentType,
404− pooContentEncoding,
405− pooContentDisposition,
406− pooContentLanguage,
407− pooCacheControl,
408− pooStorageClass
409− ]
363+
364+ names = ["content-type",
365+ "content-encoding",
366+ "content-disposition",
367+ "content-language",
368+ "cache-control",
369+ "x-amz-storage-class"]
370+ values = map (fmap encodeUtf8 . (poo &))
371+ [pooContentType, pooContentEncoding,
372+ pooContentDisposition, pooContentLanguage,
373+ pooCacheControl, pooStorageClass]
374+
410375
411376-- |
412377-- BucketInfo returned for list buckets call
413−data BucketInfo = BucketInfo
414− { biName :: Bucket,
415− biCreationDate :: UTCTime
416− }
417− deriving stock (Show, Eq)
378+data BucketInfo = BucketInfo {
379+ biName :: Bucket
380+ , biCreationDate :: UTCTime
381+ } deriving (Show, Eq)
418382
419383-- | A type alias to represent a part-number for multipart upload
420384type PartNumber = Int16
421385
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−
445386-- | A type alias to represent an upload-id for multipart upload
446387type UploadId = Text
447388
@@ -450,116 +391,95 @@ type PartTuple = (PartNumber, ETag)
450391
451392-- | Represents result from a listing of object parts of an ongoing
452393-- multipart upload.
453−data ListPartsResult = ListPartsResult
454− { lprHasMore :: Bool,
455− lprNextPart :: Maybe Int,
456− lprParts :: [ObjectPartInfo]
457− }
458− deriving stock (Show, Eq)
394+data ListPartsResult = ListPartsResult {
395+ lprHasMore :: Bool
396+ , lprNextPart :: Maybe Int
397+ , lprParts :: [ObjectPartInfo]
398+ } deriving (Show, Eq)
459399
460400-- | Represents information about an object part in an ongoing
461401-- multipart upload.
462−data ObjectPartInfo = ObjectPartInfo
463− { opiNumber :: PartNumber,
464− opiETag :: ETag,
465− opiSize :: Int64,
466− opiModTime :: UTCTime
467− }
468− deriving stock (Show, Eq)
402+data ObjectPartInfo = ObjectPartInfo {
403+ opiNumber :: PartNumber
404+ , opiETag :: ETag
405+ , opiSize :: Int64
406+ , opiModTime :: UTCTime
407+ } deriving (Show, Eq)
469408
470409-- | Represents result from a listing of incomplete uploads to a
471410-- bucket.
472−data ListUploadsResult = ListUploadsResult
473− { lurHasMore :: Bool,
474− lurNextKey :: Maybe Text,
475− lurNextUpload :: Maybe Text,
476− lurUploads :: [(Object, UploadId, UTCTime)],
477− lurCPrefixes :: [Text]
478− }
479− deriving stock (Show, Eq)
411+data ListUploadsResult = ListUploadsResult {
412+ lurHasMore :: Bool
413+ , lurNextKey :: Maybe Text
414+ , lurNextUpload :: Maybe Text
415+ , lurUploads :: [(Object, UploadId, UTCTime)]
416+ , lurCPrefixes :: [Text]
417+ } deriving (Show, Eq)
480418
481419-- | Represents information about a multipart upload.
482−data UploadInfo = UploadInfo
483− { uiKey :: Object,
484− uiUploadId :: UploadId,
485− uiInitTime :: UTCTime,
486− uiSize :: Int64
487− }
488− deriving stock (Show, Eq)
420+data UploadInfo = UploadInfo {
421+ uiKey :: Object
422+ , uiUploadId :: UploadId
423+ , uiInitTime :: UTCTime
424+ , uiSize :: Int64
425+ } deriving (Show, Eq)
489426
490427-- | Represents result from a listing of objects in a bucket.
491−data ListObjectsResult = ListObjectsResult
492− { lorHasMore :: Bool,
493− lorNextToken :: Maybe Text,
494− lorObjects :: [ObjectInfo],
495− lorCPrefixes :: [Text]
496− }
497− deriving stock (Show, Eq)
428+data ListObjectsResult = ListObjectsResult {
429+ lorHasMore :: Bool
430+ , lorNextToken :: Maybe Text
431+ , lorObjects :: [ObjectInfo]
432+ , lorCPrefixes :: [Text]
433+ } deriving (Show, Eq)
498434
499435-- | Represents result from a listing of objects version 1 in a bucket.
500−data ListObjectsV1Result = ListObjectsV1Result
501− { lorHasMore' :: Bool,
502− lorNextMarker :: Maybe Text,
503− lorObjects' :: [ObjectInfo],
504− lorCPrefixes' :: [Text]
505− }
506− deriving stock (Show, Eq)
436+data ListObjectsV1Result = ListObjectsV1Result {
437+ lorHasMore' :: Bool
438+ , lorNextMarker :: Maybe Text
439+ , lorObjects' :: [ObjectInfo]
440+ , lorCPrefixes' :: [Text]
441+ } deriving (Show, Eq)
507442
508443-- | Represents information about an object.
509444data ObjectInfo = ObjectInfo
510− { -- | Object key
511− oiObject :: Object,
512− -- | Modification time of the object
513− oiModTime :: UTCTime,
514− -- | ETag of the object
515− oiETag :: ETag,
516− -- | Size of the object in bytes
517− oiSize :: Int64,
518− -- | A map of user-metadata
519− -- pairs stored with an
520− -- object (keys will not
521− -- have the @X-Amz-Meta-@
522− -- prefix).
523− oiUserMetadata :: H.HashMap Text Text,
524− -- | A map of metadata
525− -- key-value pairs (not
526− -- including the
527− -- user-metadata pairs)
528− oiMetadata :: H.HashMap Text Text
529− }
530− deriving stock (Show, Eq)
445+ { oiObject :: Object -- ^ Object key
446+ , oiModTime :: UTCTime -- ^ Modification time of the object
447+ , oiETag :: ETag -- ^ ETag of the object
448+ , oiSize :: Int64 -- ^ Size of the object in bytes
449+ , oiUserMetadata :: H.HashMap Text Text -- ^ A map of user-metadata
450+ -- pairs stored with an
451+ -- object (keys will not
452+ -- have the @X-Amz-Meta-@
453+ -- prefix).
454+ , oiMetadata :: H.HashMap Text Text -- ^ A map of metadata
455+ -- key-value pairs (not
456+ -- including the
457+ -- user-metadata pairs)
458+ } deriving (Show, Eq)
531459
532460-- | Represents source object in server-side copy object
533461data SourceInfo = SourceInfo
534− { -- | Bucket containing the source object
535− srcBucket :: Text,
536− -- | Source object key
537− srcObject :: Text,
538− -- | Source object
539− -- byte-range
540− -- (inclusive)
541− srcRange :: Maybe (Int64, Int64),
542− -- | ETag condition on source -
543− -- object is copied only if the
544− -- source object's ETag matches
545− -- this value.
546− srcIfMatch :: Maybe Text,
547− -- | ETag not match condition
548− -- on source - object is copied
549− -- if ETag does not match this
550− -- value.
551− srcIfNoneMatch :: Maybe Text,
552− -- | Copy source object only
553− -- if the source has been
554− -- modified since this time.
555− srcIfModifiedSince :: Maybe UTCTime,
556− -- | Copy source object only
557− -- if the source has been
558− -- un-modified since this
559− -- given time.
560− srcIfUnmodifiedSince :: Maybe UTCTime
561− }
562− deriving stock (Show, Eq)
462+ { srcBucket :: Text -- ^ Bucket containing the source object
463+ , srcObject :: Text -- ^ Source object key
464+ , srcRange :: Maybe (Int64, Int64) -- ^ Source object
465+ -- byte-range
466+ -- (inclusive)
467+ , srcIfMatch :: Maybe Text -- ^ ETag condition on source -
468+ -- object is copied only if the
469+ -- source object's ETag matches
470+ -- this value.
471+ , srcIfNoneMatch :: Maybe Text -- ^ ETag not match condition
472+ -- on source - object is copied
473+ -- if ETag does not match this
474+ -- value.
475+ , srcIfModifiedSince :: Maybe UTCTime -- ^ Copy source object only
476+ -- if the source has been
477+ -- modified since this time.
478+ , srcIfUnmodifiedSince :: Maybe UTCTime -- ^ Copy source object only
479+ -- if the source has been
480+ -- un-modified since this
481+ -- given time.
482+ } deriving (Show, Eq)
563483
564484-- | Provide a default for `SourceInfo`
565485defaultSourceInfo :: SourceInfo
@@ -567,12 +487,9 @@ defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing
567487
568488-- | Represents destination object in server-side copy object
569489data DestinationInfo = DestinationInfo
570− { -- | Destination bucket
571− dstBucket :: Text,
572− -- | Destination object key
573− dstObject :: Text
574− }
575− deriving stock (Show, Eq)
490+ { dstBucket :: Text -- ^ Destination bucket
491+ , dstObject :: Text -- ^ Destination object key
492+ } deriving (Show, Eq)
576493
577494-- | Provide a default for `DestinationInfo`
578495defaultDestinationInfo :: DestinationInfo
@@ -581,106 +498,96 @@ defaultDestinationInfo = DestinationInfo "" ""
581498-- | Data type for options when getting an object from the
582499-- service. Start with the empty `defaultGetObjectOptions` and modify
583500-- it using the goo* functions.
584−data GetObjectOptions = GetObjectOptions
585− { -- | Set object's data of given offset begin and end,
501+data GetObjectOptions = GetObjectOptions {
502+ -- | Set object's data of given offset begin and end,
586503 -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
587− gooRange :: Maybe ByteRange,
504+ gooRange :: Maybe ByteRange
588505 -- | Set matching ETag condition, GetObject which matches the following
589506 -- ETag.
590− gooIfMatch :: Maybe ETag,
507+ , gooIfMatch :: Maybe ETag
591508 -- | Set matching ETag none condition, GetObject which does not match
592509 -- the following ETag.
593− gooIfNoneMatch :: Maybe ETag,
510+ , gooIfNoneMatch :: Maybe ETag
594511 -- | Set object unmodified condition, GetObject unmodified since given time.
595− gooIfUnmodifiedSince :: Maybe UTCTime,
512+ , gooIfUnmodifiedSince :: Maybe UTCTime
596513 -- | Set object modified condition, GetObject modified since given time.
597− gooIfModifiedSince :: Maybe UTCTime,
514+ , gooIfModifiedSince :: Maybe UTCTime
598515 -- | Specify SSE-C key
599− gooSSECKey :: Maybe SSECKey
516+ , gooSSECKey :: Maybe SSECKey
600517 }
601518
602519-- | Provide default `GetObjectOptions`.
603520defaultGetObjectOptions :: GetObjectOptions
604521defaultGetObjectOptions =
605− GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
522+ GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
606523
607524gooToHeaders :: GetObjectOptions -> [HT.Header]
608−gooToHeaders goo =
609− rangeHdr
610− ++ zip names values
611− ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
525+gooToHeaders goo = rangeHdr ++ zip names values
526+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
612527 where
613− names =
614− [ "If-Match",
615− "If-None-Match",
616− "If-Unmodified-Since",
617− "If-Modified-Since"
618− ]
619− values =
620− mapMaybe
621− (fmap encodeUtf8 . (goo &))
622− [ gooIfMatch,
623− gooIfNoneMatch,
624− fmap formatRFC1123 . gooIfUnmodifiedSince,
625− fmap formatRFC1123 . gooIfModifiedSince
626− ]
627− rangeHdr =
628− maybe [] (\a -> [(hRange, HT.renderByteRanges [a])]) $
629− gooRange goo
528+ names = ["If-Match",
529+ "If-None-Match",
530+ "If-Unmodified-Since",
531+ "If-Modified-Since"]
532+ values = mapMaybe (fmap encodeUtf8 . (goo &))
533+ [gooIfMatch, gooIfNoneMatch,
534+ fmap formatRFC1123 . gooIfUnmodifiedSince,
535+ fmap formatRFC1123 . gooIfModifiedSince]
536+ rangeHdr = maybe [] (\a -> [(hRange, HT.renderByteRanges [a])])
537+ $ gooRange goo
630538
631539-- | Data type returned by 'getObject' representing the object being
632540-- retrieved. Use the @gor*@ functions to access its contents.
633−data GetObjectResponse = GetObjectResponse
634− { -- | ObjectInfo of the object being retrieved.
635− gorObjectInfo :: ObjectInfo,
636− -- | A conduit of the bytes of the object.
637− gorObjectStream :: C.ConduitM () ByteString Minio ()
638− }
541+data GetObjectResponse = GetObjectResponse {
542+ -- | ObjectInfo of the object being retrieved.
543+ gorObjectInfo :: ObjectInfo
544+ -- | A conduit of the bytes of the object.
545+ , gorObjectStream :: C.ConduitM () ByteString Minio ()
546+ }
639547
640548-- | A data-type for events that can occur in the object storage
641549-- server. Reference:
642550-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
643−data Event
644− = ObjectCreated
645− | ObjectCreatedPut
646− | ObjectCreatedPost
647− | ObjectCreatedCopy
648− | ObjectCreatedMultipartUpload
649− | ObjectRemoved
650− | ObjectRemovedDelete
651− | ObjectRemovedDeleteMarkerCreated
652− | ReducedRedundancyLostObject
653− deriving stock (Eq, Show)
654−
655−instance ToText Event where
656− toText ObjectCreated = "s3:ObjectCreated:*"
657− toText ObjectCreatedPut = "s3:ObjectCreated:Put"
658− toText ObjectCreatedPost = "s3:ObjectCreated:Post"
659− toText ObjectCreatedCopy = "s3:ObjectCreated:Copy"
660− toText ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
661− toText ObjectRemoved = "s3:ObjectRemoved:*"
662− toText ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
663− toText ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
664− toText ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
551+data Event = ObjectCreated
552+ | ObjectCreatedPut
553+ | ObjectCreatedPost
554+ | ObjectCreatedCopy
555+ | ObjectCreatedMultipartUpload
556+ | ObjectRemoved
557+ | ObjectRemovedDelete
558+ | ObjectRemovedDeleteMarkerCreated
559+ | ReducedRedundancyLostObject
560+ deriving (Eq)
561+
562+instance Show Event where
563+ show ObjectCreated = "s3:ObjectCreated:*"
564+ show ObjectCreatedPut = "s3:ObjectCreated:Put"
565+ show ObjectCreatedPost = "s3:ObjectCreated:Post"
566+ show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
567+ show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
568+ show ObjectRemoved = "s3:ObjectRemoved:*"
569+ show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
570+ show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
571+ show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
665572
666573textToEvent :: Text -> Maybe Event
667574textToEvent t = case t of
668− "s3:ObjectCreated:*" -> Just ObjectCreated
669− "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
670− "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
671− "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
672− "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
673− "s3:ObjectRemoved:*" -> Just ObjectRemoved
674− "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
575+ "s3:ObjectCreated:*" -> Just ObjectCreated
576+ "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
577+ "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
578+ "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
579+ "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
580+ "s3:ObjectRemoved:*" -> Just ObjectRemoved
581+ "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
675582 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
676− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
677− _ -> Nothing
583+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
584+ _ -> Nothing
585+
678586
679587-- | Filter data type - part of notification configuration
680−newtype Filter = Filter
588+data Filter = Filter
681589 { fFilter :: FilterKey
682− }
683− deriving stock (Show, Eq)
590+ } deriving (Show, Eq)
684591
685592-- | defaultFilter is empty, used to create a notification
686593-- configuration.
@@ -688,10 +595,9 @@ defaultFilter :: Filter
688595defaultFilter = Filter defaultFilterKey
689596
690597-- | FilterKey contains FilterRules, and is part of a Filter.
691−newtype FilterKey = FilterKey
598+data FilterKey = FilterKey
692599 { fkKey :: FilterRules
693− }
694− deriving stock (Show, Eq)
600+ } deriving (Show, Eq)
695601
696602-- | defaultFilterKey is empty, used to create notification
697603-- configuration.
@@ -699,16 +605,16 @@ defaultFilterKey :: FilterKey
699605defaultFilterKey = FilterKey defaultFilterRules
700606
701607-- | FilterRules represents a collection of `FilterRule`s.
702−newtype FilterRules = FilterRules
608+data FilterRules = FilterRules
703609 { frFilterRules :: [FilterRule]
704− }
705− deriving stock (Show, Eq)
610+ } deriving (Show, Eq)
706611
707612-- | defaultFilterRules is empty, used to create notification
708613-- configuration.
709614defaultFilterRules :: FilterRules
710615defaultFilterRules = FilterRules []
711616
617+
712618-- | A filter rule that can act based on the suffix or prefix of an
713619-- object. As an example, let's create two filter rules:
714620--
@@ -719,10 +625,9 @@ defaultFilterRules = FilterRules []
719625-- for objects having a suffix of ".jpg", and the @prefixRule@
720626-- restricts it to objects having a prefix of "images/".
721627data FilterRule = FilterRule
722− { frName :: Text,
723− frValue :: Text
724− }
725− deriving stock (Show, Eq)
628+ { frName :: Text
629+ , frValue :: Text
630+ } deriving (Show, Eq)
726631
727632-- | Arn is an alias of Text
728633type Arn = Text
@@ -731,12 +636,11 @@ type Arn = Text
731636-- notification system. It could represent a Queue, Topic or Lambda
732637-- Function configuration.
733638data NotificationConfig = NotificationConfig
734− { ncId :: Text,
735− ncArn :: Arn,
736− ncEvents :: [Event],
737− ncFilter :: Filter
738− }
739− deriving stock (Show, Eq)
639+ { ncId :: Text
640+ , ncArn :: Arn
641+ , ncEvents :: [Event]
642+ , ncFilter :: Filter
643+ } deriving (Show, Eq)
740644
741645-- | A data-type to represent bucket notification configuration. It is
742646-- a collection of queue, topic or lambda function configurations. The
@@ -744,16 +648,16 @@ data NotificationConfig = NotificationConfig
744648-- described at
745649-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
746650data Notification = Notification
747− { nQueueConfigurations :: [NotificationConfig],
748− nTopicConfigurations :: [NotificationConfig],
749− nCloudFunctionConfigurations :: [NotificationConfig]
750− }
751− deriving stock (Show, Eq)
651+ { nQueueConfigurations :: [NotificationConfig]
652+ , nTopicConfigurations :: [NotificationConfig]
653+ , nCloudFunctionConfigurations :: [NotificationConfig]
654+ } deriving (Eq, Show)
752655
753656-- | The default notification configuration is empty.
754657defaultNotification :: Notification
755658defaultNotification = Notification [] [] []
756659
660+
757661--------------------------------------------------------------------------
758662-- Select API Related Types
759663--------------------------------------------------------------------------
@@ -761,42 +665,38 @@ defaultNotification = Notification [] [] []
761665-- | SelectRequest represents the Select API call. Use the
762666-- `selectRequest` function to create a value of this type.
763667data SelectRequest = SelectRequest
764− { srExpression :: Text,
765− srExpressionType :: ExpressionType,
766− srInputSerialization :: InputSerialization,
767− srOutputSerialization :: OutputSerialization,
768− srRequestProgressEnabled :: Maybe Bool
769− }
770− deriving stock (Show, Eq)
668+ { srExpression :: Text
669+ , srExpressionType :: ExpressionType
670+ , srInputSerialization :: InputSerialization
671+ , srOutputSerialization :: OutputSerialization
672+ , srRequestProgressEnabled :: Maybe Bool
673+ } deriving (Eq, Show)
771674
772675data ExpressionType = SQL
773− deriving stock (Show, Eq)
676+ deriving (Eq, Show)
774677
775678-- | InputSerialization represents format information of the input
776679-- object being queried. Use one of the smart constructors such as
777680-- `defaultCsvInput` as a starting value, and add compression info
778681-- using `setInputCompressionType`
779682data InputSerialization = InputSerialization
780− { isCompressionType :: Maybe CompressionType,
781− isFormatInfo :: InputFormatInfo
782− }
783− deriving stock (Show, Eq)
683+ { isCompressionType :: Maybe CompressionType
684+ , isFormatInfo :: InputFormatInfo
685+ } deriving (Eq, Show)
784686
785687-- | Data type representing the compression setting in a Select
786688-- request.
787−data CompressionType
788− = CompressionTypeNone
789− | CompressionTypeGzip
790− | CompressionTypeBzip2
791− deriving stock (Show, Eq)
689+data CompressionType = CompressionTypeNone
690+ | CompressionTypeGzip
691+ | CompressionTypeBzip2
692+ deriving (Eq, Show)
792693
793694-- | Data type representing input object format information in a
794695-- Select request.
795−data InputFormatInfo
796− = InputFormatCSV CSVInputProp
797− | InputFormatJSON JSONInputProp
798− | InputFormatParquet
799− deriving stock (Show, Eq)
696+data InputFormatInfo = InputFormatCSV CSVInputProp
697+ | InputFormatJSON JSONInputProp
698+ | InputFormatParquet
699+ deriving (Eq, Show)
800700
801701-- | defaultCsvInput returns InputSerialization with default CSV
802702-- format, and without any compression setting.
@@ -806,18 +706,14 @@ defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
806706-- | linesJsonInput returns InputSerialization with JSON line based
807707-- format with no compression setting.
808708linesJsonInput :: InputSerialization
809−linesJsonInput =
810− InputSerialization
811− Nothing
812− (InputFormatJSON $ JSONInputProp JSONTypeLines)
709+linesJsonInput = InputSerialization Nothing
710+ (InputFormatJSON $ JSONInputProp JSONTypeLines)
813711
814712-- | documentJsonInput returns InputSerialization with JSON document
815713-- based format with no compression setting.
816714documentJsonInput :: InputSerialization
817−documentJsonInput =
818− InputSerialization
819− Nothing
820− (InputFormatJSON $ JSONInputProp JSONTypeDocument)
715+documentJsonInput = InputSerialization Nothing
716+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)
821717
822718-- | defaultParquetInput returns InputSerialization with Parquet
823719-- format, and no compression setting.
@@ -826,14 +722,12 @@ defaultParquetInput = InputSerialization Nothing InputFormatParquet
826722
827723-- | setInputCompressionType sets the compression type for the input
828724-- of the SelectRequest
829−setInputCompressionType ::
830− CompressionType ->
831− SelectRequest ->
832− SelectRequest
725+setInputCompressionType :: CompressionType -> SelectRequest
726+ -> SelectRequest
833727setInputCompressionType c i =
834− let is = srInputSerialization i
835− is' = is {isCompressionType = Just c}
836− in i {srInputSerialization = is'}
728+ let is = srInputSerialization i
729+ is' = is { isCompressionType = Just c }
730+ in i { srInputSerialization = is' }
837731
838732-- | defaultCsvOutput returns OutputSerialization with default CSV
839733-- format.
@@ -850,42 +744,40 @@ defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
850744-- SelectRequest with the SQL query text given by @query@, the input
851745-- serialization settings (compression format and format information)
852746-- @inputSer@ and the output serialization settings @outputSer@.
853−selectRequest ::
854− Text ->
855− InputSerialization ->
856− OutputSerialization ->
857− SelectRequest
747+selectRequest :: Text -> InputSerialization -> OutputSerialization
748+ -> SelectRequest
858749selectRequest sqlQuery inputSer outputSer =
859− SelectRequest
860− { srExpression = sqlQuery,
861− srExpressionType = SQL,
862− srInputSerialization = inputSer,
863− srOutputSerialization = outputSer,
864− srRequestProgressEnabled = Nothing
865− }
750+ SelectRequest { srExpression = sqlQuery
751+ , srExpressionType = SQL
752+ , srInputSerialization = inputSer
753+ , srOutputSerialization = outputSer
754+ , srRequestProgressEnabled = Nothing
755+ }
866756
867757-- | setRequestProgressEnabled sets the flag for turning on progress
868758-- messages when the Select response is being streamed back to the
869759-- client.
870760setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
871761setRequestProgressEnabled enabled sr =
872− sr {srRequestProgressEnabled = Just enabled}
762+ sr { srRequestProgressEnabled = Just enabled }
873763
874764type CSVInputProp = CSVProp
875765
876766-- | CSVProp represents CSV format properties. It is built up using
877767-- the Monoid instance.
878−newtype CSVProp = CSVProp (H.HashMap Text Text)
879− deriving stock (Show, Eq)
768+data CSVProp = CSVProp (H.HashMap Text Text)
769+ deriving (Eq, Show)
880770
771+#if (__GLASGOW_HASKELL__ >= 804)
881772instance Semigroup CSVProp where
882− (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
773+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
774+#endif
883775
884776instance Monoid CSVProp where
885− mempty = CSVProp mempty
886−
887−csvPropsList :: CSVProp -> [(Text, Text)]
888−csvPropsList (CSVProp h) = sort $ H.toList h
777+ mempty = CSVProp mempty
778+#if (__GLASGOW_HASKELL__ < 804)
779+ mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
780+#endif
889781
890782defaultCSVProp :: CSVProp
891783defaultCSVProp = mempty
@@ -909,21 +801,18 @@ quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
909801-- | FileHeaderInfo specifies information about column headers for CSV
910802-- format.
911803data FileHeaderInfo
912− = -- | No column headers are present
913− FileHeaderNone
914− | -- | Headers are present and they should be used
915− FileHeaderUse
916− | -- | Header are present, but should be ignored
917− FileHeaderIgnore
918− deriving stock (Show, Eq)
804+ = FileHeaderNone -- ^ No column headers are present
805+ | FileHeaderUse -- ^ Headers are present and they should be used
806+ | FileHeaderIgnore -- ^ Header are present, but should be ignored
807+ deriving (Eq, Show)
919808
920809-- | Specify the CSV file header info property.
921810fileHeaderInfo :: FileHeaderInfo -> CSVProp
922−fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toStr
811+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString
923812 where
924− toStr FileHeaderNone = "NONE"
925− toStr FileHeaderUse = "USE"
926− toStr FileHeaderIgnore = "IGNORE"
813+ toString FileHeaderNone = "NONE"
814+ toString FileHeaderUse = "USE"
815+ toString FileHeaderIgnore = "IGNORE"
927816
928817-- | Specify the CSV comment character property. Lines starting with
929818-- this character are ignored by the server.
@@ -936,69 +825,64 @@ allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter"
936825
937826-- | Set the CSV format properties in the InputSerialization.
938827setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
939−setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
828+setInputCSVProps p is = is { isFormatInfo = InputFormatCSV p }
940829
941830-- | Set the CSV format properties in the OutputSerialization.
942831outputCSVFromProps :: CSVProp -> OutputSerialization
943−outputCSVFromProps = OutputSerializationCSV
832+outputCSVFromProps p = OutputSerializationCSV p
944833
945−newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
946− deriving stock (Show, Eq)
834+data JSONInputProp = JSONInputProp { jsonipType :: JSONType }
835+ deriving (Eq, Show)
947836
948837data JSONType = JSONTypeDocument | JSONTypeLines
949− deriving stock (Show, Eq)
838+ deriving (Eq, Show)
839+
950840
951841-- | OutputSerialization represents output serialization settings for
952842-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
953843-- a starting point.
954−data OutputSerialization
955− = OutputSerializationJSON JSONOutputProp
956− | OutputSerializationCSV CSVOutputProp
957− deriving stock (Show, Eq)
844+data OutputSerialization = OutputSerializationJSON JSONOutputProp
845+ | OutputSerializationCSV CSVOutputProp
846+ deriving (Eq, Show)
958847
959848type CSVOutputProp = CSVProp
960849
961850-- | quoteFields is an output serialization parameter
962851quoteFields :: QuoteFields -> CSVProp
963−quoteFields q = CSVProp $
964− H.singleton "QuoteFields" $
965− case q of
966− QuoteFieldsAsNeeded -> "ASNEEDED"
967− QuoteFieldsAlways -> "ALWAYS"
852+quoteFields q = CSVProp $ H.singleton "QuoteFields" $
853+ case q of
854+ QuoteFieldsAsNeeded -> "ASNEEDED"
855+ QuoteFieldsAlways -> "ALWAYS"
968856
969857-- | Represent the QuoteField setting.
970858data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
971− deriving stock (Show, Eq)
859+ deriving (Eq, Show)
972860
973−newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
974− deriving stock (Show, Eq)
861+data JSONOutputProp = JSONOutputProp { jsonopRecordDelimiter :: Maybe Text }
862+ deriving (Eq, Show)
975863
976864-- | Set the output record delimiter for JSON format
977865outputJSONFromRecordDelimiter :: Text -> OutputSerialization
978866outputJSONFromRecordDelimiter t =
979− OutputSerializationJSON (JSONOutputProp $ Just t)
867+ OutputSerializationJSON (JSONOutputProp $ Just t)
980868
981869-- Response related types
982870
983871-- | An EventMessage represents each kind of message received from the server.
984−data EventMessage
985− = ProgressEventMessage Progress
986− | StatsEventMessage Stats
987− | RequestLevelErrorMessage
988− Text
989− -- ^ Error code
990− Text
991− -- ^ Error message
992− | RecordPayloadEventMessage ByteString
993− deriving stock (Show, Eq)
994−
995−data MsgHeaderName
996− = MessageType
997− | EventType
998− | ContentType
999− | ErrorCode
1000− | ErrorMessage
1001− deriving stock (Show, Eq)
872+data EventMessage = ProgressEventMessage { emProgress :: Progress }
873+ | StatsEventMessage { emStats :: Stats }
874+ | RequestLevelErrorMessage { emErrorCode :: Text
875+ , emErrorMessage :: Text
876+ }
877+ | RecordPayloadEventMessage { emPayloadBytes :: ByteString }
878+ deriving (Eq, Show)
879+
880+data MsgHeaderName = MessageType
881+ | EventType
882+ | ContentType
883+ | ErrorCode
884+ | ErrorMessage
885+ deriving (Eq, Show)
1002886
1003887msgHeaderValueType :: Word8
1004888msgHeaderValueType = 7
@@ -1006,12 +890,11 @@ msgHeaderValueType = 7
1006890type MessageHeader = (MsgHeaderName, Text)
1007891
1008892-- | Represent the progress event returned in the Select response.
1009−data Progress = Progress
1010− { pBytesScanned :: Int64,
1011− pBytesProcessed :: Int64,
1012− pBytesReturned :: Int64
1013− }
1014− deriving stock (Show, Eq)
893+data Progress = Progress { pBytesScanned :: Int64
894+ , pBytesProcessed :: Int64
895+ , pBytesReturned :: Int64
896+ }
897+ deriving (Eq, Show)
1015898
1016899-- | Represent the stats event returned at the end of the Select
1017900-- response.
@@ -1024,89 +907,86 @@ type Stats = Progress
1024907-- | Represents different kinds of payload that are used with S3 API
1025908-- requests.
1026909data Payload
1027− = PayloadBS ByteString
1028− | PayloadH Handle Int64 Int64 -- file handle, offset and length
1029− | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
910+ = PayloadBS ByteString
911+ | PayloadH Handle Int64 Int64 -- file handle, offset and length
912+ | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
1030913
1031914defaultPayload :: Payload
1032915defaultPayload = PayloadBS ""
1033916
1034−data AdminReqInfo = AdminReqInfo
1035− { ariMethod :: Method,
1036− ariPayloadHash :: Maybe ByteString,
1037− ariPayload :: Payload,
1038− ariPath :: ByteString,
1039− ariHeaders :: [Header],
1040− ariQueryParams :: Query
917+data AdminReqInfo = AdminReqInfo {
918+ ariMethod :: Method
919+ , ariPayloadHash :: Maybe ByteString
920+ , ariPayload :: Payload
921+ , ariPath :: ByteString
922+ , ariHeaders :: [Header]
923+ , ariQueryParams :: Query
1041924 }
1042925
1043926data S3ReqInfo = S3ReqInfo
1044− { riMethod :: Method,
1045− riBucket :: Maybe Bucket,
1046− riObject :: Maybe Object,
1047− riQueryParams :: Query,
1048− riHeaders :: [Header],
1049− riPayload :: Payload,
1050− riPayloadHash :: Maybe ByteString,
1051− riRegion :: Maybe Region,
1052− riNeedsLocation :: Bool,
1053− riPresignExpirySecs :: Maybe UrlExpiry
927+ { riMethod :: Method
928+ , riBucket :: Maybe Bucket
929+ , riObject :: Maybe Object
930+ , riQueryParams :: Query
931+ , riHeaders :: [Header]
932+ , riPayload :: Payload
933+ , riPayloadHash :: Maybe ByteString
934+ , riRegion :: Maybe Region
935+ , riNeedsLocation :: Bool
1054936 }
1055937
1056938defaultS3ReqInfo :: S3ReqInfo
1057−defaultS3ReqInfo =
1058− S3ReqInfo
1059− HT.methodGet
1060− Nothing
1061− Nothing
1062− []
1063− []
1064− defaultPayload
1065− Nothing
1066− Nothing
1067− True
1068− Nothing
939+defaultS3ReqInfo = S3ReqInfo HT.methodGet Nothing Nothing
940+ [] [] defaultPayload Nothing Nothing True
1069941
1070942getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
1071943getS3Path b o =
1072− let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
1073− in B.concat ["/", B.intercalate "/" segments]
944+ let segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
945+ in
946+ B.concat ["/", B.intercalate "/" segments]
947+
948+-- | Time to expire for a presigned URL. It interpreted as a number of
949+-- seconds. The maximum duration that can be specified is 7 days.
950+type UrlExpiry = Int
1074951
1075952type RegionMap = H.HashMap Bucket Region
1076953
1077954-- | The Minio Monad - all computations accessing object storage
1078955-- happens in it.
1079−newtype Minio a = Minio
1080− { unMinio :: ReaderT MinioConn (ResourceT IO) a
956+newtype Minio a = Minio {
957+ unMinio :: ReaderT MinioConn (ResourceT IO) a
1081958 }
1082− deriving newtype
1083− ( Functor,
1084− Applicative,
1085− Monad,
1086− MonadIO,
1087− MonadReader MinioConn,
1088− MonadResource,
1089− MonadUnliftIO
959+ deriving (
960+ Functor
961+ , Applicative
962+ , Monad
963+ , MonadIO
964+ , MonadReader MinioConn
965+ , MonadResource
1090966 )
1091967
968+instance MonadUnliftIO Minio where
969+ askUnliftIO = Minio $ ReaderT $ \r ->
970+ withUnliftIO $ \u ->
971+ return (UnliftIO (unliftIO u . flip runReaderT r . unMinio))
972+
1092973-- | MinioConn holds connection info and a connection pool to allow
1093974-- for efficient resource re-use.
1094975data MinioConn = MinioConn
1095− { mcConnInfo :: ConnectInfo,
1096− mcConnManager :: NC.Manager,
1097− mcRegionMap :: MVar RegionMap
976+ { mcConnInfo :: ConnectInfo
977+ , mcConnManager :: NC.Manager
978+ , mcRegionMap :: MVar RegionMap
1098979 }
1099980
1100981class HasSvcNamespace env where
1101982 getSvcNamespace :: env -> Text
1102983
1103984instance HasSvcNamespace MinioConn where
1104− getSvcNamespace env =
1105− let host = connectHost $ mcConnInfo env
1106− in ( if host == "storage.googleapis.com"
1107− then "http://doc.s3.amazonaws.com/2006-03-01"
1108− else "http://s3.amazonaws.com/doc/2006-03-01/"
1109− )
985+ getSvcNamespace env = let host = connectHost $ mcConnInfo env
986+ in if | host == "storage.googleapis.com" ->
987+ "http://doc.s3.amazonaws.com/2006-03-01"
988+ | otherwise ->
989+ "http://s3.amazonaws.com/doc/2006-03-01/"
1110990
1111991-- | Takes connection information and returns a connection object to
1112992-- be passed to 'runMinio'. The returned value can be kept in the
@@ -1114,16 +994,11 @@ instance HasSvcNamespace MinioConn where
1114994-- object storage is accessed.
1115995connect :: ConnectInfo -> IO MinioConn
1116996connect ci = do
1117− let settings
1118− | connectIsSecure ci && connectDisableTLSCertValidation ci =
1119− let badTlsSettings =
1120− case def of
1121− tlsSettings@Conn.TLSSettingsSimple {} ->
1122− tlsSettings {Conn.settingDisableCertificateValidation = True}
1123− _ -> def
1124− in TLS.mkManagerSettings badTlsSettings Nothing
1125− | connectIsSecure ci = NC.tlsManagerSettings
1126− | otherwise = defaultManagerSettings
997+ let settings | connectIsSecure ci && connectDisableTLSCertValidation ci =
998+ let badTlsSettings = Conn.TLSSettingsSimple True False False
999+ in TLS.mkManagerSettings badTlsSettings Nothing
1000+ | connectIsSecure ci = NC.tlsManagerSettings
1001+ | otherwise = defaultManagerSettings
11271002 mgr <- NC.newManager settings
11281003 mkMinioConn ci mgr
11291004
@@ -1137,8 +1012,8 @@ runMinioWith conn m = runResourceT $ runMinioResWith conn m
11371012-- `MinioConn`.
11381013mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
11391014mkMinioConn ci mgr = do
1140− rMapMVar <- M.newMVar H.empty
1141− return $ MinioConn ci mgr rMapMVar
1015+ rMapMVar <- M.newMVar H.empty
1016+ return $ MinioConn ci mgr rMapMVar
11421017
11431018-- | Run the Minio action and return the result or an error.
11441019runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
@@ -1151,12 +1026,12 @@ runMinio ci m = do
11511026runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)
11521027runMinioResWith conn m =
11531028 flip runReaderT conn . unMinio $
1154− fmap Right m
1155− `U.catches` [ U.Handler handlerServiceErr,
1156− U.Handler handlerHE,
1157− U.Handler handlerFE,
1158− U.Handler handlerValidation
1159− ]
1029+ fmap Right m `U.catches`
1030+ [ U.Handler handlerServiceErr
1031+ , U.Handler handlerHE
1032+ , U.Handler handlerFE
1033+ , U.Handler handlerValidation
1034+ ]
11601035 where
11611036 handlerServiceErr = return . Left . MErrService
11621037 handlerHE = return . Left . MErrHTTP
@@ -1170,22 +1045,9 @@ runMinioRes ci m = do
11701045 conn <- liftIO $ connect ci
11711046 runMinioResWith conn m
11721047
1048+s3Name :: Text -> Text -> Name
1049+s3Name ns s = Name s (Just ns) Nothing
1050+
11731051-- | Format as per RFC 1123.
11741052formatRFC1123 :: UTCTime -> T.Text
11751053formatRFC1123 = 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+18−19
@@ -13,21 +13,23 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE FlexibleInstances #-}
1716
17+{-# LANGUAGE FlexibleInstances #-}
1818module Network.Minio.Data.ByteString
19− ( stripBS,
20− UriEncodable (..),
21− )
22−where
19+ (
20+ stripBS
21+ , UriEncodable(..)
22+ ) where
2323
2424import qualified Data.ByteString as B
2525import qualified Data.ByteString.Builder as BB
2626import qualified Data.ByteString.Char8 as BC8
2727import qualified Data.ByteString.Lazy as LB
28−import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace, toUpper)
28+import Data.Char (isSpace, toUpper, isAsciiUpper, isAsciiLower, isDigit)
2929import qualified Data.Text as T
30−import Numeric (showHex)
30+import Numeric (showHex)
31+
32+import Lib.Prelude
3133
3234stripBS :: ByteString -> ByteString
3335stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
@@ -37,10 +39,8 @@ class UriEncodable s where
3739
3840instance UriEncodable [Char] where
3941 uriEncode encodeSlash payload =
40− LB.toStrict $
41− BB.toLazyByteString $
42− mconcat $
43− map (`uriEncodeChar` encodeSlash) payload
42+ LB.toStrict $ BB.toLazyByteString $ mconcat $
43+ map (`uriEncodeChar` encodeSlash) payload
4444
4545instance UriEncodable ByteString where
4646 -- assumes that uriEncode is passed ASCII encoded strings.
@@ -59,17 +59,16 @@ uriEncodeChar '/' True = BB.byteString "%2F"
5959uriEncodeChar '/' False = BB.char7 '/'
6060uriEncodeChar ch _
6161 | isAsciiUpper ch
62− || isAsciiLower ch
63− || isDigit ch
64− || (ch == '_')
65− || (ch == '-')
66− || (ch == '.')
67− || (ch == '~') =
68− BB.char7 ch
62+ || isAsciiLower ch
63+ || isDigit ch
64+ || (ch == '_')
65+ || (ch == '-')
66+ || (ch == '.')
67+ || (ch == '~') = BB.char7 ch
6968 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
7069 where
7170 f :: Word8 -> BB.Builder
7271 f n = BB.char7 '%' <> BB.string7 hexStr
7372 where
7473 hexStr = map toUpper $ showHex q $ showHex r ""
75− (q, r) = divMod n (16 :: Word8)
74+ (q, r) = divMod (fromIntegral n) (16::Word8)
src/Network/Minio/Data/Crypto.hs+33−32
@@ -15,54 +15,55 @@
1515--
1616
1717module Network.Minio.Data.Crypto
18− ( hashSHA256,
19− hashSHA256FromSource,
20− hashMD5,
21− hashMD5ToBase64,
22− hashMD5FromSource,
23− hmacSHA256,
24− hmacSHA256RawBS,
25− digestToBS,
26− digestToBase16,
27− encodeToBase64,
28− )
29−where
30−
31−import Crypto.Hash
32− ( Digest,
33− MD5 (..),
34− SHA256 (..),
35− hashWith,
36− )
37−import Crypto.Hash.Conduit (sinkHash)
38−import Crypto.MAC.HMAC (HMAC, hmac)
39−import Data.ByteArray (ByteArrayAccess, convert)
40−import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
41−import qualified Data.Conduit as C
18+ (
19+ hashSHA256
20+ , hashSHA256FromSource
21+
22+ , hashMD5
23+ , hashMD5ToBase64
24+ , hashMD5FromSource
25+
26+ , hmacSHA256
27+ , hmacSHA256RawBS
28+ , digestToBS
29+ , digestToBase16
30+
31+ , encodeToBase64
32+ ) where
33+
34+import Crypto.Hash (Digest, MD5 (..), SHA256 (..),
35+ hashWith)
36+import Crypto.Hash.Conduit (sinkHash)
37+import Crypto.MAC.HMAC (HMAC, hmac)
38+import Data.ByteArray (ByteArrayAccess, convert)
39+import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
40+import qualified Data.Conduit as C
41+
42+import Lib.Prelude
4243
4344hashSHA256 :: ByteString -> ByteString
4445hashSHA256 = digestToBase16 . hashWith SHA256
4546
46−hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
47+hashSHA256FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
4748hashSHA256FromSource src = do
4849 digest <- C.connect src sinkSHA256Hash
4950 return $ digestToBase16 digest
5051 where
5152 -- To help with type inference
52− sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
53+ sinkSHA256Hash :: Monad m => C.ConduitM ByteString Void m (Digest SHA256)
5354 sinkSHA256Hash = sinkHash
5455
5556-- Returns MD5 hash hex encoded.
5657hashMD5 :: ByteString -> ByteString
5758hashMD5 = digestToBase16 . hashWith MD5
5859
59−hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
60+hashMD5FromSource :: Monad m => C.ConduitM () ByteString m () -> m ByteString
6061hashMD5FromSource src = do
6162 digest <- C.connect src sinkMD5Hash
6263 return $ digestToBase16 digest
6364 where
6465 -- To help with type inference
65− sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
66+ sinkMD5Hash :: Monad m => C.ConduitM ByteString Void m (Digest MD5)
6667 sinkMD5Hash = sinkHash
6768
6869hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -71,15 +72,15 @@ hmacSHA256 message key = hmac key message
7172hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
7273hmacSHA256RawBS message key = convert $ hmacSHA256 message key
7374
74−digestToBS :: (ByteArrayAccess a) => a -> ByteString
75+digestToBS :: ByteArrayAccess a => a -> ByteString
7576digestToBS = convert
7677
77−digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
78+digestToBase16 :: ByteArrayAccess a => a -> ByteString
7879digestToBase16 = convertToBase Base16
7980
8081-- Returns MD5 hash base 64 encoded.
81−hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
82+hashMD5ToBase64 :: ByteArrayAccess a => a -> ByteString
8283hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
8384
84−encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
85+encodeToBase64 :: ByteArrayAccess a => a -> ByteString
8586encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+13−17
@@ -15,24 +15,20 @@
1515--
1616
1717module Network.Minio.Data.Time
18− ( awsTimeFormat,
19− awsTimeFormatBS,
20− awsDateFormat,
21− awsDateFormatBS,
22− awsParseTime,
23− iso8601TimeFormat,
24− UrlExpiry,
25− )
26−where
27−
28−import Data.ByteString.Char8 (pack)
18+ (
19+ awsTimeFormat
20+ , awsTimeFormatBS
21+ , awsDateFormat
22+ , awsDateFormatBS
23+ , awsParseTime
24+ , iso8601TimeFormat
25+ ) where
26+
27+
28+import Data.ByteString.Char8 (pack)
2929import qualified Data.Time as Time
30−import Data.Time.Format.ISO8601 (iso8601Show)
31−import Lib.Prelude
3230
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
31+import Lib.Prelude
3632
3733awsTimeFormat :: UTCTime -> [Char]
3834awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
@@ -50,4 +46,4 @@ awsParseTime :: [Char] -> Maybe UTCTime
5046awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
5147
5248iso8601TimeFormat :: UTCTime -> [Char]
53−iso8601TimeFormat = iso8601Show
49+iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
src/Network/Minio/Errors.hs+52−59
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -14,83 +14,76 @@
1414-- limitations under the License.
1515--
1616
17−module Network.Minio.Errors
18− ( MErrV (..),
19− ServiceErr (..),
20− MinioErr (..),
21− toServiceErr,
22− )
23−where
17+module Network.Minio.Errors where
2418
25−import Control.Exception (IOException)
19+import Control.Exception
2620import qualified Network.HTTP.Conduit as NC
2721
22+import Lib.Prelude
23+
24+
2825---------------------------------
2926-- Errors
3027---------------------------------
31−
3228-- | Various validation errors
33−data MErrV
34− = MErrVSinglePUTSizeExceeded Int64
35− | MErrVPutSizeExceeded Int64
36− | MErrVETagHeaderNotFound
37− | MErrVInvalidObjectInfoResponse
38− | MErrVInvalidSrcObjSpec Text
39− | MErrVInvalidSrcObjByteRange (Int64, Int64)
40− | MErrVCopyObjSingleNoRangeAccepted
41− | MErrVRegionNotSupported Text
42− | MErrVXmlParse Text
43− | MErrVInvalidBucketName Text
44− | MErrVInvalidObjectName Text
45− | MErrVInvalidUrlExpiry Int
46− | MErrVJsonParse Text
47− | MErrVInvalidHealPath
48− | MErrVMissingCredentials
49− | MErrVInvalidEncryptionKeyLength
50− | MErrVStreamingBodyUnexpectedEOF
51− | MErrVUnexpectedPayload
52− | MErrVSTSEndpointNotFound
53− deriving stock (Show, Eq)
29+data MErrV = MErrVSinglePUTSizeExceeded Int64
30+ | MErrVPutSizeExceeded Int64
31+ | MErrVETagHeaderNotFound
32+ | MErrVInvalidObjectInfoResponse
33+ | MErrVInvalidSrcObjSpec Text
34+ | MErrVInvalidSrcObjByteRange (Int64, Int64)
35+ | MErrVCopyObjSingleNoRangeAccepted
36+ | MErrVRegionNotSupported Text
37+ | MErrVXmlParse Text
38+ | MErrVInvalidBucketName Text
39+ | MErrVInvalidObjectName Text
40+ | MErrVInvalidUrlExpiry Int
41+ | MErrVJsonParse Text
42+ | MErrVInvalidHealPath
43+ | MErrVMissingCredentials
44+ | MErrVInvalidEncryptionKeyLength
45+ | MErrVStreamingBodyUnexpectedEOF
46+ | MErrVUnexpectedPayload
47+ deriving (Show, Eq)
5448
5549instance Exception MErrV
5650
5751-- | Errors returned by S3 compatible service
58−data ServiceErr
59− = BucketAlreadyExists
60− | BucketAlreadyOwnedByYou
61− | NoSuchBucket
62− | InvalidBucketName
63− | NoSuchKey
64− | SelectErr Text Text
65− | ServiceErr Text Text
66− deriving stock (Show, Eq)
52+data ServiceErr = BucketAlreadyExists
53+ | BucketAlreadyOwnedByYou
54+ | NoSuchBucket
55+ | InvalidBucketName
56+ | NoSuchKey
57+ | SelectErr Text Text
58+ | ServiceErr Text Text
59+ deriving (Show, Eq)
6760
6861instance Exception ServiceErr
6962
7063toServiceErr :: Text -> Text -> ServiceErr
71−toServiceErr "NoSuchKey" _ = NoSuchKey
72−toServiceErr "NoSuchBucket" _ = NoSuchBucket
73−toServiceErr "InvalidBucketName" _ = InvalidBucketName
64+toServiceErr "NoSuchKey" _ = NoSuchKey
65+toServiceErr "NoSuchBucket" _ = NoSuchBucket
66+toServiceErr "InvalidBucketName" _ = InvalidBucketName
7467toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
75−toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
76−toServiceErr code message = ServiceErr code message
68+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
69+toServiceErr code message = ServiceErr code message
70+
7771
7872-- | Errors thrown by the library
79−data MinioErr
80− = MErrHTTP NC.HttpException
81− | MErrIO IOException
82− | MErrService ServiceErr
83− | MErrValidation MErrV
84− deriving stock (Show)
73+data MinioErr = MErrHTTP NC.HttpException
74+ | MErrIO IOException
75+ | MErrService ServiceErr
76+ | MErrValidation MErrV
77+ deriving (Show)
8578
8679instance Eq MinioErr where
87− MErrHTTP _ == MErrHTTP _ = True
88− MErrHTTP _ == _ = False
89− MErrIO _ == MErrIO _ = True
90− MErrIO _ == _ = False
91− MErrService a == MErrService b = a == b
92− MErrService _ == _ = False
93− MErrValidation a == MErrValidation b = a == b
94− MErrValidation _ == _ = False
80+ MErrHTTP _ == MErrHTTP _ = True
81+ MErrHTTP _ == _ = False
82+ MErrIO _ == MErrIO _ = True
83+ MErrIO _ == _ = False
84+ MErrService a == MErrService b = a == b
85+ MErrService _ == _ = False
86+ MErrValidation a == MErrValidation b = a == b
87+ MErrValidation _ == _ = False
9588
9689instance Exception MinioErr
src/Network/Minio/JsonParser.hs+16−23
@@ -15,35 +15,28 @@
1515--
1616
1717module Network.Minio.JsonParser
18− ( parseErrResponseJSON,
19− )
20−where
18+ (
19+ parseErrResponseJSON
20+ ) where
2121
22−import Data.Aeson
23− ( FromJSON,
24− eitherDecode,
25− parseJSON,
26− withObject,
27− (.:),
28− )
29−import qualified Data.Text as T
30−import Lib.Prelude
31−import Network.Minio.Errors
22+import Data.Aeson (FromJSON, eitherDecode, parseJSON,
23+ withObject, (.:))
24+import qualified Data.Text as T
3225
33−data AdminErrJSON = AdminErrJSON
34− { aeCode :: Text,
35− aeMessage :: Text
36− }
37− deriving stock (Eq, Show)
26+import Lib.Prelude
3827
28+import Network.Minio.Errors
29+
30+data AdminErrJSON = AdminErrJSON { aeCode :: Text
31+ , aeMessage :: Text
32+ } deriving (Eq, Show)
3933instance FromJSON AdminErrJSON where
40− parseJSON = withObject "AdminErrJSON" $ \v ->
41− AdminErrJSON
42− <$> v .: "Code"
43− <*> v .: "Message"
34+ parseJSON = withObject "AdminErrJSON" $ \v -> AdminErrJSON
35+ <$> v .: "Code"
36+ <*> v .: "Message"
4437
4538parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
4639parseErrResponseJSON jsondata =
4740 case eitherDecode jsondata of
4841 Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
49− Left err -> throwIO $ MErrVJsonParse $ T.pack err
42+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs+38−98
@@ -16,50 +16,20 @@
1616
1717module Network.Minio.ListOps where
1818
19−import qualified Data.Conduit as C
19+import qualified Data.Conduit as C
2020import qualified Data.Conduit.Combinators as CC
21−import qualified Data.Conduit.List as CL
22−import Network.Minio.Data
23− ( Bucket,
24− ListObjectsResult
25− ( lorCPrefixes,
26− lorHasMore,
27− lorNextToken,
28− lorObjects
29− ),
30− ListObjectsV1Result
31− ( lorCPrefixes',
32− lorHasMore',
33− lorNextMarker,
34− lorObjects'
35− ),
36− ListPartsResult (lprHasMore, lprNextPart, lprParts),
37− ListUploadsResult
38− ( lurHasMore,
39− lurNextKey,
40− lurNextUpload,
41− lurUploads
42− ),
43− Minio,
44− Object,
45− ObjectInfo,
46− ObjectPartInfo (opiSize),
47− UploadId,
48− UploadInfo (UploadInfo),
49− )
50−import Network.Minio.S3API
51− ( listIncompleteParts',
52− listIncompleteUploads',
53− listObjects',
54− listObjectsV1',
55− )
21+import qualified Data.Conduit.List as CL
22+
23+import Lib.Prelude
24+
25+import Network.Minio.Data
26+import Network.Minio.S3API
5627
5728-- | Represents a list output item - either an object or an object
5829-- prefix (i.e. a directory).
59−data ListItem
60− = ListItemObject ObjectInfo
61− | ListItemPrefix Text
62− deriving stock (Show, Eq)
30+data ListItem = ListItemObject ObjectInfo
31+ | ListItemPrefix Text
32+ deriving (Show, Eq)
6333
6434-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
6535-- similar to a file system tree traversal.
@@ -78,103 +48,73 @@ listObjects bucket prefix recurse = loop Nothing
7848 where
7949 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
8050 loop nextToken = do
81− let delimiter = bool (Just "/") Nothing recurse
51+ let
52+ delimiter = bool (Just "/") Nothing recurse
8253
8354 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
8455 CL.sourceList $ map ListItemObject $ lorObjects res
8556 unless recurse $
86− CL.sourceList $
87− map ListItemPrefix $
88− lorCPrefixes res
57+ CL.sourceList $ map ListItemPrefix $ lorCPrefixes res
8958 when (lorHasMore res) $
9059 loop (lorNextToken res)
9160
9261-- | Lists objects - similar to @listObjects@, however uses the older
9362-- V1 AWS S3 API. Prefer @listObjects@ to this.
94−listObjectsV1 ::
95− Bucket ->
96− Maybe Text ->
97− Bool ->
98− C.ConduitM () ListItem Minio ()
63+listObjectsV1 :: Bucket -> Maybe Text -> Bool
64+ -> C.ConduitM () ListItem Minio ()
9965listObjectsV1 bucket prefix recurse = loop Nothing
10066 where
10167 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
10268 loop nextMarker = do
103− let delimiter = bool (Just "/") Nothing recurse
69+ let
70+ delimiter = bool (Just "/") Nothing recurse
10471
10572 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
10673 CL.sourceList $ map ListItemObject $ lorObjects' res
10774 unless recurse $
108− CL.sourceList $
109− map ListItemPrefix $
110− lorCPrefixes' res
75+ CL.sourceList $ map ListItemPrefix $ lorCPrefixes' res
11176 when (lorHasMore' res) $
11277 loop (lorNextMarker res)
11378
11479-- | List incomplete uploads in a bucket matching the given prefix. If
11580-- recurse is set to True incomplete uploads for the given prefix are
11681-- recursively listed.
117−listIncompleteUploads ::
118− Bucket ->
119− Maybe Text ->
120− Bool ->
121− C.ConduitM () UploadInfo Minio ()
82+listIncompleteUploads :: Bucket -> Maybe Text -> Bool
83+ -> C.ConduitM () UploadInfo Minio ()
12284listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
12385 where
12486 loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
12587 loop nextKeyMarker nextUploadIdMarker = do
126− let delimiter = bool (Just "/") Nothing recurse
127−
128− res <-
129− lift $
130− listIncompleteUploads'
131− bucket
132− prefix
133− delimiter
134− nextKeyMarker
135− nextUploadIdMarker
136− Nothing
137−
138− aggrSizes <- lift $
139− forM (lurUploads res) $ \(uKey, uId, _) -> do
140− partInfos <-
141− C.runConduit $
142− listIncompleteParts bucket uKey uId
143− C..| CC.sinkList
144− return $ foldl' (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
88+ let
89+ delimiter = bool (Just "/") Nothing recurse
90+
91+ res <- lift $ listIncompleteUploads' bucket prefix delimiter
92+ nextKeyMarker nextUploadIdMarker Nothing
93+
94+ aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do
95+ partInfos <- C.runConduit $ listIncompleteParts bucket uKey uId
96+ C..| CC.sinkList
97+ return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
14598
14699 CL.sourceList $
147− zipWith
148− ( curry
149− ( \((uKey, uId, uInitTime), size) ->
150− UploadInfo uKey uId uInitTime size
151− )
152− )
153− (lurUploads res)
154− aggrSizes
100+ map (\((uKey, uId, uInitTime), size) ->
101+ UploadInfo uKey uId uInitTime size
102+ ) $ zip (lurUploads res) aggrSizes
155103
156104 when (lurHasMore res) $
157105 loop (lurNextKey res) (lurNextUpload res)
158106
107+
159108-- | List object parts of an ongoing multipart upload for given
160109-- bucket, object and uploadId.
161−listIncompleteParts ::
162− Bucket ->
163− Object ->
164− UploadId ->
165− C.ConduitM () ObjectPartInfo Minio ()
110+listIncompleteParts :: Bucket -> Object -> UploadId
111+ -> C.ConduitM () ObjectPartInfo Minio ()
166112listIncompleteParts bucket object uploadId = loop Nothing
167113 where
168114 loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
169115 loop nextPartMarker = do
170− res <-
171− lift $
172− listIncompleteParts'
173− bucket
174− object
175− uploadId
176− Nothing
177− nextPartMarker
116+ res <- lift $ listIncompleteParts' bucket object uploadId Nothing
117+ nextPartMarker
178118 CL.sourceList $ lprParts res
179119 when (lprHasMore res) $
180120 loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs+152−215
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,51 +13,45 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE CPP #-}
1716
1817module Network.Minio.PresignedOperations
19− ( UrlExpiry,
20− makePresignedUrl,
21− presignedPutObjectUrl,
22− presignedGetObjectUrl,
23− presignedHeadObjectUrl,
24− PostPolicyCondition (..),
25− ppCondBucket,
26− ppCondContentLengthRange,
27− ppCondContentType,
28− ppCondKey,
29− ppCondKeyStartsWith,
30− ppCondSuccessActionStatus,
31− PostPolicy (..),
32− PostPolicyError (..),
33− newPostPolicy,
34− showPostPolicy,
35− presignedPostPolicy,
36− )
37−where
38−
39−import Data.Aeson ((.=))
40−import qualified Data.Aeson as Json
41−import Data.ByteString.Builder (byteString, toLazyByteString)
42−import qualified Data.HashMap.Strict as H
43−import qualified Data.Text as T
44−import qualified Data.Time as Time
45−import Lib.Prelude
46−import qualified Network.HTTP.Client as NClient
47−import qualified Network.HTTP.Types as HT
48−import Network.Minio.API (buildRequest)
49−import Network.Minio.Credentials
50−import Network.Minio.Data
51−import Network.Minio.Data.Time
52−import Network.Minio.Errors
53−import Network.Minio.Sign.V4
54−import Network.URI (uriToString)
55−
56−{- ORMOLU_DISABLE -}
57−#if MIN_VERSION_aeson(2,0,0)
58−import qualified Data.Aeson.Key as A
59−#endif
60−{- ORMOLU_ENABLE -}
18+ ( UrlExpiry
19+ , makePresignedUrl
20+ , presignedPutObjectUrl
21+ , presignedGetObjectUrl
22+ , presignedHeadObjectUrl
23+
24+ , PostPolicyCondition(..)
25+ , ppCondBucket
26+ , ppCondContentLengthRange
27+ , ppCondContentType
28+ , ppCondKey
29+ , ppCondKeyStartsWith
30+ , ppCondSuccessActionStatus
31+
32+ , PostPolicy(..)
33+ , PostPolicyError(..)
34+ , newPostPolicy
35+ , showPostPolicy
36+ , presignedPostPolicy
37+ ) where
38+
39+import Data.Aeson ((.=))
40+import qualified Data.Aeson as Json
41+import Data.ByteString.Builder (byteString, toLazyByteString)
42+import qualified Data.HashMap.Strict as H
43+import qualified Data.Text as T
44+import qualified Data.Time as Time
45+import qualified Network.HTTP.Conduit as NC
46+import qualified Network.HTTP.Types as HT
47+import Network.HTTP.Types.Header (hHost)
48+
49+import Lib.Prelude
50+
51+import Network.Minio.Data
52+import Network.Minio.Data.Time
53+import Network.Minio.Errors
54+import Network.Minio.Sign.V4
6155
6256-- | Generate a presigned URL. This function allows for advanced usage
6357-- - for simple cases prefer the `presigned*Url` functions.
@@ -67,36 +61,42 @@ import qualified Data.Aeson.Key as A
6761--
6862-- All extra query parameters or headers are signed, and therefore are
6963-- required to be sent when the generated URL is actually used.
70−makePresignedUrl ::
71− UrlExpiry ->
72− HT.Method ->
73− Maybe Bucket ->
74− Maybe Object ->
75− Maybe Region ->
76− HT.Query ->
77− HT.RequestHeaders ->
78− Minio ByteString
64+makePresignedUrl :: UrlExpiry -> HT.Method -> Maybe Bucket -> Maybe Object
65+ -> Maybe Region -> HT.Query -> HT.RequestHeaders
66+ -> Minio ByteString
7967makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
80− when (expiry > 7 * 24 * 3600 || expiry < 0) $
81− throwIO $
82− MErrVInvalidUrlExpiry expiry
83−
84− let s3ri =
85− defaultS3ReqInfo
86− { riPresignExpirySecs = Just expiry,
87− riMethod = method,
88− riBucket = bucket,
89− riObject = object,
90− riRegion = region,
91− riQueryParams = extraQuery,
92− riHeaders = extraHeaders
93− }
94−
95− req <- buildRequest s3ri
96− let uri = NClient.getUri req
97− uriString = uriToString identity uri ""
98−
99− return $ encodeUtf8 uriString
68+ when (expiry > 7*24*3600 || expiry < 0) $
69+ throwIO $ MErrVInvalidUrlExpiry expiry
70+
71+ ci <- asks mcConnInfo
72+
73+ let
74+ hostHeader = (hHost, getHostAddr ci)
75+ req = NC.defaultRequest {
76+ NC.method = method
77+ , NC.secure = connectIsSecure ci
78+ , NC.host = encodeUtf8 $ connectHost ci
79+ , NC.port = connectPort ci
80+ , NC.path = getS3Path bucket object
81+ , NC.requestHeaders = hostHeader : extraHeaders
82+ , NC.queryString = HT.renderQuery True extraQuery
83+ }
84+ ts <- liftIO Time.getCurrentTime
85+
86+ let sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
87+ ts region (Just expiry) Nothing
88+
89+ signPairs = signV4 sp req
90+
91+ qpToAdd = (fmap . fmap) Just signPairs
92+ queryStr = HT.renderQueryBuilder True
93+ ((HT.parseQuery $ NC.queryString req) ++ qpToAdd)
94+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
95+
96+ return $ toS $ toLazyByteString $ scheme
97+ <> byteString (getHostAddr ci)
98+ <> byteString (getS3Path bucket object)
99+ <> queryStr
100100
101101-- | Generate a URL with authentication signature to PUT (upload) an
102102-- object. Any extra headers if passed, are signed, and so they are
@@ -105,22 +105,12 @@ makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
105105--
106106-- For a list of possible headers to pass, please refer to the PUT
107107-- object REST API AWS S3 documentation.
108−presignedPutObjectUrl ::
109− Bucket ->
110− Object ->
111− UrlExpiry ->
112− HT.RequestHeaders ->
113− Minio ByteString
108+presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders
109+ -> Minio ByteString
114110presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
115111 region <- asks (Just . connectRegion . mcConnInfo)
116− makePresignedUrl
117− expirySeconds
118− HT.methodPut
119− (Just bucket)
120− (Just object)
121− region
122− []
123− extraHeaders
112+ makePresignedUrl expirySeconds HT.methodPut
113+ (Just bucket) (Just object) region [] extraHeaders
124114
125115-- | Generate a URL with authentication signature to GET (download) an
126116-- object. All extra query parameters and headers passed here will be
@@ -131,23 +121,12 @@ presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
131121--
132122-- For a list of possible request parameters and headers, please refer
133123-- to the GET object REST API AWS S3 documentation.
134−presignedGetObjectUrl ::
135− Bucket ->
136− Object ->
137− UrlExpiry ->
138− HT.Query ->
139− HT.RequestHeaders ->
140− Minio ByteString
124+presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query
125+ -> HT.RequestHeaders -> Minio ByteString
141126presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
142127 region <- asks (Just . connectRegion . mcConnInfo)
143− makePresignedUrl
144− expirySeconds
145− HT.methodGet
146− (Just bucket)
147− (Just object)
148− region
149− extraQuery
150− extraHeaders
128+ makePresignedUrl expirySeconds HT.methodGet
129+ (Just bucket) (Just object) region extraQuery extraHeaders
151130
152131-- | Generate a URL with authentication signature to make a HEAD
153132-- request on an object. This is used to fetch metadata about an
@@ -156,74 +135,50 @@ presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
156135--
157136-- For a list of possible headers to pass, please refer to the HEAD
158137-- object REST API AWS S3 documentation.
159−presignedHeadObjectUrl ::
160− Bucket ->
161− Object ->
162− UrlExpiry ->
163− HT.RequestHeaders ->
164− Minio ByteString
138+presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry
139+ -> HT.RequestHeaders -> Minio ByteString
165140presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
166141 region <- asks (Just . connectRegion . mcConnInfo)
167− makePresignedUrl
168− expirySeconds
169− HT.methodHead
170− (Just bucket)
171− (Just object)
172− region
173− []
174− extraHeaders
142+ makePresignedUrl expirySeconds HT.methodHead
143+ (Just bucket) (Just object) region [] extraHeaders
175144
176145-- | Represents individual conditions in a Post Policy document.
177−data PostPolicyCondition
178− = PPCStartsWith Text Text
179− | PPCEquals Text Text
180− | PPCRange Text Int64 Int64
181− deriving stock (Show, Eq)
146+data PostPolicyCondition = PPCStartsWith Text Text
147+ | PPCEquals Text Text
148+ | PPCRange Text Int64 Int64
149+ deriving (Show, Eq)
182150
183−{- ORMOLU_DISABLE -}
184151instance Json.ToJSON PostPolicyCondition where
185152 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
189153 toJSON (PPCEquals k v) = Json.object [k .= v]
190−#endif
191154 toJSON (PPCRange k minVal maxVal) =
192155 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
193156
194157 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
198158 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
199−#endif
200159 toEncoding (PPCRange k minVal maxVal) =
201160 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202−{- ORMOLU_ENABLE -}
203161
204162-- | A PostPolicy is required to perform uploads via browser forms.
205−data PostPolicy = PostPolicy
206− { expiration :: UTCTime,
207− conditions :: [PostPolicyCondition]
208− }
209− deriving stock (Show, Eq)
163+data PostPolicy = PostPolicy {
164+ expiration :: UTCTime
165+ , conditions :: [PostPolicyCondition]
166+ } deriving (Show, Eq)
210167
211168instance Json.ToJSON PostPolicy where
212169 toJSON (PostPolicy e c) =
213− Json.object
214− [ "expiration" .= iso8601TimeFormat e,
215− "conditions" .= c
216− ]
170+ Json.object $ [ "expiration" .= iso8601TimeFormat e
171+ , "conditions" .= c
172+ ]
217173 toEncoding (PostPolicy e c) =
218174 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
219175
220176-- | Possible validation errors when creating a PostPolicy.
221−data PostPolicyError
222− = PPEKeyNotSpecified
223− | PPEBucketNotSpecified
224− | PPEConditionKeyEmpty
225− | PPERangeInvalid
226− deriving stock (Show, Eq)
177+data PostPolicyError = PPEKeyNotSpecified
178+ | PPEBucketNotSpecified
179+ | PPEConditionKeyEmpty
180+ | PPERangeInvalid
181+ deriving (Eq, Show)
227182
228183-- | Set the bucket name that the upload should use.
229184ppCondBucket :: Bucket -> PostPolicyCondition
@@ -231,10 +186,8 @@ ppCondBucket = PPCEquals "bucket"
231186
232187-- | Set the content length range constraint with minimum and maximum
233188-- byte count values.
234−ppCondContentLengthRange ::
235− Int64 ->
236− Int64 ->
237− PostPolicyCondition
189+ppCondContentLengthRange :: Int64 -> Int64
190+ -> PostPolicyCondition
238191ppCondContentLengthRange = PPCRange "content-length-range"
239192
240193-- | Set the content-type header for the upload.
@@ -257,99 +210,83 @@ ppCondSuccessActionStatus n =
257210
258211-- | This function creates a PostPolicy after validating its
259212-- arguments.
260−newPostPolicy ::
261− UTCTime ->
262− [PostPolicyCondition] ->
263− Either PostPolicyError PostPolicy
213+newPostPolicy :: UTCTime -> [PostPolicyCondition]
214+ -> Either PostPolicyError PostPolicy
264215newPostPolicy expirationTime conds
265216 -- object name condition must be present
266217 | not $ any (keyEquals "key") conds =
267218 Left PPEKeyNotSpecified
219+
268220 -- bucket name condition must be present
269221 | not $ any (keyEquals "bucket") conds =
270222 Left PPEBucketNotSpecified
223+
271224 -- a condition with an empty key is invalid
272225 | any (keyEquals "") conds || any isEmptyRangeKey conds =
273226 Left PPEConditionKeyEmpty
227+
274228 -- invalid range check
275229 | any isInvalidRange conds =
276230 Left PPERangeInvalid
231+
277232 -- all good!
278233 | otherwise =
279234 return $ PostPolicy expirationTime conds
235+
280236 where
281237 keyEquals k' (PPCStartsWith k _) = k == k'
282− keyEquals k' (PPCEquals k _) = k == k'
283− keyEquals _ _ = False
238+ keyEquals k' (PPCEquals k _) = k == k'
239+ keyEquals _ _ = False
240+
284241 isEmptyRangeKey (PPCRange k _ _) = k == ""
285− isEmptyRangeKey _ = False
242+ isEmptyRangeKey _ = False
243+
286244 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
287− isInvalidRange _ = False
245+ isInvalidRange _ = False
288246
289247-- | Convert Post Policy to a string (e.g. for printing).
290248showPostPolicy :: PostPolicy -> ByteString
291−showPostPolicy = toStrictBS . Json.encode
249+showPostPolicy = toS . Json.encode
292250
293251-- | Generate a presigned URL and POST policy to upload files via a
294252-- browser. On success, this function returns a URL and POST
295253-- form-data.
296−presignedPostPolicy ::
297− PostPolicy ->
298− Minio (ByteString, H.HashMap Text ByteString)
254+presignedPostPolicy :: PostPolicy
255+ -> Minio (ByteString, H.HashMap Text ByteString)
299256presignedPostPolicy p = do
300257 ci <- asks mcConnInfo
301− signTime <- liftIO Time.getCurrentTime
302− mgr <- asks mcConnManager
303− cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
304−
305− let extraConditions signParams =
306− [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
307− PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
308− PPCEquals
309− "x-amz-credential"
310− ( T.intercalate
311− "/"
312− [ coerce $ cvAccessKey cv,
313− decodeUtf8 $ credentialScope signParams
314− ]
315− )
316− ]
317− ppWithCreds signParams =
318− p
319− { conditions = conditions p ++ extraConditions signParams
320− }
321− sp =
322− SignParams
323− (coerce $ cvAccessKey cv)
324− (coerce $ cvSecretKey cv)
325− (coerce $ cvSessionToken cv)
326− ServiceS3
327− signTime
328− (Just $ connectRegion ci)
329− Nothing
330− Nothing
331− signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
332− -- compute form-data
333− mkPair (PPCStartsWith k v) = Just (k, v)
334− mkPair (PPCEquals k v) = Just (k, v)
335− mkPair _ = Nothing
336− formFromPolicy =
337− H.map encodeUtf8 $
338− H.fromList $
339− mapMaybe
340− mkPair
341− (conditions $ ppWithCreds sp)
342− formData = formFromPolicy `H.union` signData
343− -- compute POST upload URL
344− bucket = H.lookupDefault "" "bucket" formData
345− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
346− url =
347− toStrictBS $
348− toLazyByteString $
349− scheme
350− <> byteString (getHostAddr ci)
351− <> byteString "/"
352− <> byteString bucket
353− <> byteString "/"
258+ signTime <- liftIO $ Time.getCurrentTime
259+
260+ let
261+ extraConditions =
262+ [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime)
263+ , PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256"
264+ , PPCEquals "x-amz-credential"
265+ (T.intercalate "/" [connectAccessKey ci,
266+ decodeUtf8 $ mkScope signTime region])
267+ ]
268+ ppWithCreds = p {
269+ conditions = conditions p ++ extraConditions
270+ }
271+ sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
272+ signTime (Just $ connectRegion ci) Nothing Nothing
273+ signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
274+
275+
276+ -- compute form-data
277+ mkPair (PPCStartsWith k v) = Just (k, v)
278+ mkPair (PPCEquals k v) = Just (k, v)
279+ mkPair _ = Nothing
280+ formFromPolicy = H.map toS $ H.fromList $ catMaybes $
281+ mkPair <$> conditions ppWithCreds
282+ formData = formFromPolicy `H.union` signData
283+
284+ -- compute POST upload URL
285+ bucket = H.lookupDefault "" "bucket" formData
286+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
287+ region = connectRegion ci
288+
289+ url = toS $ toLazyByteString $ scheme <> byteString (getHostAddr ci) <>
290+ byteString "/" <> byteString (toS bucket) <> byteString "/"
354291
355292 return (url, formData)
src/Network/Minio/PutObject.hs+69−85
@@ -15,24 +15,29 @@
1515--
1616
1717module Network.Minio.PutObject
18− ( putObjectInternal,
19− ObjectData (..),
20− selectPartSizes,
21− )
22−where
23−
24−import Conduit (takeC)
25−import qualified Conduit as C
26−import qualified Data.ByteString.Lazy as LBS
27−import qualified Data.Conduit.Binary as CB
18+ (
19+ putObjectInternal
20+ , ObjectData(..)
21+ , selectPartSizes
22+ ) where
23+
24+
25+import Conduit (takeC)
26+import qualified Conduit as C
27+import qualified Data.ByteString.Lazy as LBS
28+import qualified Data.Conduit.Binary as CB
2829import qualified Data.Conduit.Combinators as CC
29−import qualified Data.Conduit.List as CL
30−import qualified Data.List as List
31−import Lib.Prelude
32−import Network.Minio.Data
33−import Network.Minio.Errors
34−import Network.Minio.S3API
35−import Network.Minio.Utils
30+import qualified Data.Conduit.List as CL
31+import qualified Data.List as List
32+
33+
34+import Lib.Prelude
35+
36+import Network.Minio.Data
37+import Network.Minio.Errors
38+import Network.Minio.S3API
39+import Network.Minio.Utils
40+
3641
3742-- | A data-type to represent the source data for an object. A
3843-- file-path or a producer-conduit may be provided.
@@ -45,45 +50,37 @@ import Network.Minio.Utils
4550-- the input - if it is not provided, upload will continue until the
4651-- stream ends or the object reaches `maxObjectSize` size.
4752data ObjectData m
48− = -- | Takes filepath and optional
49− -- size.
50− ODFile FilePath (Maybe Int64)
51− | -- | Pass
52− -- size
53− -- (bytes)
54− -- if
55− -- known.
56− ODStream (C.ConduitM () ByteString m ()) (Maybe Int64)
53+ = ODFile FilePath (Maybe Int64) -- ^ Takes filepath and optional
54+ -- size.
55+ | ODStream (C.ConduitM () ByteString m ()) (Maybe Int64) -- ^ Pass
56+ -- size
57+ -- (bytes)
58+ -- if
59+ -- known.
5760
5861-- | Put an object from ObjectData. This high-level API handles
5962-- objects of all sizes, and even if the object size is unknown.
60−putObjectInternal ::
61− Bucket ->
62− Object ->
63− PutObjectOptions ->
64− ObjectData Minio ->
65− Minio ETag
63+putObjectInternal :: Bucket -> Object -> PutObjectOptions
64+ -> ObjectData Minio -> Minio ETag
6665putObjectInternal b o opts (ODStream src sizeMay) = do
6766 case sizeMay of
6867 -- unable to get size, so assume non-seekable file
6968 Nothing -> sequentialMultipartUpload b o opts Nothing src
69+
7070 -- got file size, so check for single/multipart upload
7171 Just size ->
72− if
73− | size <= 64 * oneMiB -> do
74− bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
75− putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
76− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
77− | otherwise -> sequentialMultipartUpload b o opts (Just size) src
72+ if | size <= 64 * oneMiB -> do
73+ bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
74+ putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
75+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
76+ | otherwise -> sequentialMultipartUpload b o opts (Just size) src
77+
7878putObjectInternal b o opts (ODFile fp sizeMay) = do
7979 hResE <- withNewHandle fp $ \h ->
80− liftA2 (,) (isHandleSeekable h) (getFileSize h)
80+ liftM2 (,) (isHandleSeekable h) (getFileSize h)
8181
82− (isSeekable, handleSizeMay) <-
83− either
84− (const $ return (False, Nothing))
85− return
86− hResE
82+ (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return
83+ hResE
8784
8885 -- prefer given size to queried size.
8986 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
@@ -91,25 +88,18 @@ putObjectInternal b o opts (ODFile fp sizeMay) = do
9188 case finalSizeMay of
9289 -- unable to get size, so assume non-seekable file
9390 Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
91+
9492 -- got file size, so check for single/multipart upload
9593 Just size ->
96− if
97− | size <= 64 * oneMiB ->
98− either throwIO return
99− =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
100− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
101− | isSeekable -> parallelMultipartUpload b o opts fp size
102− | otherwise ->
103− sequentialMultipartUpload b o opts (Just size) $
104− CB.sourceFile fp
105−
106−parallelMultipartUpload ::
107− Bucket ->
108− Object ->
109− PutObjectOptions ->
110− FilePath ->
111− Int64 ->
112− Minio ETag
94+ if | size <= 64 * oneMiB -> either throwIO return =<<
95+ withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
96+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
97+ | isSeekable -> parallelMultipartUpload b o opts fp size
98+ | otherwise -> sequentialMultipartUpload b o opts (Just size) $
99+ CB.sourceFile fp
100+
101+parallelMultipartUpload :: Bucket -> Object -> PutObjectOptions
102+ -> FilePath -> Int64 -> Minio ETag
113103parallelMultipartUpload b o opts filePath size = do
114104 -- get a new upload id.
115105 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -119,17 +109,15 @@ parallelMultipartUpload b o opts filePath size = do
119109 let threads = fromMaybe 10 $ pooNumThreads opts
120110
121111 -- perform upload with 'threads' threads
122− uploadedPartsE <-
123− limitedMapConcurrently
124− (fromIntegral threads)
125− (uploadPart uploadId)
126− partSizeInfo
112+ uploadedPartsE <- limitedMapConcurrently (fromIntegral threads)
113+ (uploadPart uploadId) partSizeInfo
127114
128115 -- if there were any errors, rethrow exception.
129116 mapM_ throwIO $ lefts uploadedPartsE
130117
131118 -- if we get here, all parts were successfully uploaded.
132119 completeMultipartUpload b o uploadId $ rights uploadedPartsE
120+
133121 where
134122 uploadPart uploadId (partNum, offset, sz) =
135123 withNewHandle filePath $ \h -> do
@@ -137,13 +125,10 @@ parallelMultipartUpload b o opts filePath size = do
137125 putObjectPart b o uploadId partNum [] payload
138126
139127-- | Upload multipart object from conduit source sequentially
140−sequentialMultipartUpload ::
141− Bucket ->
142− Object ->
143− PutObjectOptions ->
144− Maybe Int64 ->
145− C.ConduitM () ByteString Minio () ->
146− Minio ETag
128+sequentialMultipartUpload :: Bucket -> Object -> PutObjectOptions
129+ -> Maybe Int64
130+ -> C.ConduitM () ByteString Minio ()
131+ -> Minio ETag
147132sequentialMultipartUpload b o opts sizeMay src = do
148133 -- get a new upload id.
149134 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -151,23 +136,22 @@ sequentialMultipartUpload b o opts sizeMay src = do
151136 -- upload parts in loop
152137 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
153138 (pnums, _, sizes) = List.unzip3 partSizes
154− uploadedParts <-
155− C.runConduit $
156− src
157− C..| chunkBSConduit (map fromIntegral sizes)
158− C..| CL.map PayloadBS
159− C..| uploadPart' uploadId pnums
160− C..| CC.sinkList
139+ uploadedParts <- C.runConduit
140+ $ src
141+ C..| chunkBSConduit (map fromIntegral sizes)
142+ C..| CL.map PayloadBS
143+ C..| uploadPart' uploadId pnums
144+ C..| CC.sinkList
161145
162146 -- complete multipart upload
163147 completeMultipartUpload b o uploadId uploadedParts
148+
164149 where
165150 uploadPart' _ [] = return ()
166− uploadPart' uid (pn : pns) = do
151+ uploadPart' uid (pn:pns) = do
167152 payloadMay <- C.await
168153 case payloadMay of
169154 Nothing -> return ()
170− Just payload -> do
171− pinfo <- lift $ putObjectPart b o uid pn [] payload
172− C.yield pinfo
173− uploadPart' uid pns
155+ Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload
156+ C.yield pinfo
157+ uploadPart' uid pns
src/Network/Minio/S3API.hs+319−481
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -14,185 +14,150 @@
1414-- limitations under the License.
1515--
1616
17−-- |
18−-- Module: Network.Minio.S3API
19−-- Copyright: (c) 2017-2023 MinIO Dev Team
20−-- License: Apache 2.0
21−-- Maintainer: MinIO Dev Team <dev@min.io>
22−--
23−-- Lower-level API for S3 compatible object stores. Start with @Network.Minio@
24−-- and use this only if needed.
2517module Network.Minio.S3API
26− ( Region,
27− getLocation,
28−
29− -- * Listing buckets
30−
31− --------------------
32− getService,
33−
34− -- * Listing objects
35−
36− --------------------
37− ListObjectsResult (..),
38− ListObjectsV1Result (..),
39− listObjects',
40− listObjectsV1',
41−
42− -- * Retrieving buckets
43− headBucket,
44−
45− -- * Retrieving objects
46−
47− -----------------------
48− getObject',
49− headObject,
50−
51− -- * Creating buckets and objects
52−
53− ---------------------------------
54− putBucket,
55− ETag,
56− maxSinglePutObjectSizeBytes,
57− putObjectSingle',
58− putObjectSingle,
59− copyObjectSingle,
60−
61− -- * Multipart Upload APIs
62−
63− --------------------------
64− UploadId,
65− PartTuple,
66− Payload (..),
67− PartNumber,
68− newMultipartUpload,
69− putObjectPart,
70− copyObjectPart,
71− completeMultipartUpload,
72− abortMultipartUpload,
73− ListUploadsResult (..),
74− listIncompleteUploads',
75− ListPartsResult (..),
76− listIncompleteParts',
77−
78− -- * Deletion APIs
79−
80− --------------------------
81− deleteBucket,
82− deleteObject,
83−
84− -- * Presigned Operations
85−
86− -----------------------------
87− module Network.Minio.PresignedOperations,
88−
89− -- ** Bucket Policies
90− getBucketPolicy,
91− setBucketPolicy,
92−
93− -- * Bucket Notifications
94−
95− -------------------------
96− Notification (..),
97− NotificationConfig (..),
98− Arn,
99− Event (..),
100− Filter (..),
101− FilterKey (..),
102− FilterRules (..),
103− FilterRule (..),
104− getBucketNotification,
105− putBucketNotification,
106− removeAllBucketNotification,
107− )
108−where
109−
110−import qualified Data.ByteString as BS
111−import qualified Data.Text as T
112−import Lib.Prelude
113−import qualified Network.HTTP.Conduit as NC
114−import qualified Network.HTTP.Types as HT
115−import Network.HTTP.Types.Status (status404)
116−import Network.Minio.API
117−import Network.Minio.APICommon
118−import Network.Minio.Data
119−import Network.Minio.Errors
120−import Network.Minio.PresignedOperations
121−import Network.Minio.Utils
122−import Network.Minio.XmlGenerator
123−import Network.Minio.XmlParser
124−import UnliftIO (Handler (Handler))
18+ (
19+ Region
20+ , getLocation
21+
22+ -- * Listing buckets
23+ --------------------
24+ , getService
25+
26+ -- * Listing objects
27+ --------------------
28+ , ListObjectsResult(..)
29+ , ListObjectsV1Result(..)
30+ , listObjects'
31+ , listObjectsV1'
32+
33+ -- * Retrieving buckets
34+ , headBucket
35+
36+ -- * Retrieving objects
37+ -----------------------
38+ , getObject'
39+ , headObject
40+
41+ -- * Creating buckets and objects
42+ ---------------------------------
43+ , putBucket
44+ , ETag
45+ , maxSinglePutObjectSizeBytes
46+ , putObjectSingle'
47+ , putObjectSingle
48+ , copyObjectSingle
49+
50+ -- * Multipart Upload APIs
51+ --------------------------
52+ , UploadId
53+ , PartTuple
54+ , Payload(..)
55+ , PartNumber
56+ , newMultipartUpload
57+ , putObjectPart
58+ , copyObjectPart
59+ , completeMultipartUpload
60+ , abortMultipartUpload
61+ , ListUploadsResult(..)
62+ , listIncompleteUploads'
63+ , ListPartsResult(..)
64+ , listIncompleteParts'
65+
66+ -- * Deletion APIs
67+ --------------------------
68+ , deleteBucket
69+ , deleteObject
70+
71+ -- * Presigned Operations
72+ -----------------------------
73+ , module Network.Minio.PresignedOperations
74+
75+ -- ** Bucket Policies
76+ , getBucketPolicy
77+ , setBucketPolicy
78+
79+ -- * Bucket Notifications
80+ -------------------------
81+ , Notification(..)
82+ , NotificationConfig(..)
83+ , Arn
84+ , Event(..)
85+ , Filter(..)
86+ , FilterKey(..)
87+ , FilterRules(..)
88+ , FilterRule(..)
89+ , getBucketNotification
90+ , putBucketNotification
91+ , removeAllBucketNotification
92+ ) where
93+
94+import qualified Data.ByteString as BS
95+import qualified Data.Text as T
96+import qualified Network.HTTP.Conduit as NC
97+import qualified Network.HTTP.Types as HT
98+import Network.HTTP.Types.Status (status404)
99+import UnliftIO (Handler (Handler))
100+
101+import Lib.Prelude
102+
103+import Network.Minio.API
104+import Network.Minio.APICommon
105+import Network.Minio.Data
106+import Network.Minio.Errors
107+import Network.Minio.PresignedOperations
108+import Network.Minio.Utils
109+import Network.Minio.XmlGenerator
110+import Network.Minio.XmlParser
125111
126112-- | Fetch all buckets from the service.
127113getService :: Minio [BucketInfo]
128114getService = do
129− resp <-
130− executeRequest $
131− defaultS3ReqInfo
132− { riNeedsLocation = False
133− }
115+ resp <- executeRequest $ defaultS3ReqInfo {
116+ riNeedsLocation = False
117+ }
134118 parseListBuckets $ NC.responseBody resp
135119
136120-- Parse headers from getObject and headObject calls.
137121parseGetObjectHeaders :: Object -> [HT.Header] -> Maybe ObjectInfo
138122parseGetObjectHeaders object headers =
139− let metadataPairs = getMetadata headers
140− userMetadata = getUserMetadataMap metadataPairs
141− metadata = getNonUserMetadataMap metadataPairs
142− in ObjectInfo
143− <$> Just object
144− <*> getLastModifiedHeader headers
145− <*> getETagHeader headers
146− <*> getContentLength headers
147− <*> Just userMetadata
148− <*> Just metadata
123+ let metadataPairs = getMetadata headers
124+ userMetadata = getUserMetadataMap metadataPairs
125+ metadata = getNonUserMetadataMap metadataPairs
126+ in ObjectInfo <$> Just object
127+ <*> getLastModifiedHeader headers
128+ <*> getETagHeader headers
129+ <*> getContentLength headers
130+ <*> Just userMetadata
131+ <*> Just metadata
149132
150133-- | GET an object from the service and return parsed ObjectInfo and a
151134-- conduit source for the object content
152−getObject' ::
153− Bucket ->
154− Object ->
155− HT.Query ->
156− [HT.Header] ->
157− Minio GetObjectResponse
135+getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
136+ -> Minio GetObjectResponse
158137getObject' bucket object queryParams headers = do
159− resp <- mkStreamRequest reqInfo
160− let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
161− objInfo <-
162− maybe
163− (throwIO MErrVInvalidObjectInfoResponse)
164− return
165− objInfoMaybe
166− return $
167− GetObjectResponse
168− { gorObjectInfo = objInfo,
169− gorObjectStream = NC.responseBody resp
170− }
138+ resp <- mkStreamRequest reqInfo
139+ let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
140+ objInfo <- maybe (throwIO MErrVInvalidObjectInfoResponse) return
141+ objInfoMaybe
142+ return $ GetObjectResponse { gorObjectInfo = objInfo
143+ , gorObjectStream = NC.responseBody resp
144+ }
171145 where
172− reqInfo =
173− defaultS3ReqInfo
174− { riBucket = Just bucket,
175− riObject = Just object,
176− riQueryParams = queryParams,
177− riHeaders =
178− headers
179− -- This header is required for safety as otherwise http-client,
180− -- sends Accept-Encoding: gzip, and the server may actually gzip
181− -- body. In that case Content-Length header will be missing.
182− <> [("Accept-Encoding", "identity")]
183− }
146+ reqInfo = defaultS3ReqInfo { riBucket = Just bucket
147+ , riObject = Just object
148+ , riQueryParams = queryParams
149+ , riHeaders = headers
150+ }
184151
185152-- | Creates a bucket via a PUT bucket call.
186153putBucket :: Bucket -> Region -> Minio ()
187154putBucket bucket location = do
188155 ns <- asks getSvcNamespace
189− void $
190− executeRequest $
191− defaultS3ReqInfo
192− { riMethod = HT.methodPut,
193− riBucket = Just bucket,
194− riPayload = PayloadBS $ mkCreateBucketConfig ns location,
195− riNeedsLocation = False
156+ void $ executeRequest $
157+ defaultS3ReqInfo { riMethod = HT.methodPut
158+ , riBucket = Just bucket
159+ , riPayload = PayloadBS $ mkCreateBucketConfig ns location
160+ , riNeedsLocation = False
196161 }
197162
198163-- | Single PUT object size.
@@ -208,432 +173,314 @@ putObjectSingle' bucket object headers bs = do
208173 let size = fromIntegral (BS.length bs)
209174 -- check length is within single PUT object size.
210175 when (size > maxSinglePutObjectSizeBytes) $
211− throwIO $
212− MErrVSinglePUTSizeExceeded size
176+ throwIO $ MErrVSinglePUTSizeExceeded size
213177
214178 let payload = mkStreamingPayload $ PayloadBS bs
215− resp <-
216− executeRequest $
217− defaultS3ReqInfo
218− { riMethod = HT.methodPut,
219− riBucket = Just bucket,
220− riObject = Just object,
221− riHeaders = headers,
222− riPayload = payload
223− }
179+ resp <- executeRequest $
180+ defaultS3ReqInfo { riMethod = HT.methodPut
181+ , riBucket = Just bucket
182+ , riObject = Just object
183+ , riHeaders = headers
184+ , riPayload = payload
185+ }
224186
225187 let rheaders = NC.responseHeaders resp
226188 etag = getETagHeader rheaders
227189 maybe
228190 (throwIO MErrVETagHeaderNotFound)
229− return
230− etag
191+ return etag
231192
232193-- | PUT an object into the service. This function performs a single
233194-- PUT object call, and so can only transfer objects upto 5GiB.
234−putObjectSingle ::
235− Bucket ->
236− Object ->
237− [HT.Header] ->
238− Handle ->
239− Int64 ->
240− Int64 ->
241− Minio ETag
195+putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
196+ -> Int64 -> Minio ETag
242197putObjectSingle bucket object headers h offset size = do
243198 -- check length is within single PUT object size.
244199 when (size > maxSinglePutObjectSizeBytes) $
245− throwIO $
246− MErrVSinglePUTSizeExceeded size
200+ throwIO $ MErrVSinglePUTSizeExceeded size
247201
248202 -- content-length header is automatically set by library.
249203 let payload = mkStreamingPayload $ PayloadH h offset size
250− resp <-
251− executeRequest $
252− defaultS3ReqInfo
253− { riMethod = HT.methodPut,
254− riBucket = Just bucket,
255− riObject = Just object,
256− riHeaders = headers,
257− riPayload = payload
258− }
204+ resp <- executeRequest $
205+ defaultS3ReqInfo { riMethod = HT.methodPut
206+ , riBucket = Just bucket
207+ , riObject = Just object
208+ , riHeaders = headers
209+ , riPayload = payload
210+ }
259211
260212 let rheaders = NC.responseHeaders resp
261213 etag = getETagHeader rheaders
262214 maybe
263215 (throwIO MErrVETagHeaderNotFound)
264− return
265− etag
216+ return etag
266217
267218-- | List objects in a bucket matching prefix up to delimiter,
268219-- starting from nextMarker.
269−listObjectsV1' ::
270− Bucket ->
271− Maybe Text ->
272− Maybe Text ->
273− Maybe Text ->
274− Maybe Int ->
275− Minio ListObjectsV1Result
220+listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
221+ -> Minio ListObjectsV1Result
276222listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
277− resp <-
278− executeRequest $
279− defaultS3ReqInfo
280− { riMethod = HT.methodGet,
281− riBucket = Just bucket,
282− riQueryParams = mkOptionalParams params
283− }
223+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
224+ , riBucket = Just bucket
225+ , riQueryParams = mkOptionalParams params
226+ }
284227 parseListObjectsV1Response $ NC.responseBody resp
285228 where
286− params =
287− [ ("marker", nextMarker),
288− ("prefix", prefix),
289− ("delimiter", delimiter),
290− ("max-keys", show <$> maxKeys)
229+ params = [
230+ ("marker", nextMarker)
231+ , ("prefix", prefix)
232+ , ("delimiter", delimiter)
233+ , ("max-keys", show <$> maxKeys)
291234 ]
292235
293236-- | List objects in a bucket matching prefix up to delimiter,
294237-- starting from nextToken.
295−listObjects' ::
296− Bucket ->
297− Maybe Text ->
298− Maybe Text ->
299− Maybe Text ->
300− Maybe Int ->
301− Minio ListObjectsResult
238+listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
239+ -> Minio ListObjectsResult
302240listObjects' bucket prefix nextToken delimiter maxKeys = do
303− resp <-
304− executeRequest $
305− defaultS3ReqInfo
306− { riMethod = HT.methodGet,
307− riBucket = Just bucket,
308− riQueryParams = mkOptionalParams params
309− }
241+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
242+ , riBucket = Just bucket
243+ , riQueryParams = mkOptionalParams params
244+ }
310245 parseListObjectsResponse $ NC.responseBody resp
311246 where
312− params =
313− [ ("list-type", Just "2"),
314− ("continuation_token", nextToken),
315− ("prefix", prefix),
316− ("delimiter", delimiter),
317− ("max-keys", show <$> maxKeys)
247+ params = [
248+ ("list-type", Just "2")
249+ , ("continuation_token", nextToken)
250+ , ("prefix", prefix)
251+ , ("delimiter", delimiter)
252+ , ("max-keys", show <$> maxKeys)
318253 ]
319254
320255-- | DELETE a bucket from the service.
321256deleteBucket :: Bucket -> Minio ()
322−deleteBucket bucket =
323− void $
324− executeRequest $
325− defaultS3ReqInfo
326− { riMethod = HT.methodDelete,
327− riBucket = Just bucket
257+deleteBucket bucket = void $
258+ executeRequest $
259+ defaultS3ReqInfo { riMethod = HT.methodDelete
260+ , riBucket = Just bucket
328261 }
329262
330263-- | DELETE an object from the service.
331264deleteObject :: Bucket -> Object -> Minio ()
332−deleteObject bucket object =
333− void $
334− executeRequest $
335− defaultS3ReqInfo
336− { riMethod = HT.methodDelete,
337− riBucket = Just bucket,
338− riObject = Just object
265+deleteObject bucket object = void $
266+ executeRequest $
267+ defaultS3ReqInfo { riMethod = HT.methodDelete
268+ , riBucket = Just bucket
269+ , riObject = Just object
339270 }
340271
341272-- | Create a new multipart upload.
342273newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
343274newMultipartUpload bucket object headers = do
344− resp <-
345− executeRequest $
346− defaultS3ReqInfo
347− { riMethod = HT.methodPost,
348− riBucket = Just bucket,
349− riObject = Just object,
350− riQueryParams = [("uploads", Nothing)],
351− riHeaders = headers
352− }
275+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPost
276+ , riBucket = Just bucket
277+ , riObject = Just object
278+ , riQueryParams = [("uploads", Nothing)]
279+ , riHeaders = headers
280+ }
353281 parseNewMultipartUpload $ NC.responseBody resp
354282
355283-- | PUT a part of an object as part of a multipart upload.
356−putObjectPart ::
357− Bucket ->
358− Object ->
359− UploadId ->
360− PartNumber ->
361− [HT.Header] ->
362− Payload ->
363− Minio PartTuple
284+putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
285+ -> Payload -> Minio PartTuple
364286putObjectPart bucket object uploadId partNumber headers payload = do
365287 -- transform payload to conduit to enable streaming signature
366288 let payload' = mkStreamingPayload payload
367− resp <-
368− executeRequest $
369− defaultS3ReqInfo
370− { riMethod = HT.methodPut,
371− riBucket = Just bucket,
372− riObject = Just object,
373− riQueryParams = mkOptionalParams params,
374− riHeaders = headers,
375− riPayload = payload'
376− }
289+ resp <- executeRequest $
290+ defaultS3ReqInfo { riMethod = HT.methodPut
291+ , riBucket = Just bucket
292+ , riObject = Just object
293+ , riQueryParams = mkOptionalParams params
294+ , riHeaders = headers
295+ , riPayload = payload'
296+ }
377297 let rheaders = NC.responseHeaders resp
378298 etag = getETagHeader rheaders
379299 maybe
380300 (throwIO MErrVETagHeaderNotFound)
381− (return . (partNumber,))
382− etag
301+ (return . (partNumber, )) etag
383302 where
384− params =
385− [ ("uploadId", Just uploadId),
386− ("partNumber", Just $ show partNumber)
303+ params = [
304+ ("uploadId", Just uploadId)
305+ , ("partNumber", Just $ show partNumber)
387306 ]
388307
389308srcInfoToHeaders :: SourceInfo -> [HT.Header]
390−srcInfoToHeaders srcInfo =
391− ( "x-amz-copy-source",
392− encodeUtf8 $
393− T.concat
394− [ "/",
395− srcBucket srcInfo,
396− "/",
397− srcObject srcInfo
398− ]
399− )
400− : rangeHdr
401− ++ zip names values
309+srcInfoToHeaders srcInfo = ("x-amz-copy-source",
310+ toS $ T.concat ["/", srcBucket srcInfo,
311+ "/", srcObject srcInfo]
312+ ) : rangeHdr ++ zip names values
402313 where
403− names =
404− [ "x-amz-copy-source-if-match",
405− "x-amz-copy-source-if-none-match",
406− "x-amz-copy-source-if-unmodified-since",
407− "x-amz-copy-source-if-modified-since"
408− ]
409− values =
410− mapMaybe
411− (fmap encodeUtf8 . (srcInfo &))
412− [ srcIfMatch,
413− srcIfNoneMatch,
414− fmap formatRFC1123 . srcIfUnmodifiedSince,
415− fmap formatRFC1123 . srcIfModifiedSince
416− ]
417− rangeHdr =
418− maybe [] ((\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) . toByteRange) (srcRange srcInfo)
314+ names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",
315+ "x-amz-copy-source-if-unmodified-since",
316+ "x-amz-copy-source-if-modified-since"]
317+ values = mapMaybe (fmap encodeUtf8 . (srcInfo &))
318+ [srcIfMatch, srcIfNoneMatch,
319+ fmap formatRFC1123 . srcIfUnmodifiedSince,
320+ fmap formatRFC1123 . srcIfModifiedSince]
321+ rangeHdr = maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])])
322+ $ toByteRange <$> srcRange srcInfo
419323 toByteRange :: (Int64, Int64) -> HT.ByteRange
420324 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
421325
422326-- | Performs server-side copy of an object or part of an object as an
423327-- upload part of an ongoing multi-part upload.
424−copyObjectPart ::
425− DestinationInfo ->
426− SourceInfo ->
427− UploadId ->
428− PartNumber ->
429− [HT.Header] ->
430− Minio (ETag, UTCTime)
328+copyObjectPart :: DestinationInfo -> SourceInfo -> UploadId
329+ -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
431330copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
432− resp <-
433− executeRequest $
434− defaultS3ReqInfo
435− { riMethod = HT.methodPut,
436− riBucket = Just $ dstBucket dstInfo,
437− riObject = Just $ dstObject dstInfo,
438− riQueryParams = mkOptionalParams params,
439− riHeaders = headers ++ srcInfoToHeaders srcInfo
440− }
331+ resp <- executeRequest $
332+ defaultS3ReqInfo { riMethod = HT.methodPut
333+ , riBucket = Just $ dstBucket dstInfo
334+ , riObject = Just $ dstObject dstInfo
335+ , riQueryParams = mkOptionalParams params
336+ , riHeaders = headers ++ srcInfoToHeaders srcInfo
337+ }
441338
442339 parseCopyObjectResponse $ NC.responseBody resp
443340 where
444− params =
445− [ ("uploadId", Just uploadId),
446− ("partNumber", Just $ show partNumber)
341+ params = [
342+ ("uploadId", Just uploadId)
343+ , ("partNumber", Just $ show partNumber)
447344 ]
448345
449346-- | Performs server-side copy of an object that is upto 5GiB in
450347-- size. If the object is greater than 5GiB, this function throws the
451348-- error returned by the server.
452−copyObjectSingle ::
453− Bucket ->
454− Object ->
455− SourceInfo ->
456− [HT.Header] ->
457− Minio (ETag, UTCTime)
349+copyObjectSingle :: Bucket -> Object -> SourceInfo -> [HT.Header]
350+ -> Minio (ETag, UTCTime)
458351copyObjectSingle bucket object srcInfo headers = do
459352 -- validate that srcRange is Nothing for this API.
460353 when (isJust $ srcRange srcInfo) $
461354 throwIO MErrVCopyObjSingleNoRangeAccepted
462− resp <-
463− executeRequest $
464− defaultS3ReqInfo
465− { riMethod = HT.methodPut,
466− riBucket = Just bucket,
467− riObject = Just object,
468− riHeaders = headers ++ srcInfoToHeaders srcInfo
469− }
355+ resp <- executeRequest $
356+ defaultS3ReqInfo { riMethod = HT.methodPut
357+ , riBucket = Just bucket
358+ , riObject = Just object
359+ , riHeaders = headers ++ srcInfoToHeaders srcInfo
360+ }
470361 parseCopyObjectResponse $ NC.responseBody resp
471362
472363-- | Complete a multipart upload.
473−completeMultipartUpload ::
474− Bucket ->
475− Object ->
476− UploadId ->
477− [PartTuple] ->
478− Minio ETag
364+completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
365+ -> Minio ETag
479366completeMultipartUpload bucket object uploadId partTuple = do
480− resp <-
481− executeRequest $
482− defaultS3ReqInfo
483− { riMethod = HT.methodPost,
484− riBucket = Just bucket,
485− riObject = Just object,
486− riQueryParams = mkOptionalParams params,
487− riPayload =
488− PayloadBS $
489− mkCompleteMultipartUploadRequest partTuple
490− }
367+ resp <- executeRequest $
368+ defaultS3ReqInfo { riMethod = HT.methodPost
369+ , riBucket = Just bucket
370+ , riObject = Just object
371+ , riQueryParams = mkOptionalParams params
372+ , riPayload = PayloadBS $
373+ mkCompleteMultipartUploadRequest partTuple
374+ }
491375 parseCompleteMultipartUploadResponse $ NC.responseBody resp
492376 where
493377 params = [("uploadId", Just uploadId)]
494378
495379-- | Abort a multipart upload.
496380abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
497−abortMultipartUpload bucket object uploadId =
498− void $
499− executeRequest $
500− defaultS3ReqInfo
501− { riMethod = HT.methodDelete,
502− riBucket = Just bucket,
503− riObject = Just object,
504− riQueryParams = mkOptionalParams params
505− }
381+abortMultipartUpload bucket object uploadId = void $
382+ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
383+ , riBucket = Just bucket
384+ , riObject = Just object
385+ , riQueryParams = mkOptionalParams params
386+ }
506387 where
507388 params = [("uploadId", Just uploadId)]
508389
509390-- | List incomplete multipart uploads.
510−listIncompleteUploads' ::
511− Bucket ->
512− Maybe Text ->
513− Maybe Text ->
514− Maybe Text ->
515− Maybe Text ->
516− Maybe Int ->
517− Minio ListUploadsResult
391+listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
392+ -> Maybe Text -> Maybe Int -> Minio ListUploadsResult
518393listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
519− resp <-
520− executeRequest $
521− defaultS3ReqInfo
522− { riMethod = HT.methodGet,
523− riBucket = Just bucket,
524− riQueryParams = params
525− }
394+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
395+ , riBucket = Just bucket
396+ , riQueryParams = params
397+ }
526398 parseListUploadsResponse $ NC.responseBody resp
527399 where
528400 -- build query params
529− params =
530− ("uploads", Nothing)
531− : mkOptionalParams
532− [ ("prefix", prefix),
533− ("delimiter", delimiter),
534− ("key-marker", keyMarker),
535− ("upload-id-marker", uploadIdMarker),
536− ("max-uploads", show <$> maxKeys)
537− ]
401+ params = ("uploads", Nothing) : mkOptionalParams
402+ [ ("prefix", prefix)
403+ , ("delimiter", delimiter)
404+ , ("key-marker", keyMarker)
405+ , ("upload-id-marker", uploadIdMarker)
406+ , ("max-uploads", show <$> maxKeys)
407+ ]
408+
538409
539410-- | List parts of an ongoing multipart upload.
540−listIncompleteParts' ::
541− Bucket ->
542− Object ->
543− UploadId ->
544− Maybe Text ->
545− Maybe Text ->
546− Minio ListPartsResult
411+listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
412+ -> Maybe Text -> Minio ListPartsResult
547413listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
548− resp <-
549− executeRequest $
550− defaultS3ReqInfo
551− { riMethod = HT.methodGet,
552− riBucket = Just bucket,
553− riObject = Just object,
554− riQueryParams = mkOptionalParams params
555− }
414+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
415+ , riBucket = Just bucket
416+ , riObject = Just object
417+ , riQueryParams = mkOptionalParams params
418+ }
556419 parseListPartsResponse $ NC.responseBody resp
557420 where
558421 -- build optional query params
559− params =
560− [ ("uploadId", Just uploadId),
561− ("part-number-marker", partNumMarker),
562− ("max-parts", maxParts)
422+ params = [
423+ ("uploadId", Just uploadId)
424+ , ("part-number-marker", partNumMarker)
425+ , ("max-parts", maxParts)
563426 ]
564427
565428-- | Get metadata of an object.
566429headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
567430headObject bucket object reqHeaders = do
568− resp <-
569− executeRequest $
570− defaultS3ReqInfo
571− { riMethod = HT.methodHead,
572− riBucket = Just bucket,
573− riObject = Just object,
574− riHeaders =
575− reqHeaders
576− -- This header is required for safety as otherwise http-client,
577− -- sends Accept-Encoding: gzip, and the server may actually gzip
578− -- body. In that case Content-Length header will be missing.
579− <> [("Accept-Encoding", "identity")]
580− }
431+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
432+ , riBucket = Just bucket
433+ , riObject = Just object
434+ , riHeaders = reqHeaders
435+ }
436+
581437 maybe (throwIO MErrVInvalidObjectInfoResponse) return $
582− parseGetObjectHeaders object $
583− NC.responseHeaders resp
438+ parseGetObjectHeaders object $ NC.responseHeaders resp
439+
584440
585441-- | Query the object store if a given bucket exists.
586442headBucket :: Bucket -> Minio Bool
587−headBucket bucket =
588− headBucketEx
589− `catches` [ Handler handleNoSuchBucket,
590− Handler handleStatus404
591− ]
443+headBucket bucket = headBucketEx `catches`
444+ [ Handler handleNoSuchBucket
445+ , Handler handleStatus404
446+ ]
447+
592448 where
593449 handleNoSuchBucket :: ServiceErr -> Minio Bool
594− handleNoSuchBucket e
595− | e == NoSuchBucket = return False
596− | otherwise = throwIO e
450+ handleNoSuchBucket e | e == NoSuchBucket = return False
451+ | otherwise = throwIO e
452+
597453 handleStatus404 :: NC.HttpException -> Minio Bool
598454 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
599455 if NC.responseStatus res == status404
600− then return False
601− else throwIO e
456+ then return False
457+ else throwIO e
602458 handleStatus404 e = throwIO e
459+
603460 headBucketEx = do
604− resp <-
605− executeRequest $
606− defaultS3ReqInfo
607− { riMethod = HT.methodHead,
608− riBucket = Just bucket
609− }
461+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
462+ , riBucket = Just bucket
463+ }
610464 return $ NC.responseStatus resp == HT.ok200
611465
612466-- | Set the notification configuration on a bucket.
613467putBucketNotification :: Bucket -> Notification -> Minio ()
614468putBucketNotification bucket ncfg = do
615469 ns <- asks getSvcNamespace
616− void $
617− executeRequest $
618− defaultS3ReqInfo
619− { riMethod = HT.methodPut,
620− riBucket = Just bucket,
621− riQueryParams = [("notification", Nothing)],
622− riPayload =
623− PayloadBS $
624− mkPutNotificationRequest ns ncfg
625− }
470+ void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
471+ , riBucket = Just bucket
472+ , riQueryParams = [("notification", Nothing)]
473+ , riPayload = PayloadBS $
474+ mkPutNotificationRequest ns ncfg
475+ }
626476
627477-- | Retrieve the notification configuration on a bucket.
628478getBucketNotification :: Bucket -> Minio Notification
629479getBucketNotification bucket = do
630− resp <-
631− executeRequest $
632− defaultS3ReqInfo
633− { riMethod = HT.methodGet,
634− riBucket = Just bucket,
635− riQueryParams = [("notification", Nothing)]
636− }
480+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
481+ , riBucket = Just bucket
482+ , riQueryParams = [("notification", Nothing)]
483+ }
637484 parseNotification $ NC.responseBody resp
638485
639486-- | Remove all notifications configured on a bucket.
@@ -643,14 +490,11 @@ removeAllBucketNotification = flip putBucketNotification defaultNotification
643490-- | Fetch the policy if any on a bucket.
644491getBucketPolicy :: Bucket -> Minio Text
645492getBucketPolicy bucket = do
646− resp <-
647− executeRequest $
648− defaultS3ReqInfo
649− { riMethod = HT.methodGet,
650− riBucket = Just bucket,
651− riQueryParams = [("policy", Nothing)]
652− }
653− return $ decodeUtf8Lenient $ toStrictBS $ NC.responseBody resp
493+ resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
494+ , riBucket = Just bucket
495+ , riQueryParams = [("policy", Nothing)]
496+ }
497+ return $ toS $ NC.responseBody resp
654498
655499-- | Set a new policy on a bucket.
656500-- As a special condition if the policy is empty
@@ -662,24 +506,18 @@ setBucketPolicy bucket policy = do
662506 else putBucketPolicy bucket policy
663507
664508-- | Save a new policy on a bucket.
665−putBucketPolicy :: Bucket -> Text -> Minio ()
509+putBucketPolicy :: Bucket -> Text -> Minio()
666510putBucketPolicy bucket policy = do
667− void $
668− executeRequest $
669− defaultS3ReqInfo
670− { riMethod = HT.methodPut,
671− riBucket = Just bucket,
672− riQueryParams = [("policy", Nothing)],
673− riPayload = PayloadBS $ encodeUtf8 policy
674− }
511+ void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
512+ , riBucket = Just bucket
513+ , riQueryParams = [("policy", Nothing)]
514+ , riPayload = PayloadBS $ encodeUtf8 policy
515+ }
675516
676517-- | Delete any policy set on a bucket.
677−deleteBucketPolicy :: Bucket -> Minio ()
518+deleteBucketPolicy :: Bucket -> Minio()
678519deleteBucketPolicy bucket = do
679− void $
680− executeRequest $
681− defaultS3ReqInfo
682− { riMethod = HT.methodDelete,
683− riBucket = Just bucket,
684− riQueryParams = [("policy", Nothing)]
685− }
520+ void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
521+ , riBucket = Just bucket
522+ , riQueryParams = [("policy", Nothing)]
523+ }
src/Network/Minio/SelectAPI.hs+238−233
@@ -15,103 +15,113 @@
1515--
1616
1717module Network.Minio.SelectAPI
18− ( -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
19− -- format objects in AWS S3 and in MinIO using SQL Select
20− -- statements. This allows significant reduction of data transfer
21− -- from object storage for computation-intensive tasks, as relevant
22− -- data is filtered close to the storage.
23− selectObjectContent,
24− SelectRequest,
25− selectRequest,
26−
27− -- *** Input Serialization
28− InputSerialization,
29− defaultCsvInput,
30− linesJsonInput,
31− documentJsonInput,
32− defaultParquetInput,
33− setInputCSVProps,
34− CompressionType (..),
35− setInputCompressionType,
36−
37− -- *** CSV Format details
38−
39− -- | CSV format options such as delimiters and quote characters are
40− -- specified using using the functions below. Options are combined
41− -- monoidally.
42− CSVProp,
43− recordDelimiter,
44− fieldDelimiter,
45− quoteCharacter,
46− quoteEscapeCharacter,
47− commentCharacter,
48− allowQuotedRecordDelimiter,
49− FileHeaderInfo (..),
50− fileHeaderInfo,
51− QuoteFields (..),
52− quoteFields,
53−
54− -- *** Output Serialization
55− OutputSerialization,
56− defaultCsvOutput,
57− defaultJsonOutput,
58− outputCSVFromProps,
59− outputJSONFromRecordDelimiter,
60−
61− -- *** Progress messages
62− setRequestProgressEnabled,
63−
64− -- *** Interpreting Select output
65−
66− -- | The conduit returned by `selectObjectContent` returns values of
67− -- the `EventMessage` data type. This returns the query output
68− -- messages formatted according to the chosen output serialization,
69− -- interleaved with progress messages (if enabled by
70− -- `setRequestProgressEnabled`), and at the end a statistics
71− -- message.
72− --
73− -- If the application is interested in only the payload, then
74− -- `getPayloadBytes` can be used. For example to simply print the
75− -- payload to stdout:
76− --
77− -- > resultConduit <- selectObjectContent bucket object mySelectRequest
78− -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
79− --
80− -- Note that runConduit, the connect operator (.|) and stdoutC are
81− -- all from the "conduit" package.
82− getPayloadBytes,
83− EventMessage (..),
84− Progress (..),
85− Stats,
86− )
87−where
88−
89−import Conduit ((.|))
90−import qualified Conduit as C
91−import qualified Data.Binary as Bin
92−import qualified Data.ByteString as B
93−import qualified Data.ByteString.Lazy as LB
94−import Data.Digest.CRC32 (crc32, crc32Update)
95−import Lib.Prelude
96−import qualified Network.HTTP.Conduit as NC
97−import qualified Network.HTTP.Types as HT
98−import Network.Minio.API
99−import Network.Minio.Data
100−import Network.Minio.Errors
101−import Network.Minio.Utils
102−import Network.Minio.XmlGenerator
103−import Network.Minio.XmlParser
104−import UnliftIO (MonadUnliftIO)
105−
106−data EventStreamException
107− = ESEPreludeCRCFailed
108− | ESEMessageCRCFailed
109− | ESEUnexpectedEndOfStream
110− | ESEDecodeFail [Char]
111− | ESEInvalidHeaderType
112− | ESEInvalidHeaderValueType
113− | ESEInvalidMessageType
114− deriving stock (Eq, Show)
18+ (
19+
20+ -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
21+ -- format objects in AWS S3 and in MinIO using SQL Select
22+ -- statements. This allows significant reduction of data transfer
23+ -- from object storage for computation-intensive tasks, as relevant
24+ -- data is filtered close to the storage.
25+
26+ selectObjectContent
27+
28+ , SelectRequest
29+ , selectRequest
30+
31+ -- *** Input Serialization
32+
33+ , InputSerialization
34+ , defaultCsvInput
35+ , linesJsonInput
36+ , documentJsonInput
37+ , defaultParquetInput
38+ , setInputCSVProps
39+
40+ , CompressionType(..)
41+ , setInputCompressionType
42+
43+ -- *** CSV Format details
44+
45+ -- | CSV format options such as delimiters and quote characters are
46+ -- specified using using the functions below. Options are combined
47+ -- monoidally.
48+
49+ , CSVProp
50+ , recordDelimiter
51+ , fieldDelimiter
52+ , quoteCharacter
53+ , quoteEscapeCharacter
54+ , commentCharacter
55+ , allowQuotedRecordDelimiter
56+ , FileHeaderInfo(..)
57+ , fileHeaderInfo
58+ , QuoteFields(..)
59+ , quoteFields
60+
61+ -- *** Output Serialization
62+
63+ , OutputSerialization
64+ , defaultCsvOutput
65+ , defaultJsonOutput
66+ , outputCSVFromProps
67+ , outputJSONFromRecordDelimiter
68+
69+ -- *** Progress messages
70+
71+ , setRequestProgressEnabled
72+
73+ -- *** Interpreting Select output
74+
75+ -- | The conduit returned by `selectObjectContent` returns values of
76+ -- the `EventMessage` data type. This returns the query output
77+ -- messages formatted according to the chosen output serialization,
78+ -- interleaved with progress messages (if enabled by
79+ -- `setRequestProgressEnabled`), and at the end a statistics
80+ -- message.
81+ --
82+ -- If the application is interested in only the payload, then
83+ -- `getPayloadBytes` can be used. For example to simply print the
84+ -- payload to stdout:
85+ --
86+ -- > resultConduit <- selectObjectContent bucket object mySelectRequest
87+ -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
88+ --
89+ -- Note that runConduit, the connect operator (.|) and stdoutC are
90+ -- all from the "conduit" package.
91+
92+ , getPayloadBytes
93+ , EventMessage(..)
94+ , Progress(..)
95+ , Stats
96+ ) where
97+
98+import Conduit ((.|))
99+import qualified Conduit as C
100+import qualified Data.Binary as Bin
101+import qualified Data.ByteString as B
102+import qualified Data.ByteString.Lazy as LB
103+import Data.Digest.CRC32 (crc32, crc32Update)
104+import qualified Network.HTTP.Conduit as NC
105+import qualified Network.HTTP.Types as HT
106+import UnliftIO (MonadUnliftIO)
107+
108+import Lib.Prelude
109+
110+import Network.Minio.API
111+import Network.Minio.Data
112+import Network.Minio.Errors
113+import Network.Minio.Utils
114+import Network.Minio.XmlGenerator
115+import Network.Minio.XmlParser
116+
117+data EventStreamException = ESEPreludeCRCFailed
118+ | ESEMessageCRCFailed
119+ | ESEUnexpectedEndOfStream
120+ | ESEDecodeFail [Char]
121+ | ESEInvalidHeaderType
122+ | ESEInvalidHeaderValueType
123+ | ESEInvalidMessageType
124+ deriving (Eq, Show)
115125
116126instance Exception EventStreamException
117127
@@ -119,176 +129,171 @@ instance Exception EventStreamException
119129chunkSize :: Int
120130chunkSize = 32 * 1024
121131
122−parseBinary :: (Bin.Binary a) => ByteString -> IO a
132+parseBinary :: Bin.Binary a => ByteString -> IO a
123133parseBinary b = do
124− case Bin.decodeOrFail $ LB.fromStrict b of
125− Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
126− Right (_, _, r) -> return r
134+ case Bin.decodeOrFail $ LB.fromStrict b of
135+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
136+ Right (_, _, r) -> return r
127137
128138bytesToHeaderName :: Text -> IO MsgHeaderName
129139bytesToHeaderName t = case t of
130− ":message-type" -> return MessageType
131− ":event-type" -> return EventType
132− ":content-type" -> return ContentType
133− ":error-code" -> return ErrorCode
140+ ":message-type" -> return MessageType
141+ ":event-type" -> return EventType
142+ ":content-type" -> return ContentType
143+ ":error-code" -> return ErrorCode
134144 ":error-message" -> return ErrorMessage
135− _ -> throwIO ESEInvalidHeaderType
145+ _ -> throwIO ESEInvalidHeaderType
136146
137−parseHeaders ::
138− (MonadUnliftIO m) =>
139− Word32 ->
140− C.ConduitM ByteString a m [MessageHeader]
147+parseHeaders :: MonadUnliftIO m
148+ => Word32 -> C.ConduitM ByteString a m [MessageHeader]
141149parseHeaders 0 = return []
142150parseHeaders hdrLen = do
143− bs1 <- readNBytes 1
144− n :: Word8 <- liftIO $ parseBinary bs1
151+ bs1 <- readNBytes 1
152+ n :: Word8 <- liftIO $ parseBinary bs1
145153
146− headerKeyBytes <- readNBytes $ fromIntegral n
147− let headerKey = decodeUtf8Lenient headerKeyBytes
148− headerName <- liftIO $ bytesToHeaderName headerKey
154+ headerKeyBytes <- readNBytes $ fromIntegral n
155+ let headerKey = decodeUtf8Lenient headerKeyBytes
156+ headerName <- liftIO $ bytesToHeaderName headerKey
149157
150− bs2 <- readNBytes 1
151− headerValueType :: Word8 <- liftIO $ parseBinary bs2
152− when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
158+ bs2 <- readNBytes 1
159+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
160+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
153161
154− bs3 <- readNBytes 2
155− vLen :: Word16 <- liftIO $ parseBinary bs3
156− headerValueBytes <- readNBytes $ fromIntegral vLen
157− let headerValue = decodeUtf8Lenient headerValueBytes
158− m = (headerName, headerValue)
159− k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
162+ bs3 <- readNBytes 2
163+ vLen :: Word16 <- liftIO $ parseBinary bs3
164+ headerValueBytes <- readNBytes $ fromIntegral vLen
165+ let headerValue = decodeUtf8Lenient headerValueBytes
166+ m = (headerName, headerValue)
167+ k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
160168
161− ms <- parseHeaders (hdrLen - k)
162− return (m : ms)
169+ ms <- parseHeaders (hdrLen - k)
170+ return (m:ms)
163171
164172-- readNBytes returns N bytes read from the string and throws an
165173-- exception if N bytes are not present on the stream.
166−readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
174+readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
167175readNBytes n = do
168− b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
169− if B.length b /= n
170− then throwIO ESEUnexpectedEndOfStream
171− else return b
172−
173−crcCheck ::
174− (MonadUnliftIO m) =>
175− C.ConduitM ByteString ByteString m ()
176+ b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
177+ if B.length b /= n
178+ then throwIO ESEUnexpectedEndOfStream
179+ else return b
180+
181+crcCheck :: MonadUnliftIO m
182+ => C.ConduitM ByteString ByteString m ()
176183crcCheck = do
177− b <- readNBytes 12
178− n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
179− preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
180− when (crc32 (B.take 8 b) /= preludeCRC) $
181− throwIO ESEPreludeCRCFailed
184+ b <- readNBytes 12
185+ n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
186+ preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
187+ when (crc32 (B.take 8 b) /= preludeCRC) $
188+ throwIO ESEPreludeCRCFailed
182189
183− -- we do not yield the checksum
184− C.yield $ B.take 8 b
190+ -- we do not yield the checksum
191+ C.yield $ B.take 8 b
185192
186− -- 12 bytes have been read off the current message. Now read the
187− -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
188− let startCrc = crc32 b
189− finalCrc <- accumulateYield (fromIntegral n - 16) startCrc
193+ -- 12 bytes have been read off the current message. Now read the
194+ -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
195+ let startCrc = crc32 b
196+ finalCrc <- accumulateYield (fromIntegral n-16) startCrc
190197
191− bs <- readNBytes 4
192− expectedCrc :: Word32 <- liftIO $ parseBinary bs
198+ bs <- readNBytes 4
199+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
193200
194− when (finalCrc /= expectedCrc) $
195− throwIO ESEMessageCRCFailed
201+ when (finalCrc /= expectedCrc) $
202+ throwIO ESEMessageCRCFailed
196203
197− -- we unconditionally recurse - downstream figures out when to
198− -- quit reading the stream
199− crcCheck
204+ -- we unconditionally recurse - downstream figures out when to
205+ -- quit reading the stream
206+ crcCheck
200207 where
201208 accumulateYield n checkSum = do
202− let toRead = min n chunkSize
203− b <- readNBytes toRead
204− let c' = crc32Update checkSum b
205− n' = n - B.length b
206− C.yield b
207− if n' > 0
208− then accumulateYield n' c'
209− else return c'
210−
211−handleMessage :: (MonadUnliftIO m) => C.ConduitT ByteString EventMessage m ()
209+ let toRead = min n chunkSize
210+ b <- readNBytes toRead
211+ let c' = crc32Update checkSum b
212+ n' = n - B.length b
213+ C.yield b
214+ if n' > 0
215+ then accumulateYield n' c'
216+ else return c'
217+
218+handleMessage :: MonadUnliftIO m => C.ConduitT ByteString EventMessage m ()
212219handleMessage = do
213− b1 <- readNBytes 4
214− msgLen :: Word32 <- liftIO $ parseBinary b1
215−
216− b2 <- readNBytes 4
217− hdrLen :: Word32 <- liftIO $ parseBinary b2
218−
219− hs <- parseHeaders hdrLen
220−
221− let payloadLen = msgLen - hdrLen - 16
222− getHdrVal h = fmap snd . find ((h ==) . fst)
223− eventHdrValue = getHdrVal EventType hs
224− msgHdrValue = getHdrVal MessageType hs
225− errCode = getHdrVal ErrorCode hs
226− errMsg = getHdrVal ErrorMessage hs
227−
228− case msgHdrValue of
229− Just "event" -> do
230− case eventHdrValue of
231− Just "Records" -> passThrough $ fromIntegral payloadLen
232− Just "Cont" -> return ()
233− Just "Progress" -> do
234− bs <- readNBytes $ fromIntegral payloadLen
235− progress <- parseSelectProgress bs
236− C.yield $ ProgressEventMessage progress
237− Just "Stats" -> do
238− bs <- readNBytes $ fromIntegral payloadLen
239− stats <- parseSelectProgress bs
240− C.yield $ StatsEventMessage stats
241− Just "End" -> return ()
242− _ -> throwIO ESEInvalidMessageType
243− when (eventHdrValue /= Just "End") handleMessage
244− Just "error" -> do
245− let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
246− maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
247− _ -> throwIO ESEInvalidMessageType
220+ b1 <- readNBytes 4
221+ msgLen :: Word32 <- liftIO $ parseBinary b1
222+
223+ b2 <- readNBytes 4
224+ hdrLen :: Word32 <- liftIO $ parseBinary b2
225+
226+ hs <- parseHeaders hdrLen
227+
228+ let payloadLen = msgLen - hdrLen - 16
229+ getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)
230+ eventHdrValue = getHdrVal EventType hs
231+ msgHdrValue = getHdrVal MessageType hs
232+ errCode = getHdrVal ErrorCode hs
233+ errMsg = getHdrVal ErrorMessage hs
234+
235+ case msgHdrValue of
236+ Just "event" -> do
237+ case eventHdrValue of
238+ Just "Records" -> passThrough $ fromIntegral payloadLen
239+ Just "Cont" -> return ()
240+ Just "Progress" -> do
241+ bs <- readNBytes $ fromIntegral payloadLen
242+ progress <- parseSelectProgress bs
243+ C.yield $ ProgressEventMessage progress
244+ Just "Stats" -> do
245+ bs <- readNBytes $ fromIntegral payloadLen
246+ stats <- parseSelectProgress bs
247+ C.yield $ StatsEventMessage stats
248+ Just "End" -> return ()
249+ _ -> throwIO ESEInvalidMessageType
250+ when (eventHdrValue /= Just "End") handleMessage
251+
252+ Just "error" -> do
253+ let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
254+ maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
255+
256+ _ -> throwIO ESEInvalidMessageType
257+
248258 where
249259 passThrough 0 = return ()
250260 passThrough n = do
251− let c = min n chunkSize
252− b <- readNBytes c
253− C.yield $ RecordPayloadEventMessage b
254− passThrough $ n - B.length b
255−
256−selectProtoConduit ::
257− (MonadUnliftIO m) =>
258− C.ConduitT ByteString EventMessage m ()
261+ let c = min n chunkSize
262+ b <- readNBytes c
263+ C.yield $ RecordPayloadEventMessage b
264+ passThrough $ n - B.length b
265+
266+
267+selectProtoConduit :: MonadUnliftIO m
268+ => C.ConduitT ByteString EventMessage m ()
259269selectProtoConduit = crcCheck .| handleMessage
260270
261271-- | selectObjectContent calls the SelectRequest on the given
262272-- object. It returns a Conduit of event messages that can be consumed
263273-- by the client.
264−selectObjectContent ::
265− Bucket ->
266− Object ->
267− SelectRequest ->
268− Minio (C.ConduitT () EventMessage Minio ())
274+selectObjectContent :: Bucket -> Object -> SelectRequest
275+ -> Minio (C.ConduitT () EventMessage Minio ())
269276selectObjectContent b o r = do
270− let reqInfo =
271− defaultS3ReqInfo
272− { riMethod = HT.methodPost,
273− riBucket = Just b,
274− riObject = Just o,
275− riPayload = PayloadBS $ mkSelectRequest r,
276− riNeedsLocation = False,
277− riQueryParams = [("select", Nothing), ("select-type", Just "2")]
278− }
279− -- print $ mkSelectRequest r
280− resp <- mkStreamRequest reqInfo
281− return $ NC.responseBody resp .| selectProtoConduit
277+ let reqInfo = defaultS3ReqInfo { riMethod = HT.methodPost
278+ , riBucket = Just b
279+ , riObject = Just o
280+ , riPayload = PayloadBS $ mkSelectRequest r
281+ , riNeedsLocation = False
282+ , riQueryParams = [("select", Nothing), ("select-type", Just "2")]
283+ }
284+ --print $ mkSelectRequest r
285+ resp <- mkStreamRequest reqInfo
286+ return $ NC.responseBody resp .| selectProtoConduit
282287
283288-- | A helper conduit that returns only the record payload bytes.
284−getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
289+getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
285290getPayloadBytes = do
286− evM <- C.await
287− case evM of
288− Just v -> do
289− case v of
290− RecordPayloadEventMessage b -> C.yield b
291− RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
292− _ -> return ()
293− getPayloadBytes
294− Nothing -> return ()
291+ evM <- C.await
292+ case evM of
293+ Just v -> do
294+ case v of
295+ RecordPayloadEventMessage b -> C.yield b
296+ RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
297+ _ -> return ()
298+ getPayloadBytes
299+ Nothing -> return ()
src/Network/Minio/Sign/V4.hs+327−430
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,94 +13,88 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE BangPatterns #-}
17−
18−module Network.Minio.Sign.V4
19− ( SignParams (..),
20− signV4QueryParams,
21− signV4,
22− signV4PostPolicy,
23− signV4Stream,
24− Service (..),
25− credentialScope,
26− )
27−where
28−
29−import qualified Conduit as C
30−import qualified Data.ByteArray as BA
31−import qualified Data.ByteString as B
32−import qualified Data.ByteString.Base64 as Base64
33−import qualified Data.ByteString.Char8 as B8
34−import qualified Data.ByteString.Lazy as LB
35−import Data.CaseInsensitive (mk)
36−import qualified Data.CaseInsensitive as CI
37−import qualified Data.HashMap.Strict as Map
38−import qualified Data.HashSet as Set
39−import Data.List (partition)
40−import qualified Data.List.NonEmpty as NE
41−import qualified Data.Time as Time
42−import Lib.Prelude
43−import qualified Network.HTTP.Conduit as NC
44−import Network.HTTP.Types (Header, SimpleQuery, hContentEncoding, parseQuery)
45−import qualified Network.HTTP.Types as H
46−import Network.HTTP.Types.Header (RequestHeaders)
47−import Network.Minio.Data.ByteString
48−import Network.Minio.Data.Crypto
49−import Network.Minio.Data.Time
50−import Network.Minio.Errors
51−import Text.Printf (printf)
16+
17+module Network.Minio.Sign.V4 where
18+
19+import qualified Conduit as C
20+import qualified Data.ByteString as B
21+import qualified Data.ByteString.Base64 as Base64
22+import qualified Data.ByteString.Char8 as B8
23+import Data.CaseInsensitive (mk)
24+import qualified Data.CaseInsensitive as CI
25+import qualified Data.HashMap.Strict as Map
26+import qualified Data.HashSet as Set
27+import qualified Data.Time as Time
28+import qualified Network.HTTP.Conduit as NC
29+import Network.HTTP.Types (Header, parseQuery)
30+import qualified Network.HTTP.Types as H
31+import Text.Printf (printf)
32+
33+import Lib.Prelude
34+
35+import Network.Minio.Data.ByteString
36+import Network.Minio.Data.Crypto
37+import Network.Minio.Data.Time
38+import Network.Minio.Errors
5239
5340-- these headers are not included in the string to sign when signing a
5441-- request
5542ignoredHeaders :: Set.HashSet ByteString
56−ignoredHeaders =
57− Set.fromList $
58− map
59− CI.foldedCase
60− [ H.hAuthorization,
61− H.hContentType,
62− H.hUserAgent
63− ]
64−
65−data Service = ServiceS3 | ServiceSTS
66− deriving stock (Eq, Show)
67−
68−toByteString :: Service -> ByteString
69−toByteString ServiceS3 = "s3"
70−toByteString ServiceSTS = "sts"
71−
72−data SignParams = SignParams
73− { spAccessKey :: Text,
74− spSecretKey :: BA.ScrubbedBytes,
75− spSessionToken :: Maybe BA.ScrubbedBytes,
76− spService :: Service,
77− spTimeStamp :: UTCTime,
78− spRegion :: Maybe Text,
79− spExpirySecs :: Maybe UrlExpiry,
80− spPayloadHash :: Maybe ByteString
81− }
82− deriving stock (Show)
43+ignoredHeaders = Set.fromList $ map CI.foldedCase
44+ [ H.hAuthorization
45+ , H.hContentType
46+ , H.hUserAgent
47+ ]
48+
49+data SignV4Data = SignV4Data {
50+ sv4SignTime :: UTCTime
51+ , sv4Scope :: ByteString
52+ , sv4CanonicalRequest :: ByteString
53+ , sv4HeadersToSign :: [(ByteString, ByteString)]
54+ , sv4Output :: [(ByteString, ByteString)]
55+ , sv4StringToSign :: ByteString
56+ , sv4SigningKey :: ByteString
57+ } deriving (Show)
58+
59+data SignParams = SignParams {
60+ spAccessKey :: Text
61+ , spSecretKey :: Text
62+ , spTimeStamp :: UTCTime
63+ , spRegion :: Maybe Text
64+ , spExpirySecs :: Maybe Int
65+ , spPayloadHash :: Maybe ByteString
66+ } deriving (Show)
67+
68+debugPrintSignV4Data :: SignV4Data -> IO ()
69+debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
70+ B8.putStrLn "SignV4Data:"
71+ B8.putStr "Timestamp: " >> print t
72+ B8.putStr "Scope: " >> B8.putStrLn s
73+ B8.putStrLn "Canonical Request:"
74+ B8.putStrLn cr
75+ B8.putStr "Headers to Sign: " >> print h2s
76+ B8.putStr "Output: " >> print o
77+ B8.putStr "StringToSign: " >> B8.putStrLn sts
78+ B8.putStr "SigningKey: " >> printBytes sk
79+ B8.putStrLn "END of SignV4Data ========="
80+ where
81+ printBytes b = do
82+ mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b
83+ B8.putStrLn ""
8384
8485mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
8586mkAuthHeader accessKey scope signedHeaderKeys sign =
86− let authValue =
87− B.concat
88− [ "AWS4-HMAC-SHA256 Credential=",
89− encodeUtf8 accessKey,
90− "/",
91− scope,
92− ", SignedHeaders=",
93− signedHeaderKeys,
94− ", Signature=",
95− sign
96− ]
97− in (H.hAuthorization, authValue)
98−
99−data IsStreaming = IsStreamingLength Int64 | NotStreaming
100− deriving stock (Eq, Show)
101−
102−amzSecurityToken :: ByteString
103−amzSecurityToken = "X-Amz-Security-Token"
87+ let authValue = B.concat
88+ [ "AWS4-HMAC-SHA256 Credential="
89+ , toS accessKey
90+ , "/"
91+ , scope
92+ , ", SignedHeaders="
93+ , signedHeaderKeys
94+ , ", Signature="
95+ , sign
96+ ]
97+ in (H.hAuthorization, authValue)
10498
10599-- | Given SignParams and request details, including request method,
106100-- request path, headers, query params and payload hash, generates an
@@ -114,212 +108,124 @@ amzSecurityToken = "X-Amz-Security-Token"
114108-- is being created. The expiry is interpreted as an integer number of
115109-- seconds. The output will be the list of query-parameters to add to
116110-- 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.
161−signV4 :: SignParams -> NC.Request -> [Header]
111+signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
162112signV4 !sp !req =
163− let scope = credentialScope sp
164−
165− -- extra headers to be added for signing purposes.
166− extraHeaders =
167− ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp)
168− : ( -- payload hash is only used for S3 (not STS)
169− [ ( "x-amz-content-sha256",
170− fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
171− )
172− | spService sp == ServiceS3
173− ]
174− )
175− ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
176−
177− -- 1. compute canonical request
178− reqHeaders = NC.requestHeaders req ++ extraHeaders
179− (canonicalRequest, signedHeaderKeys) =
180− getCanonicalRequestAndSignedHeaders
181− NotStreaming
182− sp
183− req
184− reqHeaders
185−
186− -- 2. compute string to sign
187− stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
188− -- 3.1 compute signing key
189− signingKey = getSigningKey sp
190− -- 3.2 compute signature
191− signature = computeSignature stringToSign signingKey
192− -- 4. compute auth header
193− authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
194− in authHeader : extraHeaders
195−
196−credentialScope :: SignParams -> ByteString
197−credentialScope sp =
198− let region = fromMaybe "" $ spRegion sp
199− in B.intercalate
200− "/"
201− [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" $ spTimeStamp sp,
202− encodeUtf8 region,
203− toByteString $ spService sp,
204− "aws4_request"
205− ]
113+ let
114+ region = fromMaybe "" $ spRegion sp
115+ ts = spTimeStamp sp
116+ scope = mkScope ts region
117+ accessKey = toS $ spAccessKey sp
118+ secretKey = toS $ spSecretKey sp
119+ expiry = spExpirySecs sp
120+ sha256Hdr = ("x-amz-content-sha256",
121+ fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp)
122+
123+ -- headers to be added to the request
124+ datePair = ("X-Amz-Date", awsTimeFormatBS ts)
125+ computedHeaders = NC.requestHeaders req ++
126+ if isJust $ expiry
127+ then []
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
146+ canonicalRequest = mkCanonicalRequest False sp
147+ (NC.setQueryString finalQP req)
148+ headersToSign
149+
150+ -- 2. compute string to sign
151+ stringToSign = mkStringToSign ts scope canonicalRequest
152+
153+ -- 3.1 compute signing key
154+ signingKey = mkSigningKey ts region secretKey
155+
156+ -- 3.2 compute signature
157+ signature = computeSignature stringToSign signingKey
158+
159+ -- 4. compute auth header
160+ authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
161+
162+ -- finally compute output pairs
163+ output = if isJust expiry
164+ then ("X-Amz-Signature", signature) : authQP
165+ else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
166+ datePair, sha256Hdr]
167+
168+ in output
169+
170+
171+mkScope :: UTCTime -> Text -> ByteString
172+mkScope ts region = B.intercalate "/"
173+ [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts
174+ , toS region
175+ , "s3"
176+ , "aws4_request"
177+ ]
206178
207−-- Folds header name, trims whitespace in header values, skips ignored headers
208−-- and sorts headers.
209179getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
210180getHeadersToSign !h =
211181 filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
212− map (bimap CI.foldedCase stripBS) h
213−
214−-- | Given the list of headers in the request, computes the canonical headers
215−-- and the signed headers strings.
216−getCanonicalHeaders :: NonEmpty Header -> (ByteString, ByteString)
217−getCanonicalHeaders h =
218− let -- Folds header name, trims spaces in header values, skips ignored
219− -- headers and sorts headers by name (we must not re-order multi-valued
220− -- headers).
221− headersToSign =
222− NE.toList $
223− NE.sortBy (\a b -> compare (fst a) (fst b)) $
224− NE.fromList $
225− NE.filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
226− NE.map (bimap CI.foldedCase stripBS) h
227−
228− canonicalHeaders = mconcat $ map (\(a, b) -> a <> ":" <> b <> "\n") headersToSign
229− signedHeaderKeys = B.intercalate ";" $ map fst headersToSign
230− in (canonicalHeaders, signedHeaderKeys)
231−
232−getCanonicalRequestAndSignedHeaders ::
233− IsStreaming ->
234− SignParams ->
235− NC.Request ->
236− [Header] ->
237− (ByteString, ByteString)
238−getCanonicalRequestAndSignedHeaders isStreaming sp req requestHeaders =
239− let httpMethod = NC.method req
240−
241− canonicalUri = uriEncode False $ NC.path req
242−
243− canonicalQueryString =
244− B.intercalate "&" $
245− map (\(x, y) -> B.concat [x, "=", y]) $
246− sort $
247− map
248− ( bimap (uriEncode True) (maybe "" (uriEncode True))
249− )
250− (parseQuery $ NC.queryString req)
251−
252− (canonicalHeaders, signedHeaderKeys) = getCanonicalHeaders $ NE.fromList requestHeaders
253− payloadHashStr =
254− case isStreaming of
255− IsStreamingLength _ -> "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
256− NotStreaming -> fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
257−
258− canonicalRequest =
259− B.intercalate
260− "\n"
261− [ httpMethod,
262− canonicalUri,
263− canonicalQueryString,
264− canonicalHeaders,
265− signedHeaderKeys,
266− payloadHashStr
267− ]
268− in (canonicalRequest, signedHeaderKeys)
269−
270−mkCanonicalRequest ::
271− Bool ->
272− SignParams ->
273− NC.Request ->
274− [(ByteString, ByteString)] ->
275− ByteString
182+ map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
183+
184+mkCanonicalRequest :: Bool -> SignParams -> NC.Request -> [(ByteString, ByteString)]
185+ -> ByteString
276186mkCanonicalRequest !isStreaming !sp !req !headersForSign =
277− let httpMethod = NC.method req
278− canonicalUri = uriEncode False $ NC.path req
279− canonicalQueryString =
280− B.intercalate "&" $
281− map (\(x, y) -> B.concat [x, "=", y]) $
282− sortBy (\a b -> compare (fst a) (fst b)) $
283− map
284− ( bimap (uriEncode True) (maybe "" (uriEncode True))
285− )
286− (parseQuery $ NC.queryString req)
287− sortedHeaders = sort headersForSign
288− canonicalHeaders =
289− B.concat $
290− map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
291− signedHeaders = B.intercalate ";" $ map fst sortedHeaders
292− payloadHashStr =
293− if isStreaming
294− then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
295− else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
296− in B.intercalate
297− "\n"
298− [ httpMethod,
299− canonicalUri,
300− canonicalQueryString,
301− canonicalHeaders,
302− signedHeaders,
303− payloadHashStr
304− ]
187+ let
188+ canonicalQueryString = B.intercalate "&" $
189+ map (\(x, y) -> B.concat [x, "=", y]) $
190+ sort $ map (\(x, y) ->
191+ (uriEncode True x, maybe "" (uriEncode True) y)) $
192+ (parseQuery $ NC.queryString req)
305193
306−mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
307−mkStringToSign ts !scope !canonicalRequest =
308− B.intercalate
309− "\n"
310− [ "AWS4-HMAC-SHA256",
311− awsTimeFormatBS ts,
312− scope,
313− hashSHA256 canonicalRequest
194+ sortedHeaders = sort headersForSign
195+
196+ canonicalHeaders = B.concat $
197+ map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
198+
199+ signedHeaders = B.intercalate ";" $ map fst sortedHeaders
200+
201+ payloadHashStr =
202+ if isStreaming
203+ then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
204+ else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
205+ in
206+ B.intercalate "\n"
207+ [ NC.method req
208+ , uriEncode False $ NC.path req
209+ , canonicalQueryString
210+ , canonicalHeaders
211+ , signedHeaders
212+ , payloadHashStr
314213 ]
315214
316−getSigningKey :: SignParams -> ByteString
317−getSigningKey sp =
318− hmacSHA256RawBS "aws4_request"
319− . hmacSHA256RawBS (toByteString $ spService sp)
320− . hmacSHA256RawBS (encodeUtf8 $ fromMaybe "" $ spRegion sp)
321− . hmacSHA256RawBS (awsDateFormatBS $ spTimeStamp sp)
322− $ B.concat ["AWS4", BA.convert $ spSecretKey sp]
215+mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
216+mkStringToSign ts !scope !canonicalRequest = B.intercalate "\n"
217+ [ "AWS4-HMAC-SHA256"
218+ , awsTimeFormatBS ts
219+ , scope
220+ , hashSHA256 canonicalRequest
221+ ]
222+
223+mkSigningKey :: UTCTime -> Text -> ByteString -> ByteString
224+mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"
225+ . hmacSHA256RawBS "s3"
226+ . hmacSHA256RawBS (toS region)
227+ . hmacSHA256RawBS (awsDateFormatBS ts)
228+ $ B.concat ["AWS4", secretKey]
323229
324230computeSignature :: ByteString -> ByteString -> ByteString
325231computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -327,168 +233,159 @@ computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
327233-- | Takes a validated Post Policy JSON bytestring, the signing time,
328234-- and ConnInfo and returns form-data for the POST upload containing
329235-- just the signature and the encoded post-policy.
330−signV4PostPolicy ::
331− ByteString ->
332− SignParams ->
333− Map.HashMap Text ByteString
236+signV4PostPolicy :: ByteString -> SignParams
237+ -> Map.HashMap Text ByteString
334238signV4PostPolicy !postPolicyJSON !sp =
335− let stringToSign = Base64.encode postPolicyJSON
336− signingKey = getSigningKey sp
337− signature = computeSignature stringToSign signingKey
338− in Map.fromList $
339− [ ("x-amz-signature", signature),
340− ("policy", stringToSign)
341− ]
342− ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
239+ let
240+ stringToSign = Base64.encode postPolicyJSON
241+ region = fromMaybe "" $ spRegion sp
242+ signingKey = mkSigningKey (spTimeStamp sp) region $ toS $ spSecretKey sp
243+ signature = computeSignature stringToSign signingKey
244+ in
245+ Map.fromList [ ("x-amz-signature", signature)
246+ , ("policy", stringToSign)
247+ ]
343248
344249chunkSizeConstant :: Int
345250chunkSizeConstant = 64 * 1024
346251
347252-- base16Len computes the number of bytes required to represent @n (> 0)@ in
348253-- hexadecimal.
349−base16Len :: (Integral a) => a -> Int
350−base16Len n
351− | n == 0 = 0
352− | otherwise = 1 + base16Len (n `div` 16)
254+base16Len :: Integral a => a -> Int
255+base16Len n | n == 0 = 0
256+ | otherwise = 1 + base16Len (n `div` 16)
353257
354258signedStreamLength :: Int64 -> Int64
355259signedStreamLength dataLen =
356− let chunkSzInt = fromIntegral chunkSizeConstant
357− (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
358− -- Structure of a chunk:
359− -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
360− encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
361− fullChunkSize = encodedChunkLen chunkSzInt
362− lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
363− finalChunkSize = 1 + 17 + 64 + 2 + 2
364− in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
365−
366−-- For streaming S3, we need to update the content-encoding header.
367−addContentEncoding :: [Header] -> [Header]
368−addContentEncoding hs =
369− -- assume there is at most one content-encoding header.
370− let (ceHdrs, others) = partition ((== hContentEncoding) . fst) hs
371− in maybe
372− (hContentEncoding, "aws-chunked")
373− (\(k, v) -> (k, v <> ",aws-chunked"))
374− (listToMaybe ceHdrs)
375− : others
376−
377−signV4Stream ::
378− Int64 ->
379− SignParams ->
380− NC.Request ->
381− (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
260+ let
261+ chunkSzInt = fromIntegral chunkSizeConstant
262+ (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
263+
264+
265+ -- Structure of a chunk:
266+ -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
267+ encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
268+ fullChunkSize = encodedChunkLen chunkSzInt
269+ lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
270+ finalChunkSize = 1 + 17 + 64 + 2 + 2
271+ in
272+ numChunks * fullChunkSize + lastChunkSize + finalChunkSize
273+
274+signV4Stream :: Int64 -> SignParams -> NC.Request
275+ -> (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
276+ -- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
382277signV4Stream !payloadLength !sp !req =
383− let ts = spTimeStamp sp
384−
385− -- compute the updated list of headers to be added for signing purposes.
386− signedContentLength = signedStreamLength payloadLength
387− extraHeaders =
388− [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
389− ("x-amz-decoded-content-length", showBS payloadLength),
390− ("content-length", showBS signedContentLength),
391− ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
278+ let
279+ ts = spTimeStamp sp
280+
281+ addContentEncoding hs =
282+ let ceMay = headMay $ filter (\(x, _) -> x == "content-encoding") hs
283+ in case ceMay of
284+ Nothing -> ("content-encoding", "aws-chunked") : hs
285+ Just (_, ce) -> ("content-encoding", ce <> ",aws-chunked") :
286+ filter (\(x, _) -> x /= "content-encoding") hs
287+
288+ -- headers to be added to the request
289+ datePair = ("X-Amz-Date", awsTimeFormatBS ts)
290+ computedHeaders = addContentEncoding $
291+ datePair : NC.requestHeaders req
292+
293+ -- headers specific to streaming signature
294+ signedContentLength = signedStreamLength payloadLength
295+ streamingHeaders :: [Header]
296+ streamingHeaders =
297+ [ ("x-amz-decoded-content-length", show payloadLength)
298+ , ("content-length", show signedContentLength )
299+ , ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
300+ ]
301+ headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders
302+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
303+ finalQP = parseQuery (NC.queryString req)
304+
305+ -- 1. Compute Seed Signature
306+ -- 1.1 Canonical Request
307+ canonicalReq = mkCanonicalRequest True sp
308+ (NC.setQueryString finalQP req)
309+ headersToSign
310+
311+ region = fromMaybe "" $ spRegion sp
312+ scope = mkScope ts region
313+ accessKey = spAccessKey sp
314+ secretKey = spSecretKey sp
315+
316+ -- 1.2 String toSign
317+ stringToSign = mkStringToSign ts scope canonicalReq
318+
319+ -- 1.3 Compute signature
320+ -- 1.3.1 compute signing key
321+ signingKey = mkSigningKey ts region $ toS secretKey
322+
323+ -- 1.3.2 Compute signature
324+ seedSignature = computeSignature stringToSign signingKey
325+
326+ -- 1.3.3 Compute Auth Header
327+ authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
328+
329+ -- 1.4 Updated headers for the request
330+ finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)
331+ -- headersToAdd = authHeader : datePair : streamingHeaders
332+
333+ toHexStr n = B8.pack $ printf "%x" n
334+
335+ (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
336+
337+ -- Function to compute string to sign for each chunk.
338+ chunkStrToSign prevSign currChunkHash =
339+ B.intercalate "\n"
340+ [ "AWS4-HMAC-SHA256-PAYLOAD"
341+ , awsTimeFormatBS ts
342+ , scope
343+ , prevSign
344+ , hashSHA256 ""
345+ , currChunkHash
392346 ]
393− ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
394− requestHeaders =
395− addContentEncoding $
396− foldr setHeader (NC.requestHeaders req) extraHeaders
397−
398− -- 1. Compute Seed Signature
399− -- 1.1 Canonical Request
400− (canonicalReq, signedHeaderKeys) =
401− getCanonicalRequestAndSignedHeaders
402− (IsStreamingLength payloadLength)
403− sp
404− req
405− requestHeaders
406−
407− scope = credentialScope sp
408− accessKey = spAccessKey sp
409− -- 1.2 String toSign
410− stringToSign = mkStringToSign ts scope canonicalReq
411− -- 1.3 Compute signature
412− -- 1.3.1 compute signing key
413− signingKey = getSigningKey sp
414− -- 1.3.2 Compute signature
415− seedSignature = computeSignature stringToSign signingKey
416− -- 1.3.3 Compute Auth Header
417− authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
418− -- 1.4 Updated headers for the request
419− finalReqHeaders = authHeader : requestHeaders
420− -- headersToAdd = authHeader : datePair : streamingHeaders
421−
422− toHexStr n = B8.pack $ printf "%x" n
423− (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
424− -- Function to compute string to sign for each chunk.
425− chunkStrToSign prevSign currChunkHash =
426− B.intercalate
427− "\n"
428− [ "AWS4-HMAC-SHA256-PAYLOAD",
429− awsTimeFormatBS ts,
430− scope,
431− prevSign,
432− hashSHA256 "",
433− currChunkHash
434− ]
435− -- Read n byte from upstream and return a strict bytestring.
436− mustTakeN n = do
437− bs <- LB.toStrict <$> (C.takeCE n C..| C.sinkLazy)
347+
348+ -- Read n byte from upstream and return a strict bytestring.
349+ mustTakeN n = do
350+ bs <- toS <$> (C.takeCE n C..| C.sinkLazy)
438351 when (B.length bs /= n) $
439− throwIO MErrVStreamingBodyUnexpectedEOF
352+ throwIO MErrVStreamingBodyUnexpectedEOF
440353 return bs
441− signerConduit n lps prevSign =
442− -- First case encodes a full chunk of length
443− -- 'chunkSizeConstant'.
444− if
445− | n > 0 -> do
446− bs <- mustTakeN chunkSizeConstant
447− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
448− nextSign = computeSignature strToSign signingKey
449− chunkBS =
450− toHexStr chunkSizeConstant
451− <> ";chunk-signature="
452− <> nextSign
453− <> "\r\n"
454− <> bs
455− <> "\r\n"
456− C.yield chunkBS
457− signerConduit (n - 1) lps nextSign
458−
459− -- Second case encodes the last chunk which is smaller than
460− -- 'chunkSizeConstant'
461− | lps > 0 -> do
462− bs <- mustTakeN $ fromIntegral lps
463− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
464− nextSign = computeSignature strToSign signingKey
465− chunkBS =
466− toHexStr lps
467− <> ";chunk-signature="
468− <> nextSign
469− <> "\r\n"
470− <> bs
471− <> "\r\n"
472− C.yield chunkBS
473− signerConduit 0 0 nextSign
474−
475− -- Last case encodes the final signature chunk that has no
476− -- data.
477− | otherwise -> do
478− let strToSign = chunkStrToSign prevSign (hashSHA256 "")
479− nextSign = computeSignature strToSign signingKey
480− lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
481− C.yield lastChunkBS
482− in \src ->
483− req
484− { NC.requestHeaders = finalReqHeaders,
485− NC.requestBody =
486− NC.requestBodySource signedContentLength $
487− src C..| signerConduit numParts lastPSize seedSignature
488− }
489−
490−-- "setHeader r hdr" adds the hdr to r, replacing it in r if it already exists.
491−setHeader :: Header -> RequestHeaders -> RequestHeaders
492−setHeader hdr r =
493− let r' = filter (\(name, _) -> name /= fst hdr) r
494− in hdr : r'
354+
355+ signerConduit n lps prevSign =
356+ -- First case encodes a full chunk of length
357+ -- 'chunkSizeConstant'.
358+ if | n > 0 -> do
359+ bs <- mustTakeN chunkSizeConstant
360+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
361+ nextSign = computeSignature strToSign signingKey
362+ chunkBS = toHexStr chunkSizeConstant
363+ <> ";chunk-signature="
364+ <> nextSign <> "\r\n" <> bs <> "\r\n"
365+ C.yield chunkBS
366+ signerConduit (n-1) lps nextSign
367+
368+ -- Second case encodes the last chunk which is smaller than
369+ -- 'chunkSizeConstant'
370+ | lps > 0 -> do
371+ bs <- mustTakeN $ fromIntegral lps
372+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
373+ nextSign = computeSignature strToSign signingKey
374+ chunkBS = toHexStr lps <> ";chunk-signature="
375+ <> nextSign <> "\r\n" <> bs <> "\r\n"
376+ C.yield chunkBS
377+ signerConduit 0 0 nextSign
378+
379+ -- Last case encodes the final signature chunk that has no
380+ -- data.
381+ | otherwise -> do
382+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")
383+ nextSign = computeSignature strToSign signingKey
384+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
385+ C.yield lastChunkBS
386+ in
387+ \src -> req { NC.requestHeaders = finalReqHeaders
388+ , NC.requestBody =
389+ NC.requestBodySource signedContentLength $
390+ src C..| signerConduit numParts lastPSize seedSignature
391+ }
src/Network/Minio/Utils.hs+127−140
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -16,77 +16,70 @@
1616
1717module Network.Minio.Utils where
1818
19−import qualified Conduit as C
20−import Control.Monad.IO.Unlift (MonadUnliftIO)
21−import qualified Control.Monad.Trans.Resource as R
22−import qualified Data.ByteString as B
23−import qualified Data.ByteString.Lazy as LB
24−import Data.CaseInsensitive (mk, original)
25−import qualified Data.Conduit.Binary as CB
26−import qualified Data.HashMap.Strict as H
27−import qualified Data.Text as T
28−import Data.Text.Read (decimal)
29−import Data.Time
30− ( defaultTimeLocale,
31− parseTimeM,
32− rfc822DateFormat,
33− )
34−import Lib.Prelude
35−import Network.HTTP.Conduit (Response)
36−import qualified Network.HTTP.Conduit as NC
37−import qualified Network.HTTP.Types as HT
38−import qualified Network.HTTP.Types.Header as Hdr
39−import Network.Minio.Data.ByteString
40−import Network.Minio.JsonParser (parseErrResponseJSON)
41−import Network.Minio.XmlCommon (parseErrResponse)
42−import qualified System.IO as IO
43−import qualified UnliftIO as U
44−import qualified UnliftIO.Async as A
45−
46−allocateReadFile ::
47− (MonadUnliftIO m, R.MonadResource m) =>
48− FilePath ->
49− m (R.ReleaseKey, Handle)
19+import qualified Conduit as C
20+import Control.Monad.IO.Unlift (MonadUnliftIO)
21+import qualified Control.Monad.Trans.Resource as R
22+import qualified Data.ByteString as B
23+import qualified Data.ByteString.Lazy as LB
24+import Data.CaseInsensitive (mk, original)
25+import qualified Data.Conduit.Binary as CB
26+import qualified Data.HashMap.Strict as H
27+import qualified Data.List as List
28+import qualified Data.Text as T
29+import Data.Text.Read (decimal)
30+import Data.Time (defaultTimeLocale, parseTimeM,
31+ rfc822DateFormat)
32+import Network.HTTP.Conduit (Response)
33+import qualified Network.HTTP.Conduit as NC
34+import qualified Network.HTTP.Types as HT
35+import qualified Network.HTTP.Types.Header as Hdr
36+import qualified System.IO as IO
37+import qualified UnliftIO as U
38+import qualified UnliftIO.Async as A
39+import qualified UnliftIO.MVar as UM
40+
41+import Lib.Prelude
42+
43+import Network.Minio.Data
44+import Network.Minio.Data.ByteString
45+import Network.Minio.JsonParser (parseErrResponseJSON)
46+import Network.Minio.XmlParser (parseErrResponse)
47+
48+allocateReadFile :: (MonadUnliftIO m, R.MonadResource m)
49+ => FilePath -> m (R.ReleaseKey, Handle)
5050allocateReadFile fp = do
5151 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
52− either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
52+ either (\(e :: IOException) -> throwIO e) (return . (rk,)) hdlE
5353 where
5454 openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
5555 cleanup = either (const $ return ()) IO.hClose
5656
5757-- | Queries the file size from the handle. Catches any file operation
5858-- exceptions and returns Nothing instead.
59−getFileSize ::
60− (MonadUnliftIO m) =>
61− Handle ->
62− m (Maybe Int64)
59+getFileSize :: (MonadUnliftIO m, R.MonadResource m)
60+ => Handle -> m (Maybe Int64)
6361getFileSize h = do
6462 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
6563 case resE of
66− Left (_ :: U.IOException) -> return Nothing
67− Right s -> return $ Just s
64+ Left (_ :: IOException) -> return Nothing
65+ Right s -> return $ Just s
6866
6967-- | Queries if handle is seekable. Catches any file operation
7068-- exceptions and return False instead.
71−isHandleSeekable ::
72− (R.MonadResource m) =>
73− Handle ->
74− m Bool
69+isHandleSeekable :: (R.MonadResource m, MonadUnliftIO m)
70+ => Handle -> m Bool
7571isHandleSeekable h = do
7672 resE <- liftIO $ try $ IO.hIsSeekable h
7773 case resE of
78− Left (_ :: U.IOException) -> return False
79− Right v -> return v
74+ Left (_ :: IOException) -> return False
75+ Right v -> return v
8076
8177-- | Helper function that opens a handle to the filepath and performs
8278-- the given action on it. Exceptions of type MError are caught and
8379-- returned - both during file handle allocation and when the action
8480-- is run.
85−withNewHandle ::
86− (MonadUnliftIO m, R.MonadResource m) =>
87− FilePath ->
88− (Handle -> m a) ->
89− m (Either U.IOException a)
81+withNewHandle :: (MonadUnliftIO m, R.MonadResource m)
82+ => FilePath -> (Handle -> m a) -> m (Either IOException a)
9083withNewHandle fp fileAction = do
9184 -- opening a handle can throw MError exception.
9285 handleE <- try $ allocateReadFile fp
@@ -100,61 +93,34 @@ withNewHandle fp fileAction = do
10093 return resE
10194
10295mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
103−mkHeaderFromPairs = map (first mk)
96+mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
10497
10598lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
106−lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
99+lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
107100
108101getETagHeader :: [HT.Header] -> Maybe Text
109102getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
110103
111104getMetadata :: [HT.Header] -> [(Text, Text)]
112105getMetadata =
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
106+ map ((\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y)))
124107
125108toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
126109toMaybeMetadataHeader (k, v) =
127− (,v) <$> userMetadataHeaderNameMaybe k
110+ (, v) <$> userMetadataHeaderNameMaybe k
128111
129112getNonUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
130−getNonUserMetadataMap =
131− H.fromList
132− . filter
133− ( isNothing
134− . userMetadataHeaderNameMaybe
135− . fst
136− )
137−
138−addXAmzMetaPrefix :: Text -> Text
139−addXAmzMetaPrefix s
140− | isJust (userMetadataHeaderNameMaybe s) = s
141− | otherwise = "X-Amz-Meta-" <> s
142−
143−mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
144−mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix x, encodeUtf8 y))
113+getNonUserMetadataMap = H.fromList
114+ . filter ( isNothing
115+ . userMetadataHeaderNameMaybe
116+ . fst
117+ )
145118
146119-- | This function collects all headers starting with `x-amz-meta-`
147120-- and strips off this prefix, and returns a map.
148121getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
149−getUserMetadataMap =
150− H.fromList
151− . mapMaybe toMaybeMetadataHeader
152−
153−getHostHeader :: (ByteString, Int) -> ByteString
154−getHostHeader (host_, port_) =
155− if port_ == 80 || port_ == 443
156− then host_
157− else host_ <> ":" <> show port_
122+getUserMetadataMap = H.fromList
123+ . mapMaybe toMaybeMetadataHeader
158124
159125getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
160126getLastModifiedHeader hs = do
@@ -164,21 +130,19 @@ getLastModifiedHeader hs = do
164130getContentLength :: [HT.Header] -> Maybe Int64
165131getContentLength hs = do
166132 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
167− fst <$> either (const Nothing) Just (decimal nbs)
133+ fst <$> hush (decimal nbs)
134+
168135
169136decodeUtf8Lenient :: ByteString -> Text
170137decodeUtf8Lenient = decodeUtf8With lenientDecode
171138
172139isSuccessStatus :: HT.Status -> Bool
173−isSuccessStatus sts =
174− let s = HT.statusCode sts
175− in (s >= 200 && s < 300)
176−
177−httpLbs ::
178− (MonadIO m) =>
179− NC.Request ->
180− NC.Manager ->
181− m (NC.Response LByteString)
140+isSuccessStatus sts = let s = HT.statusCode sts
141+ in (s >= 200 && s < 300)
142+
143+httpLbs :: MonadIO m
144+ => NC.Request -> NC.Manager
145+ -> m (NC.Response LByteString)
182146httpLbs req mgr = do
183147 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
184148 resp <- either throwIO return respE
@@ -190,26 +154,21 @@ httpLbs req mgr = do
190154 Just "application/json" -> do
191155 sErr <- parseErrResponseJSON $ NC.responseBody resp
192156 throwIO sErr
193− _ ->
194− throwIO $
195− NC.HttpExceptionRequest req $
196− NC.StatusCodeException (void resp) (showBS resp)
157+
158+ _ -> throwIO $ NC.HttpExceptionRequest req $
159+ NC.StatusCodeException (void resp) (show resp)
197160
198161 return resp
199162 where
200− tryHttpEx ::
201− IO (NC.Response LByteString) ->
202− IO (Either NC.HttpException (NC.Response LByteString))
163+ tryHttpEx :: IO (NC.Response LByteString)
164+ -> IO (Either NC.HttpException (NC.Response LByteString))
203165 tryHttpEx = try
204− contentTypeMay resp =
205− lookupHeader Hdr.hContentType $
206− NC.responseHeaders resp
207−
208−http ::
209− (MonadUnliftIO m, R.MonadResource m) =>
210− NC.Request ->
211− NC.Manager ->
212− m (Response (C.ConduitT () ByteString m ()))
166+ contentTypeMay resp = lookupHeader Hdr.hContentType $
167+ NC.responseHeaders resp
168+
169+http :: (MonadUnliftIO m, R.MonadResource m)
170+ => NC.Request -> NC.Manager
171+ -> m (Response (C.ConduitT () ByteString m ()))
213172http req mgr = do
214173 respE <- tryHttpEx $ NC.http req mgr
215174 resp <- either throwIO return respE
@@ -219,31 +178,25 @@ http req mgr = do
219178 respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
220179 sErr <- parseErrResponse respBody
221180 throwIO sErr
181+
222182 _ -> do
223183 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
224− throwIO $
225− NC.HttpExceptionRequest req $
226− NC.StatusCodeException (void resp) content
184+ throwIO $ NC.HttpExceptionRequest req $
185+ NC.StatusCodeException (void resp) content
186+
227187
228188 return resp
229189 where
230− tryHttpEx ::
231− (MonadUnliftIO m) =>
232− m a ->
233− m (Either NC.HttpException a)
190+ tryHttpEx :: (MonadUnliftIO m) => m a
191+ -> m (Either NC.HttpException a)
234192 tryHttpEx = try
235− contentTypeMay resp =
236− lookupHeader Hdr.hContentType $
237− NC.responseHeaders resp
193+ contentTypeMay resp = lookupHeader Hdr.hContentType $
194+ NC.responseHeaders resp
238195
239196-- Similar to mapConcurrently but limits the number of threads that
240197-- can run using a quantity semaphore.
241−limitedMapConcurrently ::
242− (MonadUnliftIO m) =>
243− Int ->
244− (t -> m a) ->
245− [t] ->
246− m [a]
198+limitedMapConcurrently :: MonadUnliftIO m
199+ => Int -> (t -> m a) -> [t] -> m [a]
247200limitedMapConcurrently 0 _ _ = return []
248201limitedMapConcurrently count act args = do
249202 t' <- U.newTVarIO count
@@ -252,15 +205,17 @@ limitedMapConcurrently count act args = do
252205 where
253206 wThread t arg =
254207 U.bracket_ (waitSem t) (signalSem t) $ act arg
208+
255209 -- quantity semaphore implementation using TVar
256210 waitSem t = U.atomically $ do
257211 v <- U.readTVar t
258212 if v > 0
259− then U.writeTVar t (v - 1)
260− else U.retrySTM
213+ then U.writeTVar t (v-1)
214+ else U.retrySTM
215+
261216 signalSem t = U.atomically $ do
262217 v <- U.readTVar t
263− U.writeTVar t (v + 1)
218+ U.writeTVar t (v+1)
264219
265220-- helper function to 'drop' empty optional parameter.
266221mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -269,7 +224,7 @@ mkQuery k mv = (k,) <$> mv
269224-- helper function to build query parameters that are optional.
270225-- don't use it with mandatory query params with empty value.
271226mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
272−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
227+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
273228
274229-- | Conduit that rechunks bytestrings into the given chunk
275230-- lengths. Stops after given chunk lengths are yielded. Stops if
@@ -277,9 +232,41 @@ mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
277232-- received. Does not throw any errors.
278233chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
279234chunkBSConduit [] = return ()
280−chunkBSConduit (s : ss) = do
281− bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
282− if
283− | B.length bs == s -> C.yield bs >> chunkBSConduit ss
284− | B.length bs > 0 -> C.yield bs
285− | otherwise -> return ()
235+chunkBSConduit (s:ss) = do
236+ bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
237+ if | B.length bs == s -> C.yield bs >> chunkBSConduit ss
238+ | B.length bs > 0 -> C.yield bs
239+ | otherwise -> return ()
240+
241+-- | Select part sizes - the logic is that the minimum part-size will
242+-- be 64MiB.
243+selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
244+selectPartSizes size = uncurry (List.zip3 [1..]) $
245+ List.unzip $ loop 0 size
246+ where
247+ ceil :: Double -> Int64
248+ ceil = ceiling
249+ partSize = max minPartSize (ceil $ fromIntegral size /
250+ fromIntegral maxMultipartParts)
251+
252+ m = fromIntegral partSize
253+ loop st sz
254+ | st > sz = []
255+ | st + m >= sz = [(st, sz - st)]
256+ | otherwise = (st, m) : loop (st + m) sz
257+
258+lookupRegionCache :: Bucket -> Minio (Maybe Region)
259+lookupRegionCache b = do
260+ rMVar <- asks mcRegionMap
261+ rMap <- UM.readMVar rMVar
262+ return $ H.lookup b rMap
263+
264+addToRegionCache :: Bucket -> Region -> Minio ()
265+addToRegionCache b region = do
266+ rMVar <- asks mcRegionMap
267+ UM.modifyMVar_ rMVar $ return . H.insert b region
268+
269+deleteFromRegionCache :: Bucket -> Minio ()
270+deleteFromRegionCache b = do
271+ rMVar <- asks mcRegionMap
272+ UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/XmlCommon.hsdeleted−65
@@ -1,65 +0,0 @@
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+99−165
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,112 +15,89 @@
1515--
1616
1717module Network.Minio.XmlGenerator
18− ( mkCreateBucketConfig,
19− mkCompleteMultipartUploadRequest,
20− mkPutNotificationRequest,
21− mkSelectRequest,
22− )
23−where
18+ ( mkCreateBucketConfig
19+ , mkCompleteMultipartUploadRequest
20+ , mkPutNotificationRequest
21+ , mkSelectRequest
22+ ) where
23+
2424
2525import qualified Data.ByteString.Lazy as LBS
26−import qualified Data.Text as T
27−import Network.Minio.Data
28−import Network.Minio.XmlCommon
29−import Text.XML
26+import qualified Data.HashMap.Strict as H
27+import qualified Data.Text as T
28+import Text.XML
29+
30+import Lib.Prelude
31+
32+import Network.Minio.Data
33+
3034
3135-- | Create a bucketConfig request body XML
3236mkCreateBucketConfig :: Text -> Region -> ByteString
3337mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
3438 where
35− s3Element n = Element (s3Name ns n) mempty
36− root =
37− s3Element
38− "CreateBucketConfiguration"
39− [ NodeElement $
40− s3Element
41− "LocationConstraint"
42− [NodeContent location]
39+ s3Element n = Element (s3Name ns n) mempty
40+ root = s3Element "CreateBucketConfiguration"
41+ [ NodeElement $ s3Element "LocationConstraint"
42+ [ NodeContent location]
4343 ]
44− bucketConfig = Document (Prologue [] Nothing []) root []
44+ bucketConfig = Document (Prologue [] Nothing []) root []
4545
4646-- | Create a completeMultipartUpload request body XML
4747mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
4848mkCompleteMultipartUploadRequest partInfo =
4949 LBS.toStrict $ renderLBS def cmur
5050 where
51− root =
52− Element "CompleteMultipartUpload" mempty $
53− map (NodeElement . mkPart) partInfo
54− mkPart (n, etag) =
55− Element
56− "Part"
57− mempty
58− [ NodeElement $
59− Element
60− "PartNumber"
61− mempty
62− [NodeContent $ T.pack $ show n],
63− NodeElement $
64− Element
65− "ETag"
66− mempty
67− [NodeContent etag]
68− ]
51+ root = Element "CompleteMultipartUpload" mempty $
52+ map (NodeElement . mkPart) partInfo
53+ mkPart (n, etag) = Element "Part" mempty
54+ [ NodeElement $ Element "PartNumber" mempty
55+ [NodeContent $ T.pack $ show n]
56+ , NodeElement $ Element "ETag" mempty
57+ [NodeContent etag]
58+ ]
6959 cmur = Document (Prologue [] Nothing []) root []
7060
7161-- Simplified XML representation without element attributes.
72−data XNode
73− = XNode Text [XNode]
74− | XLeaf Text Text
75− deriving stock (Eq, Show)
62+data XNode = XNode Text [XNode]
63+ | XLeaf Text Text
64+ deriving (Eq, Show)
7665
7766toXML :: Text -> XNode -> ByteString
78−toXML ns node =
79− LBS.toStrict $
80− renderLBS def $
81− Document (Prologue [] Nothing []) (xmlNode node) []
67+toXML ns node = LBS.toStrict $ renderLBS def $
68+ Document (Prologue [] Nothing []) (xmlNode node) []
8269 where
8370 xmlNode :: XNode -> Element
84− xmlNode (XNode name nodes) =
85− Element (s3Name ns name) mempty $
86− map (NodeElement . xmlNode) nodes
87− xmlNode (XLeaf name content) =
88− Element
89− (s3Name ns name)
90− mempty
91− [NodeContent content]
71+ xmlNode (XNode name nodes) = Element (s3Name ns name) mempty $
72+ map (NodeElement . xmlNode) nodes
73+ xmlNode (XLeaf name content) = Element (s3Name ns name) mempty
74+ [NodeContent content]
9275
9376class ToXNode a where
9477 toXNode :: a -> XNode
9578
9679instance ToXNode Event where
97− toXNode = XLeaf "Event" . toText
80+ toXNode = XLeaf "Event" . show
9881
9982instance ToXNode Notification where
100− toXNode (Notification qc tc lc) =
101− XNode "NotificationConfiguration" $
102− map (toXNodesWithArnName "QueueConfiguration" "Queue") qc
103− ++ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc
104− ++ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
83+ toXNode (Notification qc tc lc) = XNode "NotificationConfiguration" $
84+ map (toXNodesWithArnName "QueueConfiguration" "Queue") qc ++
85+ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc ++
86+ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
10587
10688toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
107−toXNodesWithArnName eltName arnName (NotificationConfig itemId arn events fRule) =
108− XNode eltName $
109− [XLeaf "Id" itemId, XLeaf arnName arn]
110− ++ map toXNode events
111− ++ [toXNode fRule]
89+toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
90+ XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events ++
91+ [toXNode fRule]
11292
11393instance ToXNode Filter where
11494 toXNode (Filter (FilterKey (FilterRules rules))) =
11595 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
11696
11797getFRXNode :: FilterRule -> XNode
118−getFRXNode (FilterRule n v) =
119− XNode
120− "FilterRule"
121− [ XLeaf "Name" n,
122− XLeaf "Value" v
123− ]
98+getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
99+ , XLeaf "Value" v
100+ ]
124101
125102mkPutNotificationRequest :: Text -> Notification -> ByteString
126103mkPutNotificationRequest ns = toXML ns . toXNode
@@ -129,103 +106,60 @@ mkSelectRequest :: SelectRequest -> ByteString
129106mkSelectRequest r = LBS.toStrict $ renderLBS def sr
130107 where
131108 sr = Document (Prologue [] Nothing []) root []
132− root =
133− Element "SelectRequest" mempty $
134− [ NodeElement
135− ( Element
136− "Expression"
137− mempty
138− [NodeContent $ srExpression r]
139− ),
140− NodeElement
141− ( Element
142− "ExpressionType"
143− mempty
144− [NodeContent $ show $ srExpressionType r]
145− ),
146− NodeElement
147− ( Element "InputSerialization" mempty $
148− inputSerializationNodes $
149− srInputSerialization r
150− ),
151− NodeElement
152− ( Element "OutputSerialization" mempty $
153− outputSerializationNodes $
154− srOutputSerialization r
155− )
156− ]
157− ++ maybe [] reqProgElem (srRequestProgressEnabled r)
158− reqProgElem enabled =
159− [ NodeElement
160− ( Element
161− "RequestProgress"
162− mempty
163− [ NodeElement
164− ( Element
165− "Enabled"
166− mempty
167− [ NodeContent
168− (if enabled then "TRUE" else "FALSE")
169− ]
170− )
171− ]
172− )
173− ]
174− inputSerializationNodes is =
175− comprTypeNode (isCompressionType is)
176− ++ [NodeElement $ formatNode (isFormatInfo is)]
177− comprTypeNode (Just c) =
178− [ NodeElement $
179− Element
180− "CompressionType"
181− mempty
182− [ NodeContent $ case c of
183− CompressionTypeNone -> "NONE"
184− CompressionTypeGzip -> "GZIP"
185− CompressionTypeBzip2 -> "BZIP2"
186− ]
187− ]
109+ root = Element "SelectRequest" mempty $
110+ [ NodeElement (Element "Expression" mempty
111+ [NodeContent $ srExpression r])
112+ , NodeElement (Element "ExpressionType" mempty
113+ [NodeContent $ show $ srExpressionType r])
114+ , NodeElement (Element "InputSerialization" mempty $
115+ inputSerializationNodes $ srInputSerialization r)
116+ , NodeElement (Element "OutputSerialization" mempty $
117+ outputSerializationNodes $ srOutputSerialization r)
118+ ] ++ maybe [] reqProgElem (srRequestProgressEnabled r)
119+ reqProgElem enabled = [NodeElement
120+ (Element "RequestProgress" mempty
121+ [NodeElement
122+ (Element "Enabled" mempty
123+ [NodeContent
124+ (if enabled then "TRUE" else "FALSE")]
125+ )
126+ ]
127+ )
128+ ]
129+ inputSerializationNodes is = comprTypeNode (isCompressionType is) ++
130+ [NodeElement $ formatNode (isFormatInfo is)]
131+ comprTypeNode (Just c) = [NodeElement $ Element "CompressionType" mempty
132+ [NodeContent $ case c of
133+ CompressionTypeNone -> "NONE"
134+ CompressionTypeGzip -> "GZIP"
135+ CompressionTypeBzip2 -> "BZIP2"
136+ ]
137+ ]
188138 comprTypeNode Nothing = []
139+
189140 kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
190− formatNode (InputFormatCSV c) =
191− Element
192− "CSV"
193− mempty
194− (map (NodeElement . kvElement) (csvPropsList c))
141+ formatNode (InputFormatCSV (CSVProp h)) =
142+ Element "CSV" mempty
143+ (map NodeElement $ map kvElement $ H.toList h)
195144 formatNode (InputFormatJSON p) =
196− Element
197− "JSON"
198− mempty
199− [ NodeElement
200− ( Element
201− "Type"
202− mempty
203− [ NodeContent $ case jsonipType p of
204− JSONTypeDocument -> "DOCUMENT"
205− JSONTypeLines -> "LINES"
206− ]
207− )
208− ]
145+ Element "JSON" mempty
146+ [NodeElement
147+ (Element "Type" mempty
148+ [NodeContent $ case jsonipType p of
149+ JSONTypeDocument -> "DOCUMENT"
150+ JSONTypeLines -> "LINES"
151+ ]
152+ )
153+ ]
209154 formatNode InputFormatParquet = Element "Parquet" mempty []
155+
210156 outputSerializationNodes (OutputSerializationJSON j) =
211− [ NodeElement
212− ( Element "JSON" mempty $
213− rdElem $
214− jsonopRecordDelimiter j
215− )
216− ]
217− outputSerializationNodes (OutputSerializationCSV c) =
218− [ NodeElement $
219− Element
220− "CSV"
221− mempty
222− (map (NodeElement . kvElement) (csvPropsList c))
223− ]
157+ [NodeElement (Element "JSON" mempty $
158+ rdElem $ jsonopRecordDelimiter j)]
159+ outputSerializationNodes (OutputSerializationCSV (CSVProp h)) =
160+ [NodeElement $ Element "CSV" mempty
161+ (map NodeElement $ map kvElement $ H.toList h)]
162+
224163 rdElem Nothing = []
225− rdElem (Just t) =
226− [ NodeElement $
227− Element
228− "RecordDelimiter"
229− mempty
230− [NodeContent t]
231− ]
164+ rdElem (Just t) = [NodeElement $ Element "RecordDelimiter" mempty
165+ [NodeContent t]]
src/Network/Minio/XmlParser.hs+154−107
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,38 +15,75 @@
1515--
1616
1717module Network.Minio.XmlParser
18− ( parseListBuckets,
19− parseLocation,
20− parseNewMultipartUpload,
21− parseCompleteMultipartUploadResponse,
22− parseCopyObjectResponse,
23− parseListObjectsResponse,
24− parseListObjectsV1Response,
25− parseListUploadsResponse,
26− parseListPartsResponse,
27− parseErrResponse,
28− parseNotification,
29− parseSelectProgress,
30− )
31−where
18+ ( parseListBuckets
19+ , parseLocation
20+ , parseNewMultipartUpload
21+ , parseCompleteMultipartUploadResponse
22+ , parseCopyObjectResponse
23+ , parseListObjectsResponse
24+ , parseListObjectsV1Response
25+ , parseListUploadsResponse
26+ , parseListPartsResponse
27+ , parseErrResponse
28+ , parseNotification
29+ , parseSelectProgress
30+ ) where
3231
3332import qualified Data.ByteString.Lazy as LB
34−import qualified Data.HashMap.Strict as H
35−import Data.List (zip4, zip6)
36−import qualified Data.Text as T
37−import Data.Time
38−import Network.Minio.Data
39−import Network.Minio.XmlCommon
40−import Text.XML.Cursor hiding (bool)
33+import qualified Data.HashMap.Strict as H
34+import Data.List (zip3, zip4, zip6)
35+import qualified Data.Text as T
36+import Data.Text.Read (decimal)
37+import Data.Time
38+import Text.XML
39+import Text.XML.Cursor hiding (bool)
40+
41+import Lib.Prelude
42+
43+import Network.Minio.Data
44+import Network.Minio.Errors
45+
46+
47+-- | Represent the time format string returned by S3 API calls.
48+s3TimeFormat :: [Char]
49+s3TimeFormat = iso8601DateFormat $ Just "%T%QZ"
50+
51+-- | Helper functions.
52+uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
53+uncurry4 f (a, b, c, d) = f a b c d
54+
55+uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
56+uncurry6 f (a, b, c, d, e, g) = f a b c d e g
57+
58+-- | Parse time strings from XML
59+parseS3XMLTime :: MonadIO m => Text -> m UTCTime
60+parseS3XMLTime t =
61+ maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
62+ parseTimeM True defaultTimeLocale s3TimeFormat $ T.unpack t
63+
64+parseDecimal :: (MonadIO m, Integral a) => Text -> m a
65+parseDecimal numStr = either (throwIO . MErrVXmlParse . show) return $
66+ fst <$> decimal numStr
67+
68+parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
69+parseDecimals numStr = forM numStr parseDecimal
70+
71+s3Elem :: Text -> Text -> Axis
72+s3Elem ns = element . s3Name ns
73+
74+parseRoot :: (MonadIO m) => LByteString -> m Cursor
75+parseRoot = either (throwIO . MErrVXmlParse . show) (return . fromDocument)
76+ . parseLBS def
4177
4278-- | Parse the response XML of a list buckets call.
4379parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
4480parseListBuckets xmldata = do
4581 r <- parseRoot xmldata
4682 ns <- asks getSvcNamespace
47− let s3Elem' = s3Elem ns
48− names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
49− timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
83+ let
84+ s3Elem' = s3Elem ns
85+ names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
86+ timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
5087
5188 times <- mapM parseS3XMLTime timeStrings
5289 return $ zipWith BucketInfo names times
@@ -79,38 +116,41 @@ parseCopyObjectResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =
79116parseCopyObjectResponse xmldata = do
80117 r <- parseRoot xmldata
81118 ns <- asks getSvcNamespace
82− let s3Elem' = s3Elem ns
83− mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
119+ let
120+ s3Elem' = s3Elem ns
121+ mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
84122
85123 mtime <- parseS3XMLTime mtimeStr
86124 return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
87125
88126-- | Parse the response XML of a list objects v1 call.
89−parseListObjectsV1Response ::
90− (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
91− LByteString ->
92− m ListObjectsV1Result
127+parseListObjectsV1Response :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
128+ => LByteString -> m ListObjectsV1Result
93129parseListObjectsV1Response xmldata = do
94130 r <- parseRoot xmldata
95131 ns <- asks getSvcNamespace
96− let s3Elem' = s3Elem ns
97− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
98− nextMarker = listToMaybe $ r $/ s3Elem' "NextMarker" &/ content
99− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
100− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
101− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
102− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
103− -- if response xml contains empty etag response fill them with as
104− -- many empty Text for the zip4 below to work as intended.
105− etags = etagsList ++ repeat ""
106− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
132+ let
133+ s3Elem' = s3Elem ns
134+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
135+
136+ nextMarker = headMay $ r $/ s3Elem' "NextMarker" &/ content
137+
138+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
139+
140+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
141+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
142+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
143+ -- if response xml contains empty etag response fill them with as
144+ -- many empty Text for the zip4 below to work as intended.
145+ etags = etagsList ++ repeat ""
146+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
107147
108148 modTimes <- mapM parseS3XMLTime modTimeStr
109149 sizes <- parseDecimals sizeStr
110150
111− let objects =
112− map (uncurry6 ObjectInfo) $
113− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
151+ let
152+ objects = map (uncurry6 ObjectInfo) $
153+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
114154
115155 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
116156
@@ -119,24 +159,28 @@ parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
119159parseListObjectsResponse xmldata = do
120160 r <- parseRoot xmldata
121161 ns <- asks getSvcNamespace
122− let s3Elem' = s3Elem ns
123− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
124− nextToken = listToMaybe $ r $/ s3Elem' "NextContinuationToken" &/ content
125− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
126− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
127− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
128− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
129− -- if response xml contains empty etag response fill them with as
130− -- many empty Text for the zip4 below to work as intended.
131− etags = etagsList ++ repeat ""
132− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
162+ let
163+ s3Elem' = s3Elem ns
164+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
165+
166+ nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ content
167+
168+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
169+
170+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
171+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
172+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
173+ -- if response xml contains empty etag response fill them with as
174+ -- many empty Text for the zip4 below to work as intended.
175+ etags = etagsList ++ repeat ""
176+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
133177
134178 modTimes <- mapM parseS3XMLTime modTimeStr
135179 sizes <- parseDecimals sizeStr
136180
137− let objects =
138− map (uncurry6 ObjectInfo) $
139− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
181+ let
182+ objects = map (uncurry6 ObjectInfo) $
183+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
140184
141185 return $ ListObjectsResult hasMore nextToken objects prefixes
142186
@@ -145,18 +189,20 @@ parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
145189parseListUploadsResponse xmldata = do
146190 r <- parseRoot xmldata
147191 ns <- asks getSvcNamespace
148− let s3Elem' = s3Elem ns
149− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
150− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
151− nextKey = listToMaybe $ r $/ s3Elem' "NextKeyMarker" &/ content
152− nextUpload = listToMaybe $ r $/ s3Elem' "NextUploadIdMarker" &/ content
153− uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
154− uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
155− uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
192+ let
193+ s3Elem' = s3Elem ns
194+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
195+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
196+ nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content
197+ nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content
198+ uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
199+ uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
200+ uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
156201
157202 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
158203
159− let uploads = zip3 uploadKeys uploadIds uploadInitTimes
204+ let
205+ uploads = zip3 uploadKeys uploadIds uploadInitTimes
160206
161207 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
162208
@@ -164,25 +210,34 @@ parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
164210parseListPartsResponse xmldata = do
165211 r <- parseRoot xmldata
166212 ns <- asks getSvcNamespace
167− let s3Elem' = s3Elem ns
168− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
169− nextPartNumStr = listToMaybe $ r $/ s3Elem' "NextPartNumberMarker" &/ content
170− partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
171− partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
172− partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
173− partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
213+ let
214+ s3Elem' = s3Elem ns
215+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
216+ nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content
217+ partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
218+ partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
219+ partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
220+ partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
174221
175222 partModTimes <- mapM parseS3XMLTime partModTimeStr
176223 partSizes <- parseDecimals partSizeStr
177224 partNumbers <- parseDecimals partNumberStr
178225 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
179226
180− let partInfos =
181− map (uncurry4 ObjectPartInfo) $
182− zip4 partNumbers partETags partSizes partModTimes
227+ let
228+ partInfos = map (uncurry4 ObjectPartInfo) $
229+ zip4 partNumbers partETags partSizes partModTimes
183230
184231 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
185232
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+
186241parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
187242parseNotification xmldata = do
188243 r <- parseRoot xmldata
@@ -191,40 +246,32 @@ parseNotification xmldata = do
191246 qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
192247 tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
193248 lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
194− Notification
195− <$> mapM (parseNode ns "Queue") qcfg
196− <*> mapM (parseNode ns "Topic") tcfg
197− <*> mapM (parseNode ns "CloudFunction") lcfg
249+ Notification <$> (mapM (parseNode ns "Queue") qcfg)
250+ <*> (mapM (parseNode ns "Topic") tcfg)
251+ <*> (mapM (parseNode ns "CloudFunction") lcfg)
198252 where
253+
199254 getFilterRule ns c =
200255 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
201256 value = T.concat $ c $/ s3Elem ns "Value" &/ content
202− in FilterRule name value
257+ in FilterRule name value
258+
203259 parseNode ns arnName nodeData = do
204260 let c = fromNode nodeData
205− itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
261+ id = T.concat $ c $/ s3Elem ns "Id" &/ content
206262 arn = T.concat $ c $/ s3Elem ns arnName &/ content
207− events = mapMaybe textToEvent (c $/ s3Elem ns "Event" &/ content)
208− rules =
209− c
210− $/ s3Elem ns "Filter"
211− &/ s3Elem ns "S3Key"
212− &/ s3Elem ns "FilterRule"
213− &| getFilterRule ns
214− return $
215− NotificationConfig
216− itemId
217− arn
218− events
219− (Filter $ FilterKey $ FilterRules rules)
220−
221−parseSelectProgress :: (MonadIO m) => ByteString -> m Progress
263+ events = catMaybes $ map textToEvent $ c $/ s3Elem ns "Event" &/ content
264+ rules = c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key" &/
265+ s3Elem ns "FilterRule" &| getFilterRule ns
266+ return $ NotificationConfig id arn events
267+ (Filter $ FilterKey $ FilterRules rules)
268+
269+parseSelectProgress :: MonadIO m => ByteString -> m Progress
222270parseSelectProgress xmldata = do
223− r <- parseRoot $ LB.fromStrict xmldata
224− let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
225− bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content
226− bReturned = T.concat $ r $/ element "BytesReturned" &/ content
227− Progress
228− <$> parseDecimal bScanned
229− <*> parseDecimal bProcessed
230− <*> parseDecimal bReturned
271+ r <- parseRoot $ LB.fromStrict xmldata
272+ let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
273+ bProcessed = T.concat $ r $/element "BytesProcessed" &/ content
274+ bReturned = T.concat $ r $/element "BytesReturned" &/ content
275+ Progress <$> parseDecimal bScanned
276+ <*> parseDecimal bProcessed
277+ <*> parseDecimal bReturned
stack.yaml+4−4
@@ -15,7 +15,7 @@
1515# resolver:
1616# name: custom-snapshot
1717# location: "./custom-snapshot.yaml"
18−resolver: lts-22.19
18+resolver: lts-14.6
1919
2020# User packages to be built.
2121# Various formats can be used as shown in the example below.
@@ -36,17 +36,17 @@ resolver: lts-22.19
3636# non-dependency (i.e. a user package), and its test suites and benchmarks
3737# will not be run. This is useful for tweaking upstream packages.
3838packages:
39− - "."
39+- '.'
4040# Dependency packages to be pulled from upstream that are not in the resolver
4141# (e.g., acme-missiles-0.3)
42−extra-deps:
43− - crypton-connection-0.3.2
42+extra-deps: []
4443
4544# Override default flag values for local packages and extra-deps
4645flags: {}
4746
4847# Extra package databases containing global packages
4948extra-package-dbs: []
49+
5050# Control whether we use the GHC we find on the path
5151# system-ghc: true
5252#
stack.yaml.lockdeleted−19
@@ -1,19 +0,0 @@
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+480−754
@@ -1,5 +1,6 @@
1+{-# LANGUAGE OverloadedStrings #-}
12--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
34--
45-- Licensed under the Apache License, Version 2.0 (the "License");
56-- you may not use this file except in compliance with the License.
@@ -14,34 +15,37 @@
1415-- limitations under the License.
1516--
1617
17−import Conduit (replicateC)
18−import qualified Conduit as C
19−import qualified Control.Monad.Trans.Resource as R
20−import qualified Data.ByteString as BS
21−import Data.Conduit (yield)
22−import qualified Data.Conduit.Binary as CB
23−import Data.Conduit.Combinators (sinkList)
24−import qualified Data.HashMap.Strict as H
25−import qualified Data.Text as T
26−import Data.Time (fromGregorian)
27−import qualified Data.Time as Time
28−import Lib.Prelude
18+import qualified Test.QuickCheck as Q
19+import Test.Tasty
20+import Test.Tasty.HUnit
21+import Test.Tasty.QuickCheck as QC
22+
23+import Conduit (replicateC)
24+import qualified Conduit as C
25+import qualified Control.Monad.Trans.Resource as R
26+import qualified Data.ByteString as BS
27+import Data.Conduit (yield)
28+import qualified Data.Conduit.Binary as CB
29+import Data.Conduit.Combinators (sinkList)
30+import qualified Data.HashMap.Strict as H
31+import qualified Data.Text as T
32+import Data.Time (fromGregorian)
33+import qualified Data.Time as Time
2934import qualified Network.HTTP.Client.MultipartFormData as Form
30−import qualified Network.HTTP.Conduit as NC
31−import qualified Network.HTTP.Types as HT
32−import Network.Minio
33−import Network.Minio.Credentials (Creds (CredsStatic))
34−import Network.Minio.Data
35−import Network.Minio.Data.Crypto
36−import Network.Minio.S3API
37−import Network.Minio.Utils
38−import System.Directory (getTemporaryDirectory)
39−import qualified System.Environment as Env
40−import qualified Test.QuickCheck as Q
41−import Test.Tasty
42−import Test.Tasty.HUnit
43−import Test.Tasty.QuickCheck as QC
44−import qualified UnliftIO.IO as UIO
35+import qualified Network.HTTP.Conduit as NC
36+import qualified Network.HTTP.Types as HT
37+import System.Directory (getTemporaryDirectory)
38+import System.Environment (lookupEnv)
39+import qualified System.IO as SIO
40+
41+import Lib.Prelude
42+
43+import Network.Minio
44+import Network.Minio.Data
45+import Network.Minio.Data.Crypto
46+import Network.Minio.PutObject
47+import Network.Minio.S3API
48+import Network.Minio.Utils
4549
4650main :: IO ()
4751main = defaultMain tests
@@ -49,107 +53,47 @@ main = defaultMain tests
4953tests :: TestTree
5054tests = testGroup "Tests" [liveServerUnitTests]
5155
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−
9756-- conduit that generates random binary stream of given length
98−randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
99−randomDataSrc = genBS
57+randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
58+randomDataSrc s' = genBS s'
10059 where
101− concatIt bs n =
102− BS.concat $
103− replicate (fromIntegral q) bs
104− ++ [BS.take (fromIntegral r) bs]
105− where
106− (q, r) = n `divMod` fromIntegral (BS.length bs)
60+ concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++
61+ [BS.take (fromIntegral r) bs]
62+ where (q, r) = n `divMod` fromIntegral (BS.length bs)
63+
10764 genBS s = do
10865 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
10966 let byteArr64 = BS.pack w8s
11067 if s < oneMiB
11168 then yield $ concatIt byteArr64 s
112− else do
113− yield $ concatIt byteArr64 oneMiB
114− genBS (s - oneMiB)
69+ else do yield $ concatIt byteArr64 oneMiB
70+ genBS (s - oneMiB)
11571
116−mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
72+mkRandFile :: R.MonadResource m => Int64 -> m FilePath
11773mkRandFile size = do
118− dir <- liftIO getTemporaryDirectory
74+ dir <- liftIO $ getTemporaryDirectory
11975 C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
12076
12177funTestBucketPrefix :: Text
12278funTestBucketPrefix = "miniohstest-"
12379
124−funTestWithBucketNoSTS ::
125− TestName ->
126− (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
127− TestTree
128−funTestWithBucketNoSTS t minioTest = testCaseSteps t $ \step -> do
129− -- generate a random name for the bucket
130− bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
131− let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
132− liftStep = liftIO . step
133− connInfo <- loadTestServerConnInfo
134− ret <- runMinio connInfo $ do
135− liftStep $ "Creating bucket for test - " ++ t
136− foundBucket <- bucketExists b
137− liftIO $ foundBucket @?= False
138− makeBucket b Nothing
139− minioTest liftStep b
140− deleteBucket b
141− isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
142−
143−funTestWithBucket ::
144− TestName ->
145− (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
146− TestTree
80+loadTestServer :: IO ConnectInfo
81+loadTestServer = do
82+ val <- lookupEnv "MINIO_LOCAL"
83+ isSecure <- lookupEnv "MINIO_SECURE"
84+ return $ case (val, isSecure) of
85+ (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"
86+ (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"
87+ (Nothing, _) -> minioPlayCI
88+
89+funTestWithBucket :: TestName
90+ -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree
14791funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
14892 -- generate a random name for the bucket
14993 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
15094 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
15195 liftStep = liftIO . step
152− connInfo <- loadTestServerConnInfo
96+ connInfo <- loadTestServer
15397 ret <- runMinio connInfo $ do
15498 liftStep $ "Creating bucket for test - " ++ t
15599 foundBucket <- bucketExists b
@@ -159,40 +103,26 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
159103 deleteBucket b
160104 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
161105
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−
173106liveServerUnitTests :: TestTree
174−liveServerUnitTests =
175− testGroup
176− "Unit tests against a live server"
177− [ basicTests,
178− listingTest,
179− highLevelListingTest,
180− lowLevelMultipartTest,
181− putObjectSizeTest,
182− putObjectNoSizeTest,
183− multipartTest,
184− putObjectContentTypeTest,
185− putObjectContentLanguageTest,
186− putObjectStorageClassTest,
187− putObjectUserMetadataTest,
188− getObjectTest,
189− copyObjectTests,
190− presignedUrlFunTest,
191− presignedPostPolicyFunTest,
192− bucketPolicyFunTest,
193− getNPutSSECTest,
194− assumeRoleRequestTest
195− ]
107+liveServerUnitTests = testGroup "Unit tests against a live server"
108+ [ basicTests
109+ , listingTest
110+ , highLevelListingTest
111+ , lowLevelMultipartTest
112+ , putObjectSizeTest
113+ , putObjectNoSizeTest
114+ , multipartTest
115+ , putObjectContentTypeTest
116+ , putObjectContentLanguageTest
117+ , putObjectStorageClassTest
118+ , putObjectUserMetadataTest
119+ , getObjectTest
120+ , copyObjectTests
121+ , presignedUrlFunTest
122+ , presignedPostPolicyFunTest
123+ , bucketPolicyFunTest
124+ , getNPutSSECTest
125+ ]
196126
197127basicTests :: TestTree
198128basicTests = funTestWithBucket "Basic tests" $
@@ -201,109 +131,83 @@ basicTests = funTestWithBucket "Basic tests" $
201131 buckets <- getService
202132 unless (length (filter (== bucket) $ map biName buckets) == 1) $
203133 liftIO $
204− assertFailure
205− ( "The bucket "
206− ++ show bucket
207− ++ " was expected to exist."
208− )
134+ assertFailure ("The bucket " ++ show bucket ++
135+ " was expected to exist.")
209136
210137 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
211138 mbE <- try $ makeBucket bucket Nothing
212139 case mbE of
213140 Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
214− _ -> return ()
141+ _ -> return ()
215142
216143 step "makeBucket with an invalid bucket name and check for appropriate exception."
217144 invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
218145 case invalidMBE of
219146 Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
220− _ -> return ()
147+ _ -> return ()
221148
222149 step "getLocation works"
223150 region <- getLocation bucket
224151 liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
225152
226153 step "singlepart putObject works"
227− testFilepath <- mkRandFile 200
228− fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
154+ fPutObject bucket "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
229155
230156 step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
231− fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
157+ fpE <- try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
232158 case fpE of
233159 Left exn -> liftIO $ exn @?= NoSuchBucket
234− _ -> return ()
160+ _ -> return ()
235161
236162 outFile <- mkRandFile 0
237163 step "simple fGetObject works"
238− fGetObject bucket "test-file" outFile defaultGetObjectOptions
164+ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions
239165
240166 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
241167 step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
242− resE <-
243− try $
244− fGetObject
245− bucket
246− "test-file"
247− outFile
248− defaultGetObjectOptions
249− { gooIfUnmodifiedSince = Just unmodifiedTime
250− }
168+ resE <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
169+ gooIfUnmodifiedSince = (Just unmodifiedTime)
170+ }
251171 case resE of
252172 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
253− _ -> return ()
173+ _ -> return ()
254174
255175 step "fGetObject an object with no matching etag, check for exception"
256− resE1 <-
257− try $
258− fGetObject
259− bucket
260− "test-file"
261− outFile
262− defaultGetObjectOptions
263− { gooIfMatch = Just "invalid-etag"
264− }
176+ resE1 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
177+ gooIfMatch = (Just "invalid-etag")
178+ }
265179 case resE1 of
266180 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
267− _ -> return ()
181+ _ -> return ()
268182
269183 step "fGetObject an object with no valid range, check for exception"
270− resE2 <-
271− try $
272− fGetObject
273− bucket
274− "test-file"
275− outFile
276− defaultGetObjectOptions
277− { gooRange = Just $ HT.ByteRangeFromTo 100 300
278− }
184+ resE2 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
185+ gooRange = (Just $ HT.ByteRangeFromTo 100 200)
186+ }
279187 case resE2 of
280− Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
281− _ -> return ()
188+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
189+ _ -> return ()
282190
283191 step "fGetObject on object with a valid range"
284− fGetObject
285− bucket
286− "test-file"
287− outFile
288− defaultGetObjectOptions
289− { gooRange = Just $ HT.ByteRangeFrom 1
290− }
192+ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
193+ gooRange = (Just $ HT.ByteRangeFrom 1)
194+ }
291195
292196 step "fGetObject a non-existent object and check for NoSuchKey exception"
293197 resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
294198 case resE3 of
295199 Left exn -> liftIO $ exn @?= NoSuchKey
296− _ -> return ()
200+ _ -> return ()
297201
298202 step "create new multipart upload works"
299203 uid <- newMultipartUpload bucket "newmpupload" []
300− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
204+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
301205
302206 step "abort a new multipart upload works"
303207 abortMultipartUpload bucket "newmpupload" uid
304208
305209 step "delete object works"
306− deleteObject bucket "test-file"
210+ deleteObject bucket "lsb-release"
307211
308212 step "statObject test"
309213 let object = "sample"
@@ -313,310 +217,257 @@ basicTests = funTestWithBucket "Basic tests" $
313217
314218 step "get metadata of the object"
315219 res <- statObject bucket object defaultGetObjectOptions
316− liftIO $ oiSize res @?= 0
220+ liftIO $ (oiSize res) @?= 0
317221
318222 step "delete object"
319223 deleteObject bucket object
320224
321225lowLevelMultipartTest :: TestTree
322226lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
323− \step bucket -> do
324− -- low-level multipart operation tests.
325− let object = "newmpupload"
326− mb15 = 15 * 1024 * 1024
227+ \step bucket -> do
228+ -- low-level multipart operation tests.
229+ let object = "newmpupload"
230+ mb15 = 15 * 1024 * 1024
327231
328− step "Prepare for low-level multipart tests."
329− step "create new multipart upload"
330− uid <- newMultipartUpload bucket object []
331− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
232+ step "Prepare for low-level multipart tests."
233+ step "create new multipart upload"
234+ uid <- newMultipartUpload bucket object []
235+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
332236
333− randFile <- mkRandFile mb15
237+ randFile <- mkRandFile mb15
334238
335− step "put object parts 1 of 1"
336− partInfo <-
337− UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
338− putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
239+ step "put object parts 1 of 1"
240+ h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
241+ partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
339242
340− step "complete multipart"
341− void $ completeMultipartUpload bucket object uid [partInfo]
243+ step "complete multipart"
244+ void $ completeMultipartUpload bucket object uid [partInfo]
342245
343− destFile <- mkRandFile 0
344− step "Retrieve the created object and check size"
345− fGetObject bucket object destFile defaultGetObjectOptions
346− gotSize <- withNewHandle destFile getFileSize
347− liftIO $
348− gotSize
349− == Right (Just mb15)
350− @? "Wrong file size of put file after getting"
246+ destFile <- mkRandFile 0
247+ step "Retrieve the created object and check size"
248+ fGetObject bucket object destFile defaultGetObjectOptions
249+ gotSize <- withNewHandle destFile getFileSize
250+ liftIO $ gotSize == Right (Just mb15) @?
251+ "Wrong file size of put file after getting"
351252
352− step "Cleanup actions"
353− removeObject bucket object
253+ step "Cleanup actions"
254+ removeObject bucket object
354255
355256putObjectSizeTest :: TestTree
356257putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
357258 \step bucket -> do
358− -- putObject test (conduit source, size specified)
359− let obj = "msingle"
360− mb1 = 1 * 1024 * 1024
259+ -- putObject test (conduit source, size specified)
260+ let obj = "msingle"
261+ mb1 = 1 * 1024 * 1024
361262
362− step "Prepare for putObject with from source with size."
363− rFile <- mkRandFile mb1
263+ step "Prepare for putObject with from source with size."
264+ rFile <- mkRandFile mb1
364265
365− step "Upload single file."
366− putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
266+ step "Upload single file."
267+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
367268
368− step "Retrieve and verify file size"
369− destFile <- mkRandFile 0
370− fGetObject bucket obj destFile defaultGetObjectOptions
371− gotSize <- withNewHandle destFile getFileSize
372− liftIO $
373− gotSize
374− == Right (Just mb1)
375− @? "Wrong file size of put file after getting"
269+ step "Retrieve and verify file size"
270+ destFile <- mkRandFile 0
271+ fGetObject bucket obj destFile defaultGetObjectOptions
272+ gotSize <- withNewHandle destFile getFileSize
273+ liftIO $ gotSize == Right (Just mb1) @?
274+ "Wrong file size of put file after getting"
376275
377− step "Cleanup actions"
378− deleteObject bucket obj
276+ step "Cleanup actions"
277+ deleteObject bucket obj
379278
380279putObjectNoSizeTest :: TestTree
381280putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
382281 \step bucket -> do
383− -- putObject test (conduit source, no size specified)
384− let obj = "mpart"
385− mb70 = 70 * 1024 * 1024
282+ -- putObject test (conduit source, no size specified)
283+ let obj = "mpart"
284+ mb70 = 70 * 1024 * 1024
386285
387− step "Prepare for putObject with from source without providing size."
388− rFile <- mkRandFile mb70
286+ step "Prepare for putObject with from source without providing size."
287+ rFile <- mkRandFile mb70
389288
390− step "Upload multipart file."
391− putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
289+ step "Upload multipart file."
290+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
392291
393− step "Retrieve and verify file size"
394− destFile <- mkRandFile 0
395− fGetObject bucket obj destFile defaultGetObjectOptions
396− gotSize <- withNewHandle destFile getFileSize
397− liftIO $
398− gotSize
399− == Right (Just mb70)
400− @? "Wrong file size of put file after getting"
292+ step "Retrieve and verify file size"
293+ destFile <- mkRandFile 0
294+ fGetObject bucket obj destFile defaultGetObjectOptions
295+ gotSize <- withNewHandle destFile getFileSize
296+ liftIO $ gotSize == Right (Just mb70) @?
297+ "Wrong file size of put file after getting"
401298
402− step "Cleanup actions"
403− deleteObject bucket obj
299+ step "Cleanup actions"
300+ deleteObject bucket obj
404301
405302highLevelListingTest :: TestTree
406303highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
407304 \step bucket -> do
408− step "High-level listObjects Test"
409− step "put 3 objects"
410− let extractObjectsFromList =
411− mapM
412− ( \case
413− ListItemObject o -> Just $ oiObject o
414− _ -> Nothing
415− )
416− extractObjectsAndDirsFromList =
417− map
418− ( \case
419− ListItemObject o -> oiObject o
420− ListItemPrefix d -> d
421− )
422− expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
423− expectedNonRecList = ["o4", "dir/"]
424−
425− testFilepath <- mkRandFile 200
426− forM_ expectedObjects $
427− \obj -> fPutObject bucket obj testFilepath defaultPutObjectOptions
428−
429− step "High-level listing of objects"
430− items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
431− liftIO $
432− assertEqual "Objects/Dirs match failed!" expectedNonRecList $
305+ step "High-level listObjects Test"
306+ step "put 3 objects"
307+ let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
308+ extractObjectsFromList os =
309+ mapM (\t -> case t of
310+ ListItemObject o -> Just $ oiObject o
311+ _ -> Nothing) os
312+ expectedNonRecList = ["o4", "dir/"]
313+ extractObjectsAndDirsFromList os =
314+ map (\t -> case t of
315+ ListItemObject o -> oiObject o
316+ ListItemPrefix d -> d) os
317+
318+ forM_ expectedObjects $
319+ \obj -> fPutObject bucket obj "/etc/lsb-release" defaultPutObjectOptions
320+
321+ step "High-level listing of objects"
322+ items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
323+ liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
433324 extractObjectsAndDirsFromList items
434325
435− step "High-level recursive listing of objects"
436− objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
326+ step "High-level recursive listing of objects"
327+ objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
437328
438− liftIO
439− $ assertEqual
440− "Objects match failed!"
441− (Just $ sort expectedObjects)
442− $ extractObjectsFromList objects
329+ liftIO $ assertEqual "Objects match failed!"
330+ (Just $ sort expectedObjects) $
331+ extractObjectsFromList objects
443332
444− step "High-level listing of objects (version 1)"
445− itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
446− liftIO $
447− assertEqual "Objects/Dirs match failed!" expectedNonRecList $
333+ step "High-level listing of objects (version 1)"
334+ itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
335+ liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
448336 extractObjectsAndDirsFromList itemsV1
449337
450− step "High-level recursive listing of objects (version 1)"
451− objectsV1 <-
452− C.runConduit $
453− listObjectsV1 bucket Nothing True
454− C..| sinkList
455−
456− liftIO
457− $ assertEqual
458− "Objects match failed!"
459− (Just $ sort expectedObjects)
460− $ extractObjectsFromList objectsV1
461−
462− let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
463− expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
464− step "High-level listing with prefix"
465− prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
466− liftIO
467− $ assertEqual
468− "Objects/Dirs under prefix match failed!"
469− expectedPrefListing
470− $ extractObjectsAndDirsFromList prefItems
471−
472− step "High-level listing with prefix recursive"
473− prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
474− liftIO
475− $ assertEqual
476− "Objects/Dirs under prefix match recursive failed!"
477− expectedPrefListingRec
478− $ extractObjectsFromList prefItemsRec
479−
480− step "High-level listing with prefix (version 1)"
481− prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
482− liftIO
483− $ assertEqual
484− "Objects/Dirs under prefix match failed!"
485− expectedPrefListing
486− $ extractObjectsAndDirsFromList prefItemsV1
487−
488− step "High-level listing with prefix recursive (version 1)"
489− prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
490− liftIO
491− $ assertEqual
492− "Objects/Dirs under prefix match recursive failed!"
493− expectedPrefListingRec
494− $ extractObjectsFromList prefItemsRecV1
495−
496− step "Cleanup actions"
497− forM_ expectedObjects $
498− \obj -> removeObject bucket obj
499−
500− step "High-level listIncompleteUploads Test"
501− let object = "newmpupload"
502− step "create 10 multipart uploads"
503− forM_ [1 .. 10 :: Int] $ \_ -> do
338+ step "High-level recursive listing of objects (version 1)"
339+ objectsV1 <- C.runConduit $ listObjectsV1 bucket Nothing True C..|
340+ sinkList
341+
342+ liftIO $ assertEqual "Objects match failed!"
343+ (Just $ sort expectedObjects) $
344+ extractObjectsFromList objectsV1
345+
346+ let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
347+ expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
348+ step "High-level listing with prefix"
349+ prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
350+ liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
351+ expectedPrefListing $ extractObjectsAndDirsFromList prefItems
352+
353+ step "High-level listing with prefix recursive"
354+ prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
355+ liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
356+ expectedPrefListingRec $ extractObjectsFromList prefItemsRec
357+
358+ step "High-level listing with prefix (version 1)"
359+ prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
360+ liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
361+ expectedPrefListing $ extractObjectsAndDirsFromList prefItemsV1
362+
363+ step "High-level listing with prefix recursive (version 1)"
364+ prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
365+ liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
366+ expectedPrefListingRec $ extractObjectsFromList prefItemsRecV1
367+
368+ step "Cleanup actions"
369+ forM_ expectedObjects $
370+ \obj -> removeObject bucket obj
371+
372+ step "High-level listIncompleteUploads Test"
373+ let object = "newmpupload"
374+ step "create 10 multipart uploads"
375+ forM_ [1..10::Int] $ \_ -> do
376+ uid <- newMultipartUpload bucket object []
377+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
378+
379+ step "High-level listing of incomplete multipart uploads"
380+ uploads <- C.runConduit $
381+ listIncompleteUploads bucket (Just "newmpupload") True C..|
382+ sinkList
383+ liftIO $ length uploads @?= 10
384+
385+ step "cleanup"
386+ forM_ uploads $ \(UploadInfo _ uid _ _) ->
387+ abortMultipartUpload bucket object uid
388+
389+ step "High-level listIncompleteParts Test"
390+ let mb5 = 5 * 1024 * 1024
391+
392+ step "create a multipart upload"
504393 uid <- newMultipartUpload bucket object []
505394 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
506395
507− step "High-level listing of incomplete multipart uploads"
508− uploads <-
509− C.runConduit $
510− listIncompleteUploads bucket (Just "newmpupload") True
511− C..| sinkList
512− liftIO $ length uploads @?= 10
513−
514− step "cleanup"
515− forM_ uploads $ \(UploadInfo _ uid _ _) ->
516− abortMultipartUpload bucket object uid
517−
518− step "High-level listIncompleteParts Test"
519− let mb5 = 5 * 1024 * 1024
520−
521− step "create a multipart upload"
522− uid <- newMultipartUpload bucket object []
523− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
524−
525− step "put object parts 1..10"
526− inputFile <- mkRandFile mb5
527− UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
528− forM_ [1 .. 10] $ \pnum ->
396+ step "put object parts 1..10"
397+ inputFile <- mkRandFile mb5
398+ h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
399+ forM_ [1..10] $ \pnum ->
529400 putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
530401
531− step "fetch list parts"
532− incompleteParts <-
533− C.runConduit $
534− listIncompleteParts bucket object uid
535− C..| sinkList
536− liftIO $ length incompleteParts @?= 10
402+ step "fetch list parts"
403+ incompleteParts <- C.runConduit $ listIncompleteParts bucket object uid
404+ C..| sinkList
405+ liftIO $ length incompleteParts @?= 10
537406
538− step "cleanup"
539− abortMultipartUpload bucket object uid
407+ step "cleanup"
408+ abortMultipartUpload bucket object uid
540409
541410listingTest :: TestTree
542411listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
543− step "listObjects' test"
544− step "put 10 objects"
545− let objects = (\s -> T.concat ["test-file-", T.pack (show s)]) <$> [1 .. 10 :: Int]
546−
547− testFilepath <- mkRandFile 200
548− forM_ [1 .. 10 :: Int] $ \s ->
549− fPutObject bucket (T.concat ["test-file-", T.pack (show s)]) testFilepath defaultPutObjectOptions
550−
551− step "Simple list"
552− res <- listObjects' bucket Nothing Nothing Nothing Nothing
553− let expectedObjects = sort objects
554− liftIO $
555− assertEqual
556− "Objects match failed!"
557− expectedObjects
558− (map oiObject $ lorObjects res)
559−
560− step "Simple list version 1"
561− resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
562− let expected =
563− sort $
564− map
565− ( T.concat
566− . ("test-file-" :)
567− . (: [])
568− . T.pack
569− . show
570− )
571− [1 .. 10 :: Int]
572− liftIO $
573− assertEqual
574− "Objects match failed!"
575− expected
576− (map oiObject $ lorObjects' resV1)
577−
578− step "Cleanup actions"
579− forM_ objects $ \obj -> deleteObject bucket obj
580−
581− step "listIncompleteUploads' test"
582− step "create 10 multipart uploads"
583− let object = "newmpupload"
584− forM_ [1 .. 10 :: Int] $ \_ -> do
585− uid <- newMultipartUpload bucket object []
586− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
412+ step "listObjects' test"
413+ step "put 10 objects"
414+ let objects = (\s ->T.concat ["lsb-release", T.pack (show s)]) <$> [1..10::Int]
415+
416+ forM_ [1..10::Int] $ \s ->
417+ fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release" defaultPutObjectOptions
418+
419+ step "Simple list"
420+ res <- listObjects' bucket Nothing Nothing Nothing Nothing
421+ let expectedObjects = sort objects
422+ liftIO $ assertEqual "Objects match failed!" expectedObjects
423+ (map oiObject $ lorObjects res)
424+
425+ step "Simple list version 1"
426+ resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
427+ let expected = sort $ map (T.concat .
428+ ("lsb-release":) .
429+ (\x -> [x]) .
430+ T.pack .
431+ show) [1..10::Int]
432+ liftIO $ assertEqual "Objects match failed!" expected
433+ (map oiObject $ lorObjects' resV1)
434+
435+ step "Cleanup actions"
436+ forM_ objects $ \obj -> deleteObject bucket obj
437+
438+ step "listIncompleteUploads' test"
439+ step "create 10 multipart uploads"
440+ let object = "newmpupload"
441+ forM_ [1..10::Int] $ \_ -> do
442+ uid <- newMultipartUpload bucket object []
443+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
444+
445+ step "list incomplete multipart uploads"
446+ incompleteUploads <- listIncompleteUploads' bucket (Just "newmpupload") Nothing
447+ Nothing Nothing Nothing
448+ liftIO $ (length $ lurUploads incompleteUploads) @?= 10
449+
450+ step "cleanup"
451+ forM_ (lurUploads incompleteUploads) $
452+ \(_, uid, _) -> abortMultipartUpload bucket object uid
453+
454+ step "Basic listIncompleteParts Test"
455+ let mb5 = 5 * 1024 * 1024
456+
457+ step "create a multipart upload"
458+ uid <- newMultipartUpload bucket object []
459+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
587460
588− step "list incomplete multipart uploads"
589− incompleteUploads <-
590− listIncompleteUploads'
591− bucket
592− (Just "newmpupload")
593− Nothing
594− Nothing
595− Nothing
596− Nothing
597− liftIO $ length (lurUploads incompleteUploads) @?= 10
598−
599− step "cleanup"
600− forM_ (lurUploads incompleteUploads) $
601− \(_, uid, _) -> abortMultipartUpload bucket object uid
602−
603− step "Basic listIncompleteParts Test"
604− let mb5 = 5 * 1024 * 1024
605−
606− step "create a multipart upload"
607− uid <- newMultipartUpload bucket object []
608− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
609−
610− step "put object parts 1..10"
611− inputFile <- mkRandFile mb5
612− UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
613− forM_ [1 .. 10] $ \pnum ->
614− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
615−
616− step "fetch list parts"
617− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
618− liftIO $ length (lprParts listPartsResult) @?= 10
619− abortMultipartUpload bucket object uid
461+ step "put object parts 1..10"
462+ inputFile <- mkRandFile mb5
463+ h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
464+ forM_ [1..10] $ \pnum ->
465+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
466+
467+ step "fetch list parts"
468+ listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
469+ liftIO $ (length $ lprParts listPartsResult) @?= 10
470+ abortMultipartUpload bucket object uid
620471
621472presignedUrlFunTest :: TestTree
622473presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -628,47 +479,29 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
628479 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
629480
630481 step "PUT object presigned URL - makePresignedUrl"
631− putUrl <-
632− makePresignedUrl
633− 3600
634− HT.methodPut
635− (Just bucket)
636− (Just obj)
637− (Just "us-east-1")
638− []
639− []
640−
641− -- print putUrl
482+ putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
483+ (Just obj) (Just "us-east-1") [] []
484+
642485 let size1 = 1000 :: Int64
643486 inputFile <- mkRandFile size1
644487
645488 -- attempt to upload using the presigned URL
646489 putResp <- putR size1 inputFile mgr putUrl
647− liftIO $
648− (NC.responseStatus putResp == HT.status200)
649− @? "presigned PUT failed"
490+ liftIO $ (NC.responseStatus putResp == HT.status200) @?
491+ "presigned PUT failed"
650492
651493 step "GET object presigned URL - makePresignedUrl"
652− getUrl <-
653− makePresignedUrl
654− 3600
655− HT.methodGet
656− (Just bucket)
657− (Just obj)
658− (Just "us-east-1")
659− []
660− []
494+ getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
495+ (Just obj) (Just "us-east-1") [] []
661496
662497 getResp <- getR mgr getUrl
663− liftIO $
664− (NC.responseStatus getResp == HT.status200)
665− @? "presigned GET failed"
498+ liftIO $ (NC.responseStatus getResp == HT.status200) @?
499+ "presigned GET failed"
666500
667501 -- read content from file to compare with response above
668502 bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
669− liftIO $
670− (bs == NC.responseBody getResp)
671− @? "presigned put and get got mismatched data"
503+ liftIO $ (bs == NC.responseBody getResp) @?
504+ "presigned put and get got mismatched data"
672505
673506 step "PUT object presigned - presignedPutObjectURL"
674507 putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
@@ -677,71 +510,66 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
677510 testFile <- mkRandFile size2
678511
679512 putResp2 <- putR size2 testFile mgr putUrl2
680− liftIO $
681− (NC.responseStatus putResp2 == HT.status200)
682− @? "presigned PUT failed (presignedPutObjectUrl)"
513+ liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
514+ "presigned PUT failed (presignedPutObjectUrl)"
683515
684516 step "HEAD object presigned URL - presignedHeadObjectUrl"
685517 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
686518
687− headResp <- do
688− let req = NC.parseRequest_ $ decodeUtf8 headUrl
689− NC.httpLbs (req {NC.method = HT.methodHead}) mgr
690− liftIO $
691− (NC.responseStatus headResp == HT.status200)
692− @? "presigned HEAD failed (presignedHeadObjectUrl)"
519+ headResp <- do let req = NC.parseRequest_ $ toS headUrl
520+ NC.httpLbs (req {NC.method = HT.methodHead}) mgr
521+ liftIO $ (NC.responseStatus headResp == HT.status200) @?
522+ "presigned HEAD failed (presignedHeadObjectUrl)"
693523
694524 -- check that header info is accurate
695525 let h = H.fromList $ NC.responseHeaders headResp
696526 cLen = H.lookupDefault "0" HT.hContentLength h
697− liftIO $ (cLen == showBS size2) @? "Head req returned bad content length"
527+ liftIO $ (cLen == show size2) @? "Head req returned bad content length"
698528
699529 step "GET object presigned URL - presignedGetObjectUrl"
700530 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
701531
702532 getResp2 <- getR mgr getUrl2
703− liftIO $
704− (NC.responseStatus getResp2 == HT.status200)
705− @? "presigned GET failed (presignedGetObjectUrl)"
533+ liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
534+ "presigned GET failed (presignedGetObjectUrl)"
706535
707536 -- read content from file to compare with response above
708537 bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
709− liftIO $
710− (bs2 == NC.responseBody getResp2)
711− @? "presigned put and get got mismatched data (presigned*Url)"
538+ liftIO $ (bs2 == NC.responseBody getResp2) @?
539+ "presigned put and get got mismatched data (presigned*Url)"
540+
712541
713542 mapM_ (removeObject bucket) [obj, obj2]
714543 where
715544 putR size filePath mgr url = do
716− let req = NC.parseRequest_ $ decodeUtf8 url
717− let req' =
718− req
719− { NC.method = HT.methodPut,
720− NC.requestBody =
721− NC.requestBodySource size $
722− CB.sourceFile filePath
723− }
545+ let req = NC.parseRequest_ $ toS url
546+ let req' = req { NC.method = HT.methodPut
547+ , NC.requestBody = NC.requestBodySource size $
548+ CB.sourceFile filePath}
724549 NC.httpLbs req' mgr
550+
725551 getR mgr url = do
726− let req = NC.parseRequest_ $ decodeUtf8 url
552+ let req = NC.parseRequest_ $ toS url
727553 NC.httpLbs req mgr
728554
729555presignedPostPolicyFunTest :: TestTree
730−presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
556+presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
731557 \step bucket -> do
558+
732559 step "presignedPostPolicy basic test"
733− now <- liftIO Time.getCurrentTime
560+ now <- liftIO $ Time.getCurrentTime
734561
735562 let key = "presignedPostPolicyTest/myfile"
736− policyConds =
737− [ ppCondBucket bucket,
738− ppCondKey key,
739− ppCondContentLengthRange 1 1000,
740− ppCondContentType "application/octet-stream",
741− ppCondSuccessActionStatus 200
742− ]
563+ policyConds = [ ppCondBucket bucket
564+ , ppCondKey key
565+ , ppCondContentLengthRange 1 1000
566+ , ppCondContentType "application/octet-stream"
567+ , ppCondSuccessActionStatus 200
568+ ]
569+
743570 expirationTime = Time.addUTCTime 3600 now
744571 postPolicyE = newPostPolicy expirationTime policyConds
572+
745573 size = 1000 :: Int64
746574
747575 inputFile <- mkRandFile size
@@ -752,35 +580,35 @@ presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests
752580 (url, formData) <- presignedPostPolicy postPolicy
753581 -- liftIO (print url) >> liftIO (print formData)
754582 result <- liftIO $ postForm url formData inputFile
755− liftIO $
756− (NC.responseStatus result == HT.status200)
757− @? "presigned POST failed"
583+ liftIO $ (NC.responseStatus result == HT.status200) @?
584+ "presigned POST failed"
758585
759586 mapM_ (removeObject bucket) [key]
760− where
761− postForm url formData inputFile = do
762− req <- NC.parseRequest $ decodeUtf8 url
763− let parts =
764− map (uncurry Form.partBS) $
765− H.toList formData
766− parts' = parts ++ [Form.partFile "file" inputFile]
767− req' <- Form.formDataBody parts' req
768− mgr <- NC.newManager NC.tlsManagerSettings
769− NC.httpLbs req' mgr
587+ where
588+
589+ postForm url formData inputFile = do
590+ req <- NC.parseRequest $ toS url
591+ let parts = map (\(x, y) -> Form.partBS x y) $
592+ H.toList formData
593+ parts' = parts ++ [Form.partFile "file" inputFile]
594+ req' <- Form.formDataBody parts' req
595+ mgr <- NC.newManager NC.tlsManagerSettings
596+ NC.httpLbs req' mgr
770597
771598bucketPolicyFunTest :: TestTree
772599bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
773600 \step bucket -> do
601+
774602 step "bucketPolicy basic test - no policy exception"
775603 resE <- try $ getBucketPolicy bucket
776604 case resE of
777605 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
778− _ -> return ()
606+ _ -> return ()
779607
780608 resE' <- try $ setBucketPolicy bucket T.empty
781609 case resE' of
782610 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
783− _ -> return ()
611+ _ -> return ()
784612
785613 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/*\"]}]}"
786614
@@ -788,7 +616,7 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
788616 resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
789617 case resE'' of
790618 Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
791− _ -> return ()
619+ _ -> return ()
792620
793621 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 <> "/*\"]}]}"
794622
@@ -803,22 +631,13 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
803631 step "verify bucket policy: (2) get `myobject` anonymously"
804632 connInfo <- asks mcConnInfo
805633 let proto = bool "http://" "https://" $ connectIsSecure connInfo
806− url =
807− BS.concat
808− [ proto,
809− getHostAddr connInfo,
810− "/",
811− encodeUtf8 bucket,
812− "/",
813− encodeUtf8 obj
814− ]
815− respE <-
816− liftIO $
817− fmap (Right . toStrictBS) (NC.simpleHttp $ decodeUtf8 url)
818− `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
634+ url = BS.concat [proto, getHostAddr connInfo, "/", toS bucket,
635+ "/", toS obj]
636+ respE <- liftIO $ (fmap (Right . toS) $ NC.simpleHttp $ toS url) `catch`
637+ (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
819638 case respE of
820− Left err -> liftIO $ assertFailure $ show err
821− Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
639+ Left err -> liftIO $ assertFailure $ show err
640+ Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")
822641
823642 deleteObject bucket obj
824643
@@ -828,28 +647,25 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
828647multipartTest :: TestTree
829648multipartTest = funTestWithBucket "Multipart Tests" $
830649 \step bucket -> do
831− -- Commenting out test since it's platform specific.
832− -- FIXME: Need to find a platform agnostic way to test this.
833− -- step "Prepare for putObjectInternal with non-seekable file, with size."
834− -- step "Upload multipart file."
835− -- let mb80 = 80 * 1024 * 1024
836− -- obj = "mpart"
650+ step "Prepare for putObjectInternal with non-seekable file, with size."
651+ step "Upload multipart file."
652+ let mb80 = 80 * 1024 * 1024
653+ obj = "mpart"
837654
838− -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
655+ void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
839656
840− -- step "Retrieve and verify file size"
841− -- destFile <- mkRandFile 0
842− -- fGetObject bucket obj destFile defaultGetObjectOptions
843− -- gotSize <- withNewHandle destFile getFileSize
844− -- liftIO $
845− -- gotSize == Right (Just mb80)
846− -- @? "Wrong file size of put file after getting"
657+ step "Retrieve and verify file size"
658+ destFile <- mkRandFile 0
659+ fGetObject bucket obj destFile defaultGetObjectOptions
660+ gotSize <- withNewHandle destFile getFileSize
661+ liftIO $ gotSize == Right (Just mb80) @?
662+ "Wrong file size of put file after getting"
847663
848− -- step "Cleanup actions"
849− -- removeObject bucket obj
664+ step "Cleanup actions"
665+ removeObject bucket obj
850666
851− -- step "cleanup"
852− -- removeObject bucket "big"
667+ step "cleanup"
668+ removeObject bucket "big"
853669
854670 step "Prepare for removeIncompleteUpload"
855671 -- low-level multipart operation tests.
@@ -863,17 +679,15 @@ multipartTest = funTestWithBucket "Multipart Tests" $
863679 randFile <- mkRandFile kb5
864680
865681 step "upload 2 parts"
866− forM_ [1, 2] $ \partNum -> do
867− UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
868− void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
682+ forM_ [1,2] $ \partNum -> do
683+ h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
684+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
869685
870686 step "remove ongoing upload"
871687 removeIncompleteUpload bucket object
872− uploads <-
873− C.runConduit $
874− listIncompleteUploads bucket (Just object) False
875− C..| sinkList
876− liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
688+ uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
689+ C..| sinkList
690+ liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
877691
878692putObjectContentTypeTest :: TestTree
879693putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
@@ -884,13 +698,9 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
884698
885699 step "create server object with content-type"
886700 inputFile <- mkRandFile size1
887− fPutObject
888− bucket
889− object
890− inputFile
891− defaultPutObjectOptions
892− { pooContentType = Just "application/javascript"
893− }
701+ fPutObject bucket object inputFile defaultPutObjectOptions {
702+ pooContentType = Just "application/javascript"
703+ }
894704
895705 -- retrieve obj info to check
896706 oi <- headObject bucket object []
@@ -900,23 +710,16 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
900710 liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
901711
902712 step "upload object with content-encoding set to identity"
903− fPutObject
904− bucket
905− object
906− inputFile
907− defaultPutObjectOptions
908− { pooContentEncoding = Just "identity"
909− }
713+ fPutObject bucket object inputFile defaultPutObjectOptions {
714+ pooContentEncoding = Just "identity"
715+ }
910716
911717 oiCE <- headObject bucket object []
912718 let m' = oiMetadata oiCE
913719
914720 step "Validate content-encoding"
915− liftIO $
916− assertEqual
917− "Content-Encoding did not match"
918− (Just "identity")
919− (H.lookup "Content-Encoding" m')
721+ liftIO $ assertEqual "Content-Encoding did not match" (Just "identity")
722+ (H.lookup "Content-Encoding" m')
920723
921724 step "Cleanup actions"
922725
@@ -931,24 +734,17 @@ putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage test
931734
932735 step "create server object with content-language"
933736 inputFile <- mkRandFile size1
934− fPutObject
935− bucket
936− object
937− inputFile
938− defaultPutObjectOptions
939− { pooContentLanguage = Just "en-US"
940− }
737+ fPutObject bucket object inputFile defaultPutObjectOptions {
738+ pooContentLanguage = Just "en-US"
739+ }
941740
942741 -- retrieve obj info to check
943742 oi <- headObject bucket object []
944743 let m = oiMetadata oi
945744
946745 step "Validate content-language"
947− liftIO $
948− assertEqual
949− "content-language did not match"
950− (Just "en-US")
951− (H.lookup "Content-Language" m)
746+ liftIO $ assertEqual "content-language did not match" (Just "en-US")
747+ (H.lookup "Content-Language" m)
952748 step "Cleanup actions"
953749
954750 removeObject bucket object
@@ -963,26 +759,19 @@ putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
963759 step "create server object with usermetdata"
964760 inputFile <- mkRandFile size1
965761
966− fPutObject
967− bucket
968− object
969− inputFile
970− defaultPutObjectOptions
971− { pooUserMetadata =
972− [ ("x-Amz-meta-mykey1", "myval1"),
973− ("mykey2", "myval2")
974− ]
975− }
762+ fPutObject bucket object inputFile defaultPutObjectOptions {
763+ pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")
764+ , ("mykey2", "myval2")
765+ ]
766+ }
976767
977768 step "Validate user-metadata"
978769 -- retrieve obj info to check
979770 oi <- headObject bucket object []
980771 let m = oiUserMetadata oi
981772 -- need to do a case-insensitive comparison
982− sortedMeta =
983− sort $
984− map (bimap T.toLower T.toLower) $
985− H.toList m
773+ sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
774+ H.toList m
986775 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
987776
988777 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -998,26 +787,19 @@ getObjectTest = funTestWithBucket "getObject test" $
998787 size1 = 100 :: Int64
999788
1000789 inputFile <- mkRandFile size1
1001− fPutObject
1002− bucket
1003− object
1004− inputFile
1005− defaultPutObjectOptions
1006− { pooUserMetadata =
1007− [ ("x-Amz-meta-mykey1", "myval1"),
1008− ("mykey2", "myval2")
1009− ]
1010− }
790+ fPutObject bucket object inputFile defaultPutObjectOptions {
791+ pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")
792+ , ("mykey2", "myval2")
793+ ]
794+ }
1011795
1012796 step "get the object - check the metadata matches"
1013797 -- retrieve obj info to check
1014798 gor <- getObject bucket object defaultGetObjectOptions
1015799 let m = oiUserMetadata $ gorObjectInfo gor
1016800 -- need to do a case-insensitive comparison
1017− sortedMeta =
1018− sort $
1019− map (bimap T.toLower T.toLower) $
1020− H.toList m
801+ sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
802+ H.toList m
1021803 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
1022804
1023805 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -1045,21 +827,13 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
1045827 inputFile' <- mkRandFile size1
1046828 inputFile'' <- mkRandFile size0
1047829
1048− fPutObject
1049− bucket
1050− object
1051− inputFile
1052− defaultPutObjectOptions
1053− { pooStorageClass = Just "STANDARD"
1054− }
1055−
1056− fPutObject
1057− bucket
1058− object'
1059− inputFile'
1060− defaultPutObjectOptions
1061− { pooStorageClass = Just "REDUCED_REDUNDANCY"
1062− }
830+ fPutObject bucket object inputFile defaultPutObjectOptions {
831+ pooStorageClass = Just "STANDARD"
832+ }
833+
834+ fPutObject bucket object' inputFile' defaultPutObjectOptions {
835+ pooStorageClass = Just "REDUCED_REDUNDANCY"
836+ }
1063837
1064838 removeObject bucket object
1065839
@@ -1068,24 +842,15 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
1068842 let m' = oiMetadata oi'
1069843
1070844 step "Validate x-amz-storage-class rrs"
1071− liftIO $
1072− assertEqual
1073− "storageClass did not match"
1074− (Just "REDUCED_REDUNDANCY")
1075− (H.lookup "X-Amz-Storage-Class" m')
1076−
1077− fpE <-
1078− try $
1079− fPutObject
1080− bucket
1081− object''
1082− inputFile''
1083− defaultPutObjectOptions
1084− { pooStorageClass = Just "INVALID_STORAGE_CLASS"
1085− }
845+ liftIO $ assertEqual "storageClass did not match" (Just "REDUCED_REDUNDANCY")
846+ (H.lookup "X-Amz-Storage-Class" m')
847+
848+ fpE <- try $ fPutObject bucket object'' inputFile'' defaultPutObjectOptions {
849+ pooStorageClass = Just "INVALID_STORAGE_CLASS"
850+ }
1086851 case fpE of
1087852 Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
1088− _ -> return ()
853+ _ -> return ()
1089854
1090855 step "Cleanup actions"
1091856
@@ -1104,7 +869,7 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1104869 fPutObject bucket object inputFile defaultPutObjectOptions
1105870
1106871 step "copy object"
1107− let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
872+ let srcInfo = defaultSourceInfo { srcBucket = bucket, srcObject = object}
1108873 (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
1109874
1110875 -- retrieve obj info to check
@@ -1115,9 +880,8 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1115880
1116881 let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
1117882
1118− liftIO $
1119− (s == size1 && e == etag && isMTimeDiffOk)
1120− @? "Copied object did not match expected."
883+ liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
884+ "Copied object did not match expected."
1121885
1122886 step "cleanup actions"
1123887 removeObject bucket object
@@ -1138,18 +902,12 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1138902 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
1139903
1140904 step "put object parts 1-3"
1141− let srcInfo' = defaultSourceInfo {srcBucket = bucket, srcObject = srcObj}
1142− dstInfo' = defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1143− parts <- forM [1 .. 3] $ \p -> do
1144− (etag', _) <-
1145− copyObjectPart
1146− dstInfo'
1147− srcInfo'
1148− { srcRange = Just $ (,) ((p - 1) * mb5) ((p - 1) * mb5 + (mb5 - 1))
1149− }
1150− uid
1151− (fromIntegral p)
1152− []
905+ let srcInfo' = defaultSourceInfo { srcBucket = bucket, srcObject = srcObj }
906+ dstInfo' = defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj }
907+ parts <- forM [1..3] $ \p -> do
908+ (etag', _) <- copyObjectPart dstInfo' srcInfo'{
909+ srcRange = Just $ (,) ((p-1)*mb5) ((p-1)*mb5 + (mb5 - 1))
910+ } uid (fromIntegral p) []
1153911 return (fromIntegral p, etag')
1154912
1155913 step "complete multipart"
@@ -1195,13 +953,11 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1195953 fPutObject bucket src inputFile' defaultPutObjectOptions
1196954
1197955 step "copy last 10MiB of object"
1198− copyObject
1199− defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1200− defaultSourceInfo
1201− { srcBucket = bucket,
1202− srcObject = src,
1203− srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
1204− }
956+ copyObject defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj } defaultSourceInfo {
957+ srcBucket = bucket
958+ , srcObject = src
959+ , srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
960+ }
1205961
1206962 step "verify uploaded object"
1207963 cSize <- oiSize <$> headObject bucket copyObj []
@@ -1212,72 +968,42 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
1212968
1213969getNPutSSECTest :: TestTree
1214970getNPutSSECTest =
1215− funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
1216− -- Skip this test if the server is not using TLS as encryption is
1217− -- disabled anyway.
1218− isTLSConn <- asks (connectIsSecure . mcConnInfo)
1219− if isTLSConn
1220− then do
1221− step "Make an encryption key"
1222− key <- case mkSSECKey $ BS.pack [0 .. 31] of
1223− Nothing -> liftIO $ assertFailure "This should not happen"
1224− Just k -> return k
1225−
1226− let mb1 = 1024 * 1024
1227− obj = "1"
1228− step "Upload an object using the encryption key"
1229− rFile <- mkRandFile mb1
1230− let putOpts = defaultPutObjectOptions {pooSSE = Just $ SSEC key}
1231− fPutObject bucket obj rFile putOpts
1232−
1233− step "Stat object without key - should fail"
1234− headRes <- try $ statObject bucket obj defaultGetObjectOptions
1235− case headRes of
1236− Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
1237− Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
1238− | NC.responseStatus rsp == HT.status400 -> return ()
1239− | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1240− Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1241−
1242− step "Get file and check length"
1243− dstFile <- mkRandFile 0
1244− let getOpts = defaultGetObjectOptions {gooSSECKey = Just key}
1245− fGetObject bucket obj dstFile getOpts
1246−
1247− gotSize <- withNewHandle dstFile getFileSize
1248− liftIO $
1249− gotSize
1250− == Right (Just mb1)
1251− @? "Wrong file size of object when getting"
1252−
1253− step "Cleanup"
1254− deleteObject bucket obj
1255− else step "Skipping encryption test as server is not using TLS"
1256−
1257−assumeRoleRequestTest :: TestTree
1258−assumeRoleRequestTest = testCaseSteps "Assume Role STS API" $ \step -> do
1259− step "Load credentials"
1260− (_, (ak, sk)) <- lookupTestEnv
1261− val <- Env.lookupEnv "MINIO_LOCAL"
1262− isSecure <- Env.lookupEnv "MINIO_SECURE"
1263− let localMinioCred = Just $ CredentialValue ak sk mempty
1264− playCreds =
1265− case connectCreds minioPlayCI of
1266− CredsStatic c -> Just c
1267− _ -> Nothing
1268− (cvMay, loc) =
1269− case (val, isSecure) of
1270− (Just _, Just _) -> (localMinioCred, "https://localhost:9000")
1271− (Just _, Nothing) -> (localMinioCred, "http://localhost:9000")
1272− (Nothing, _) -> (playCreds, "https://play.min.io:9000")
1273− cv <- maybe (assertFailure "bad creds") return cvMay
1274− let assumeRole =
1275− STSAssumeRole cv $
1276− defaultSTSAssumeRoleOptions
1277− { saroLocation = Just "us-east-1",
1278− saroEndpoint = Just loc
1279− }
1280− step "AssumeRole request"
1281− res <- requestSTSCredential assumeRole
1282− let v = credentialValueText $ fst res
1283− print (v, snd res)
971+ funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
972+ -- Skip this test if the server is not using TLS as encryption is
973+ -- disabled anyway.
974+ isTLSConn <- asks (connectIsSecure . mcConnInfo)
975+ if isTLSConn
976+ then do step "Make an encryption key"
977+ key <- case mkSSECKey $ BS.pack [0..31] of
978+ Nothing -> liftIO $ assertFailure "This should not happen"
979+ Just k -> return k
980+
981+ let mb1 = 1024*1024
982+ obj = "1"
983+ step "Upload an object using the encryption key"
984+ rFile <- mkRandFile mb1
985+ let putOpts = defaultPutObjectOptions { pooSSE = Just $ SSEC key }
986+ fPutObject bucket obj rFile putOpts
987+
988+ step "Stat object without key - should fail"
989+ headRes <- try $ statObject bucket obj defaultGetObjectOptions
990+ case headRes of
991+ Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
992+ Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
993+ | NC.responseStatus rsp == HT.status400 -> return ()
994+ | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
995+ Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
996+
997+ step "Get file and check length"
998+ dstFile <- mkRandFile 0
999+ let getOpts = defaultGetObjectOptions { gooSSECKey = Just key }
1000+ fGetObject bucket obj dstFile getOpts
1001+
1002+ gotSize <- withNewHandle dstFile getFileSize
1003+ liftIO $ gotSize == Right (Just mb1) @?
1004+ "Wrong file size of object when getting"
1005+
1006+ step "Cleanup"
1007+ deleteObject bucket obj
1008+
1009+ else step "Skipping encryption test as server is not using TLS"
test/Network/Minio/API/Test.hs+61−75
@@ -15,102 +15,88 @@
1515--
1616
1717module Network.Minio.API.Test
18− ( bucketNameValidityTests,
19− objectNameValidityTests,
20− parseServerInfoJSONTest,
21− parseHealStatusTest,
22− parseHealStartRespTest,
23− )
24−where
25−
26−import Data.Aeson (eitherDecode)
27−import Network.Minio.API
28−import Network.Minio.AdminAPI
29−import Test.Tasty
30−import Test.Tasty.HUnit
18+ ( bucketNameValidityTests
19+ , objectNameValidityTests
20+ , parseServerInfoJSONTest
21+ , parseHealStatusTest
22+ , parseHealStartRespTest
23+ ) where
24+
25+import Data.Aeson (eitherDecode)
26+import Test.Tasty
27+import Test.Tasty.HUnit
28+
29+import Lib.Prelude
30+
31+import Network.Minio.AdminAPI
32+import Network.Minio.API
3133
3234assertBool' :: Bool -> Assertion
3335assertBool' = assertBool "Test failed!"
3436
3537bucketNameValidityTests :: TestTree
36−bucketNameValidityTests =
37− testGroup
38− "Bucket Name Validity Tests"
39− [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName "",
40− testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab",
41− testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
42− testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD",
43− testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2",
44− testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-",
45− testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg",
46− testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1",
47− testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea",
48− testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d",
49− testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
50− ]
38+bucketNameValidityTests = testGroup "Bucket Name Validity Tests"
39+ [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName ""
40+ , testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab"
41+ , testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
42+ , testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD"
43+ , testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2"
44+ , testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-"
45+ , testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg"
46+ , testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1"
47+ , testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea"
48+ , testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d"
49+ , testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
50+ ]
5151
5252objectNameValidityTests :: TestTree
53−objectNameValidityTests =
54− testGroup
55− "Object Name Validity Tests"
56− [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName "",
57− testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
58− ]
53+objectNameValidityTests = testGroup "Object Name Validity Tests"
54+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""
55+ , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
56+ ]
5957
6058parseServerInfoJSONTest :: TestTree
61−parseServerInfoJSONTest =
62− testGroup "Parse MinIO Admin API ServerInfo JSON test" $
63− map
64− ( \(tName, tDesc, tfn, tVal) ->
65− testCase tName $
66− assertBool tDesc $
67− tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
68− )
69− testCases
59+parseServerInfoJSONTest = testGroup "Parse MinIO Admin API ServerInfo JSON test" $
60+ map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
61+ tfn (eitherDecode tVal :: Either [Char] [ServerInfo])) testCases
7062 where
71− testCases =
72− [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON),
73− ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON),
74− ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
75− ]
63+ testCases = [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON)
64+ , ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON)
65+ , ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
66+ ]
7667 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+
7769 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+
7871 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\":[]}}}]"
7972
8073parseHealStatusTest :: TestTree
81−parseHealStatusTest =
82− testGroup "Parse MinIO Admin API HealStatus JSON test" $
83− map
84− ( \(tName, tDesc, tfn, tVal) ->
85− testCase tName $
86− assertBool tDesc $
87− tfn (eitherDecode tVal :: Either [Char] HealStatus)
88− )
89− testCases
74+parseHealStatusTest = testGroup "Parse MinIO Admin API HealStatus JSON test" $
75+ map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
76+ tfn (eitherDecode tVal :: Either [Char] HealStatus)) testCases
77+
9078 where
91− testCases =
92− [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON'),
93− ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON'),
94− ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
95− ]
79+ testCases = [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON')
80+ , ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON')
81+ , ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
82+ ]
83+
9684 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+
9786 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+
9888 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}]}"
9989
10090parseHealStartRespTest :: TestTree
101−parseHealStartRespTest =
102− testGroup "Parse MinIO Admin API HealStartResp JSON test" $
103− map
104− ( \(tName, tDesc, tfn, tVal) ->
105− testCase tName $
106− assertBool tDesc $
107− tfn (eitherDecode tVal :: Either [Char] HealStartResp)
108− )
109− testCases
91+parseHealStartRespTest = testGroup "Parse MinIO Admin API HealStartResp JSON test" $
92+ map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
93+ tfn (eitherDecode tVal :: Either [Char] HealStartResp)) testCases
94+
11095 where
111− testCases =
112− [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON),
113− ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
114− ]
96+ testCases = [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON)
97+ , ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
98+ ]
99+
115100 hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
101+
116102 missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
test/Network/Minio/JsonParser/Test.hs+27−26
@@ -15,26 +15,26 @@
1515--
1616
1717module Network.Minio.JsonParser.Test
18− ( jsonParserTests,
19− )
20−where
18+ (
19+ jsonParserTests
20+ ) where
2121
22−import Lib.Prelude
23−import Network.Minio.Errors
24−import Network.Minio.JsonParser
25−import Test.Tasty
26−import Test.Tasty.HUnit
27−import UnliftIO (MonadUnliftIO)
22+import Test.Tasty
23+import Test.Tasty.HUnit
24+import UnliftIO (MonadUnliftIO)
25+
26+import Lib.Prelude
27+
28+import Network.Minio.Errors
29+import Network.Minio.JsonParser
2830
2931jsonParserTests :: TestTree
30−jsonParserTests =
31− testGroup
32− "JSON Parser Tests"
33− [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34− ]
32+jsonParserTests = testGroup "JSON Parser Tests"
33+ [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34+ ]
3535
3636tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
37−tryValidationErr = try
37+tryValidationErr act = try act
3838
3939assertValidationErr :: MErrV -> Assertion
4040assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
@@ -44,20 +44,21 @@ testParseErrResponseJSON = do
4444 -- 1. Test parsing of an invalid error json.
4545 parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
4646 when (isRight parseResE) $
47− assertFailure $
48− "Parsing should have failed => " ++ show parseResE
47+ assertFailure $ "Parsing should have failed => " ++ show parseResE
4948
5049 forM_ cases $ \(jsondata, sErr) -> do
5150 parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
5251 either assertValidationErr (@?= sErr) parseErr
52+
5353 where
54− cases =
55− [ -- 2. Test parsing of a valid error json.
56− ( "{\"Code\":\"InvalidAccessKeyId\",\"Message\":\"The access key ID you provided does not exist in our records.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
57− ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58− ),
59− -- 3. Test parsing of a valid, empty Resource.
60− ( "{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
61− ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
62− )
54+ cases = [
55+ -- 2. Test parsing of a valid error json.
56+ ("{\"Code\":\"InvalidAccessKeyId\",\"Message\":\"The access key ID you provided does not exist in our records.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
57+ ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58+ )
59+ ,
60+ -- 3. Test parsing of a valid, empty Resource.
61+ ("{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
62+ ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
63+ )
6364 ]
test/Network/Minio/TestHelpers.hs+8−8
@@ -15,18 +15,18 @@
1515--
1616
1717module Network.Minio.TestHelpers
18− ( runTestNS,
19− )
20−where
18+ ( runTestNS
19+ ) where
2120
22−import Network.Minio.Data
21+import Network.Minio.Data
2322
24−newtype TestNS = TestNS {testNamespace :: Text}
23+import Lib.Prelude
24+
25+newtype TestNS = TestNS { testNamespace :: Text }
2526
2627instance HasSvcNamespace TestNS where
2728 getSvcNamespace = testNamespace
2829
2930runTestNS :: ReaderT TestNS m a -> m a
30−runTestNS =
31− flip runReaderT $
32− TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
31+runTestNS = flip runReaderT $
32+ TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs+17−15
@@ -15,31 +15,33 @@
1515--
1616
1717module Network.Minio.Utils.Test
18− ( limitedMapConcurrentlyTests,
19− )
20−where
18+ (
19+ limitedMapConcurrentlyTests
20+ ) where
2121
22−import Network.Minio.Utils
23−import Test.Tasty
24−import Test.Tasty.HUnit
22+import Test.Tasty
23+import Test.Tasty.HUnit
24+
25+import Lib.Prelude
26+
27+import Network.Minio.Utils
2528
2629limitedMapConcurrentlyTests :: TestTree
27−limitedMapConcurrentlyTests =
28− testGroup
29− "limitedMapConcurrently Tests"
30− [ testCase "Test with various thread counts" testLMC
31− ]
30+limitedMapConcurrentlyTests = testGroup "limitedMapConcurrently Tests"
31+ [ testCase "Test with various thread counts" testLMC
32+ ]
3233
3334testLMC :: Assertion
3435testLMC = do
3536 let maxNum = 50
3637 -- test with thread count of 1 to 2*maxNum
37− forM_ [1 .. (2 * maxNum)] $ \threads -> do
38− res <- limitedMapConcurrently threads compute [1 .. maxNum]
38+ forM_ [1..(2*maxNum)] $ \threads -> do
39+ res <- limitedMapConcurrently threads compute [1..maxNum]
3940 sum res @?= overallResultCheck maxNum
4041 where
4142 -- simple function to run in each thread
4243 compute :: Int -> IO Int
43− compute n = return $ sum [1 .. n]
44+ compute n = return $ sum [1..n]
45+
4446 -- function to check overall result
45− overallResultCheck n = sum $ map (\t -> (t * (t + 1)) `div` 2) [1 .. n]
47+ overallResultCheck n = sum $ map (\t -> (t * (t+1)) `div` 2) [1..n]
test/Network/Minio/XmlGenerator/Test.hs+98−137
@@ -13,33 +13,30 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE QuasiQuotes #-}
1716
17+{-# LANGUAGE QuasiQuotes #-}
1818module Network.Minio.XmlGenerator.Test
19− ( xmlGeneratorTests,
20− )
21−where
19+ ( xmlGeneratorTests
20+ ) where
21+
22+import Test.Tasty
23+import Test.Tasty.HUnit
24+import Text.RawString.QQ (r)
2225
23−import qualified Data.ByteString.Lazy as LBS
24−import Lib.Prelude
25−import Network.Minio.Data
26−import Network.Minio.TestHelpers
27−import Network.Minio.XmlGenerator
28−import Network.Minio.XmlParser (parseNotification)
29−import Test.Tasty
30−import Test.Tasty.HUnit
31−import Text.RawString.QQ (r)
32−import Text.XML (def, parseLBS)
26+import Lib.Prelude
27+
28+import Network.Minio.Data
29+import Network.Minio.TestHelpers
30+import Network.Minio.XmlGenerator
31+import Network.Minio.XmlParser (parseNotification)
3332
3433xmlGeneratorTests :: TestTree
35−xmlGeneratorTests =
36− testGroup
37− "XML Generator Tests"
38− [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig,
39− testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest,
40− testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest,
41− testCase "Test mkSelectRequest" testMkSelectRequest
42− ]
34+xmlGeneratorTests = testGroup "XML Generator Tests"
35+ [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig
36+ , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest
37+ , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest
38+ , testCase "Test mkSelectRequest" testMkSelectRequest
39+ ]
4340
4441testMkCreateBucketConfig :: Assertion
4542testMkCreateBucketConfig = do
@@ -47,136 +44,100 @@ testMkCreateBucketConfig = do
4744 assertEqual "CreateBucketConfiguration xml should match: " expected $
4845 mkCreateBucketConfig ns "EU"
4946 where
50− expected =
51− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
52− \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
53− \<LocationConstraint>EU</LocationConstraint>\
54− \</CreateBucketConfiguration>"
47+ expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
48+ \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
49+ \<LocationConstraint>EU</LocationConstraint>\
50+ \</CreateBucketConfiguration>"
5551
5652testMkCompleteMultipartUploadRequest :: Assertion
5753testMkCompleteMultipartUploadRequest =
5854 assertEqual "completeMultipartUpload xml should match: " expected $
59− mkCompleteMultipartUploadRequest [(1, "abc")]
55+ mkCompleteMultipartUploadRequest [(1, "abc")]
6056 where
61− expected =
62− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
63− \<CompleteMultipartUpload>\
64− \<Part>\
65− \<PartNumber>1</PartNumber><ETag>abc</ETag>\
66− \</Part>\
67− \</CompleteMultipartUpload>"
57+ expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
58+ \<CompleteMultipartUpload>\
59+ \<Part>\
60+ \<PartNumber>1</PartNumber><ETag>abc</ETag>\
61+ \</Part>\
62+ \</CompleteMultipartUpload>"
63+
6864
6965testMkPutNotificationRequest :: Assertion
7066testMkPutNotificationRequest =
7167 forM_ cases $ \val -> do
7268 let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
73− result = fromStrictBS $ mkPutNotificationRequest ns val
69+ result = toS $ mkPutNotificationRequest ns val
7470 ntf <- runExceptT $ runTestNS $ parseNotification result
75− either
76− (\_ -> assertFailure "XML Parse Error!")
77− (@?= val)
78− ntf
71+ either (\_ -> assertFailure "XML Parse Error!")
72+ (@?= val) ntf
7973 where
80− cases =
81− [ Notification
82− []
83− [ NotificationConfig
84− "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
85− "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
86− [ReducedRedundancyLostObject, ObjectCreated]
87− defaultFilter
88− ]
89− [],
90− Notification
91− [ NotificationConfig
92− "1"
93− "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
94− [ObjectCreatedPut]
95− ( Filter $
96− FilterKey $
97− FilterRules
98− [ FilterRule "prefix" "images/",
99− FilterRule "suffix" ".jpg"
100− ]
101− ),
102− NotificationConfig
103− ""
104− "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
105− [ObjectCreated]
106− defaultFilter
107− ]
108− [ NotificationConfig
109− ""
110− "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
111− [ReducedRedundancyLostObject]
112− defaultFilter
113− ]
114− [ NotificationConfig
115− "ObjectCreatedEvents"
116− "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
117− [ObjectCreated]
118− defaultFilter
119− ]
120− ]
74+ cases = [ Notification []
75+ [ NotificationConfig
76+ "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
77+ "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
78+ [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
79+ ]
80+ []
81+ , Notification
82+ [ NotificationConfig
83+ "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
84+ [ObjectCreatedPut]
85+ (Filter $ FilterKey $ FilterRules
86+ [ FilterRule "prefix" "images/"
87+ , FilterRule "suffix" ".jpg"])
88+ , NotificationConfig
89+ "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
90+ [ObjectCreated] defaultFilter
91+ ]
92+ [ NotificationConfig
93+ "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
94+ [ReducedRedundancyLostObject] defaultFilter
95+ ]
96+ [ NotificationConfig
97+ "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
98+ [ObjectCreated] defaultFilter
99+ ]
100+ ]
121101
122102testMkSelectRequest :: Assertion
123103testMkSelectRequest = mapM_ assertFn cases
124104 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
129− (Right genDoc, Right expDoc) -> assertEqual "selectRequest XML should match: " expDoc genDoc
130− (Left err, _) -> assertFailure $ "Generated selectRequest failed to parse as XML" ++ show err
131− (_, Left err) -> assertFailure $ "Expected selectRequest failed to parse as XML" ++ show err
132− cases =
133− [ ( SelectRequest
134− "Select * from S3Object"
135− SQL
136− ( InputSerialization
137− (Just CompressionTypeGzip)
138− ( InputFormatCSV $
139− fileHeaderInfo FileHeaderIgnore
140− <> recordDelimiter "\n"
141− <> fieldDelimiter ","
142− <> quoteCharacter "\""
143− <> quoteEscapeCharacter "\""
105+ assertFn (a, b) = assertEqual "selectRequest XML should match: " b $ mkSelectRequest a
106+ cases = [ ( SelectRequest "Select * from S3Object" SQL
107+ (InputSerialization (Just CompressionTypeGzip)
108+ (InputFormatCSV $ fileHeaderInfo FileHeaderIgnore
109+ <> recordDelimiter "\n"
110+ <> fieldDelimiter ","
111+ <> quoteCharacter "\""
112+ <> quoteEscapeCharacter "\""
113+ ))
114+ (OutputSerializationCSV $ quoteFields QuoteFieldsAsNeeded
115+ <> recordDelimiter "\n"
116+ <> fieldDelimiter ","
117+ <> quoteCharacter "\""
118+ <> quoteEscapeCharacter "\""
144119 )
145− )
146− ( OutputSerializationCSV $
147− quoteFields QuoteFieldsAsNeeded
148− <> recordDelimiter "\n"
149− <> fieldDelimiter ","
150− <> quoteCharacter "\""
151− <> quoteEscapeCharacter "\""
152− )
153− (Just False),
154− [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><FieldDelimiter>,</FieldDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><RecordDelimiter>
155−</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
156−</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
157− ),
158− ( setRequestProgressEnabled False $
159− setInputCompressionType CompressionTypeGzip $
160− selectRequest
161− "Select * from S3Object"
162− documentJsonInput
163− (outputJSONFromRecordDelimiter "\n"),
164− [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter>
120+ (Just False)
121+ , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><QuoteCharacter>&#34;</QuoteCharacter><RecordDelimiter>
122+</RecordDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
123+</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
124+ )
125+ , ( setRequestProgressEnabled False $
126+ setInputCompressionType CompressionTypeGzip $
127+ selectRequest "Select * from S3Object" documentJsonInput
128+ (outputJSONFromRecordDelimiter "\n")
129+ , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter>
165130</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
166− ),
167− ( setRequestProgressEnabled False $
168− setInputCompressionType CompressionTypeNone $
169− selectRequest
170− "Select * from S3Object"
171− defaultParquetInput
172− ( outputCSVFromProps $
173− quoteFields QuoteFieldsAsNeeded
174− <> recordDelimiter "\n"
175− <> fieldDelimiter ","
176− <> quoteCharacter "\""
177− <> quoteEscapeCharacter "\""
178− ),
179− [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
180−</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
181− )
182− ]
131+ )
132+ , ( setRequestProgressEnabled False $
133+ setInputCompressionType CompressionTypeNone $
134+ selectRequest "Select * from S3Object" defaultParquetInput
135+ (outputCSVFromProps $ quoteFields QuoteFieldsAsNeeded
136+ <> recordDelimiter "\n"
137+ <> fieldDelimiter ","
138+ <> quoteCharacter "\""
139+ <> quoteEscapeCharacter "\"")
140+ , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
141+</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
142+ )
143+ ]
test/Network/Minio/XmlParser/Test.hs+276−298
@@ -13,49 +13,48 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE QuasiQuotes #-}
1716
17+{-# LANGUAGE QuasiQuotes #-}
1818module Network.Minio.XmlParser.Test
19− ( xmlParserTests,
20− )
21−where
22−
23−import qualified Data.HashMap.Strict as H
24−import Data.Time (fromGregorian)
25−import Lib.Prelude
26−import Network.Minio.Data
27−import Network.Minio.Errors
28−import Network.Minio.TestHelpers
29−import Network.Minio.XmlParser
30−import Test.Tasty
31−import Test.Tasty.HUnit
32−import Text.RawString.QQ (r)
33−import UnliftIO (MonadUnliftIO)
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+
29+import Lib.Prelude
30+
31+import Network.Minio.Data
32+import Network.Minio.Errors
33+import Network.Minio.TestHelpers
34+import Network.Minio.XmlParser
3435
3536xmlParserTests :: TestTree
36−xmlParserTests =
37− testGroup
38− "XML Parser Tests"
39− [ testCase "Test parseLocation" testParseLocation,
40− testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload,
41− testCase "Test parseListObjectsResponse" testParseListObjectsResult,
42− testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result,
43− testCase "Test parseListUploadsresponse" testParseListIncompleteUploads,
44− testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse,
45− testCase "Test parseListPartsResponse" testParseListPartsResponse,
46− testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse,
47− testCase "Test parseNotification" testParseNotification,
48− testCase "Test parseSelectProgress" testParseSelectProgress
49− ]
37+xmlParserTests = testGroup "XML Parser Tests"
38+ [ testCase "Test parseLocation" testParseLocation
39+ , testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload
40+ , testCase "Test parseListObjectsResponse" testParseListObjectsResult
41+ , testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result
42+ , testCase "Test parseListUploadsresponse" testParseListIncompleteUploads
43+ , testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse
44+ , testCase "Test parseListPartsResponse" testParseListPartsResponse
45+ , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse
46+ , testCase "Test parseNotification" testParseNotification
47+ , testCase "Test parseSelectProgress" testParseSelectProgress
48+ ]
5049
5150tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
52−tryValidationErr = try
51+tryValidationErr act = try act
5352
5453assertValidtionErr :: MErrV -> Assertion
5554assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
5655
5756eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
58−eitherValidationErr (Left e) _ = assertValidtionErr e
57+eitherValidationErr (Left e) _ = assertValidtionErr e
5958eitherValidationErr (Right a) f = f a
6059
6160testParseLocation :: Assertion
@@ -63,224 +62,224 @@ testParseLocation = do
6362 -- 1. Test parsing of an invalid location constraint xml.
6463 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
6564 when (isRight parseResE) $
66− assertFailure $
67− "Parsing should have failed => " ++ show parseResE
65+ assertFailure $ "Parsing should have failed => " ++ show parseResE
6866
6967 forM_ cases $ \(xmldata, expectedLocation) -> do
7068 parseLocE <- tryValidationErr $ parseLocation xmldata
7169 either assertValidtionErr (@?= expectedLocation) parseLocE
7270 where
73− cases =
74− [ -- 2. Test parsing of a valid location xml.
75− ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
76− \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
77− "EU"
78− ),
79− -- 3. Test parsing of a valid, empty location xml.
80− ( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
81− "us-east-1"
82− )
71+ cases = [
72+ -- 2. Test parsing of a valid location xml.
73+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
74+ \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
75+ "EU"
76+ )
77+ ,
78+ -- 3. Test parsing of a valid, empty location xml.
79+ ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
80+ "us-east-1"
81+ )
8382 ]
8483
84+
8585testParseNewMultipartUpload :: Assertion
8686testParseNewMultipartUpload = do
8787 forM_ cases $ \(xmldata, expectedUploadId) -> do
8888 parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
8989 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
9090 where
91− cases =
92− [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
93− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
94− \ <Bucket>example-bucket</Bucket>\
95− \ <Key>example-object</Key>\
96− \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
97− \</InitiateMultipartUploadResult>",
98− "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
99− ),
100− ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
101− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
102− \ <Bucket>example-bucket</Bucket>\
103− \ <Key>example-object</Key>\
104− \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
105− \</InitiateMultipartUploadResult>",
106− "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
107− )
91+ cases = [
92+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
93+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
94+ \ <Bucket>example-bucket</Bucket>\
95+ \ <Key>example-object</Key>\
96+ \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
97+ \</InitiateMultipartUploadResult>",
98+ "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
99+ ),
100+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
101+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
102+ \ <Bucket>example-bucket</Bucket>\
103+ \ <Key>example-object</Key>\
104+ \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
105+ \</InitiateMultipartUploadResult>",
106+ "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
107+ )
108108 ]
109109
110110testParseListObjectsResult :: Assertion
111111testParseListObjectsResult = do
112− let xmldata =
113− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
114− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
115− \<Name>bucket</Name>\
116− \<Prefix/>\
117− \<NextContinuationToken>opaque</NextContinuationToken>\
118− \<KeyCount>1000</KeyCount>\
119− \<MaxKeys>1000</MaxKeys>\
120− \<IsTruncated>true</IsTruncated>\
121− \<Contents>\
122− \<Key>my-image.jpg</Key>\
123− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
124− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
125− \<Size>434234</Size>\
126− \<StorageClass>STANDARD</StorageClass>\
127− \</Contents>\
128− \</ListBucketResult>"
129− expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
130− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
131− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
112+ let
113+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
114+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
115+ \<Name>bucket</Name>\
116+ \<Prefix/>\
117+ \<NextContinuationToken>opaque</NextContinuationToken>\
118+ \<KeyCount>1000</KeyCount>\
119+ \<MaxKeys>1000</MaxKeys>\
120+ \<IsTruncated>true</IsTruncated>\
121+ \<Contents>\
122+ \<Key>my-image.jpg</Key>\
123+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
124+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
125+ \<Size>434234</Size>\
126+ \<StorageClass>STANDARD</StorageClass>\
127+ \</Contents>\
128+ \</ListBucketResult>"
129+
130+ expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
131+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
132+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
132133
133134 parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
134135 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
135136
136137testParseListObjectsV1Result :: Assertion
137138testParseListObjectsV1Result = do
138− let xmldata =
139− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
140− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
141− \<Name>bucket</Name>\
142− \<Prefix/>\
143− \<NextMarker>my-image1.jpg</NextMarker>\
144− \<KeyCount>1000</KeyCount>\
145− \<MaxKeys>1000</MaxKeys>\
146− \<IsTruncated>true</IsTruncated>\
147− \<Contents>\
148− \<Key>my-image.jpg</Key>\
149− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
150− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
151− \<Size>434234</Size>\
152− \<StorageClass>STANDARD</StorageClass>\
153− \</Contents>\
154− \</ListBucketResult>"
155− expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
156− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
157− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
139+ let
140+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
141+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
142+ \<Name>bucket</Name>\
143+ \<Prefix/>\
144+ \<NextMarker>my-image1.jpg</NextMarker>\
145+ \<KeyCount>1000</KeyCount>\
146+ \<MaxKeys>1000</MaxKeys>\
147+ \<IsTruncated>true</IsTruncated>\
148+ \<Contents>\
149+ \<Key>my-image.jpg</Key>\
150+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
151+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
152+ \<Size>434234</Size>\
153+ \<StorageClass>STANDARD</StorageClass>\
154+ \</Contents>\
155+ \</ListBucketResult>"
156+
157+ expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
158+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
159+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
158160
159161 parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
160162 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
161163
162164testParseListIncompleteUploads :: Assertion
163165testParseListIncompleteUploads = do
164− let xmldata =
165− "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
166− \<Bucket>example-bucket</Bucket>\
167− \<KeyMarker/>\
168− \<UploadIdMarker/>\
169− \<NextKeyMarker>sample.jpg</NextKeyMarker>\
170− \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
171− \<Delimiter>/</Delimiter>\
172− \<Prefix/>\
173− \<MaxUploads>1000</MaxUploads>\
174− \<IsTruncated>false</IsTruncated>\
175− \<Upload>\
176− \<Key>sample.jpg</Key>\
177− \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
178− \<Initiator>\
179− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
180− \<DisplayName>s3-nickname</DisplayName>\
181− \</Initiator>\
182− \<Owner>\
183− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
184− \<DisplayName>s3-nickname</DisplayName>\
185− \</Owner>\
186− \<StorageClass>STANDARD</StorageClass>\
187− \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
188− \</Upload>\
189− \<CommonPrefixes>\
190− \<Prefix>photos/</Prefix>\
191− \</CommonPrefixes>\
192− \<CommonPrefixes>\
193− \<Prefix>videos/</Prefix>\
194− \</CommonPrefixes>\
195− \</ListMultipartUploadsResult>"
196− expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
197− uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
198− initTime = UTCTime (fromGregorian 2010 11 26) 69857
199− prefixes = ["photos/", "videos/"]
166+ let
167+ xmldata = "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
168+ \<Bucket>example-bucket</Bucket>\
169+ \<KeyMarker/>\
170+ \<UploadIdMarker/>\
171+ \<NextKeyMarker>sample.jpg</NextKeyMarker>\
172+ \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
173+ \<Delimiter>/</Delimiter>\
174+ \<Prefix/>\
175+ \<MaxUploads>1000</MaxUploads>\
176+ \<IsTruncated>false</IsTruncated>\
177+ \<Upload>\
178+ \<Key>sample.jpg</Key>\
179+ \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
180+ \<Initiator>\
181+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
182+ \<DisplayName>s3-nickname</DisplayName>\
183+ \</Initiator>\
184+ \<Owner>\
185+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
186+ \<DisplayName>s3-nickname</DisplayName>\
187+ \</Owner>\
188+ \<StorageClass>STANDARD</StorageClass>\
189+ \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
190+ \</Upload>\
191+ \<CommonPrefixes>\
192+ \<Prefix>photos/</Prefix>\
193+ \</CommonPrefixes>\
194+ \<CommonPrefixes>\
195+ \<Prefix>videos/</Prefix>\
196+ \</CommonPrefixes>\
197+ \</ListMultipartUploadsResult>"
198+ expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
199+ uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
200+ initTime = UTCTime (fromGregorian 2010 11 26) 69857
201+ prefixes = ["photos/", "videos/"]
200202
201203 parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
202204 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
203205
206+
204207testParseCompleteMultipartUploadResponse :: Assertion
205208testParseCompleteMultipartUploadResponse = do
206− let xmldata =
207− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
208− \<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
209− \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
210− \<Bucket>Example-Bucket</Bucket>\
211− \<Key>Example-Object</Key>\
212− \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
213− \</CompleteMultipartUploadResult>"
214− expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
209+ let
210+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
211+\<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
212+ \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
213+ \<Bucket>Example-Bucket</Bucket>\
214+ \<Key>Example-Object</Key>\
215+ \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
216+\</CompleteMultipartUploadResult>"
217+ expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
215218
216219 parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
217220 eitherValidationErr parsedETagE (@?= expectedETag)
218221
219222testParseListPartsResponse :: Assertion
220223testParseListPartsResponse = do
221− let xmldata =
222− "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
223− \<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
224− \<Bucket>example-bucket</Bucket>\
225− \<Key>example-object</Key>\
226− \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
227− \<Initiator>\
228− \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
229− \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
230− \</Initiator>\
231− \<Owner>\
232− \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
233− \<DisplayName>someName</DisplayName>\
234− \</Owner>\
235− \<StorageClass>STANDARD</StorageClass>\
236− \<PartNumberMarker>1</PartNumberMarker>\
237− \<NextPartNumberMarker>3</NextPartNumberMarker>\
238− \<MaxParts>2</MaxParts>\
239− \<IsTruncated>true</IsTruncated>\
240− \<Part>\
241− \<PartNumber>2</PartNumber>\
242− \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
243− \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
244− \<Size>10485760</Size>\
245− \</Part>\
246− \<Part>\
247− \<PartNumber>3</PartNumber>\
248− \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
249− \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
250− \<Size>10485760</Size>\
251− \</Part>\
252− \</ListPartsResult>"
253− expectedListResult = ListPartsResult True (Just 3) [part1, part2]
254− part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
255− modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
256− part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
257− modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
224+ let
225+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
226+\<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
227+ \<Bucket>example-bucket</Bucket>\
228+ \<Key>example-object</Key>\
229+ \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
230+ \<Initiator>\
231+ \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
232+ \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
233+ \</Initiator>\
234+ \<Owner>\
235+ \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
236+ \<DisplayName>someName</DisplayName>\
237+ \</Owner>\
238+ \<StorageClass>STANDARD</StorageClass>\
239+ \<PartNumberMarker>1</PartNumberMarker>\
240+ \<NextPartNumberMarker>3</NextPartNumberMarker>\
241+ \<MaxParts>2</MaxParts>\
242+ \<IsTruncated>true</IsTruncated>\
243+ \<Part>\
244+ \<PartNumber>2</PartNumber>\
245+ \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
246+ \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
247+ \<Size>10485760</Size>\
248+ \</Part>\
249+ \<Part>\
250+ \<PartNumber>3</PartNumber>\
251+ \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
252+ \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
253+ \<Size>10485760</Size>\
254+ \</Part>\
255+\</ListPartsResult>"
256+
257+ expectedListResult = ListPartsResult True (Just 3) [part1, part2]
258+ part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
259+ modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
260+ part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
261+ modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
258262
259263 parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
260264 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
261265
262266testParseCopyObjectResponse :: Assertion
263267testParseCopyObjectResponse = do
264− let cases =
265− [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
266− \<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
267− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
268− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
269− \</CopyObjectResult>",
270− ( "\"9b2cf535f27731c974343645a3985328\"",
271− UTCTime (fromGregorian 2009 10 28) 81120
272− )
273− ),
274− ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
275− \<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
276− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
277− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
278− \</CopyPartResult>",
279− ( "\"9b2cf535f27731c974343645a3985328\"",
280− UTCTime (fromGregorian 2009 10 28) 81120
281− )
282− )
283− ]
268+ let
269+ cases = [ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
270+\<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
271+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
272+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
273+\</CopyObjectResult>",
274+ ("\"9b2cf535f27731c974343645a3985328\"",
275+ UTCTime (fromGregorian 2009 10 28) 81120))
276+ , ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
277+\<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
278+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
279+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
280+\</CopyPartResult>",
281+ ("\"9b2cf535f27731c974343645a3985328\"",
282+ UTCTime (fromGregorian 2009 10 28) 81120))]
284283
285284 forM_ cases $ \(xmldata, (etag, modTime)) -> do
286285 parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
@@ -288,89 +287,73 @@ testParseCopyObjectResponse = do
288287
289288testParseNotification :: Assertion
290289testParseNotification = do
291− let cases =
292− [ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
293− \ <TopicConfiguration>\
294− \ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
295− \ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
296− \ <Event>s3:ReducedRedundancyLostObject</Event>\
297− \ <Event>s3:ObjectCreated:*</Event>\
298− \ </TopicConfiguration>\
299− \</NotificationConfiguration>",
300− Notification
301− []
302− [ NotificationConfig
290+ let
291+ cases = [ ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
292+\ <TopicConfiguration>\
293+\ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
294+\ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
295+\ <Event>s3:ReducedRedundancyLostObject</Event>\
296+\ <Event>s3:ObjectCreated:*</Event>\
297+\ </TopicConfiguration>\
298+\</NotificationConfiguration>",
299+ Notification []
300+ [ NotificationConfig
303301 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
304302 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
305− [ReducedRedundancyLostObject, ObjectCreated]
306− defaultFilter
307− ]
308− []
309− ),
310− ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
311− \ <CloudFunctionConfiguration>\
312− \ <Id>ObjectCreatedEvents</Id>\
313− \ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
314− \ <Event>s3:ObjectCreated:*</Event>\
315− \ </CloudFunctionConfiguration>\
316− \ <QueueConfiguration>\
317− \ <Id>1</Id>\
318− \ <Filter>\
319− \ <S3Key>\
320− \ <FilterRule>\
321− \ <Name>prefix</Name>\
322− \ <Value>images/</Value>\
323− \ </FilterRule>\
324− \ <FilterRule>\
325− \ <Name>suffix</Name>\
326− \ <Value>.jpg</Value>\
327− \ </FilterRule>\
328− \ </S3Key>\
329− \ </Filter>\
330− \ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
331− \ <Event>s3:ObjectCreated:Put</Event>\
332− \ </QueueConfiguration>\
333− \ <TopicConfiguration>\
334− \ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
335− \ <Event>s3:ReducedRedundancyLostObject</Event>\
336− \ </TopicConfiguration>\
337− \ <QueueConfiguration>\
338− \ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
339− \ <Event>s3:ObjectCreated:*</Event>\
340− \ </QueueConfiguration>)\
341− \</NotificationConfiguration>",
342− Notification
343− [ NotificationConfig
344− "1"
345− "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
346− [ObjectCreatedPut]
347− ( Filter $
348− FilterKey $
349− FilterRules
350− [ FilterRule "prefix" "images/",
351− FilterRule "suffix" ".jpg"
352− ]
353− ),
354− NotificationConfig
355− ""
356− "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
357− [ObjectCreated]
358− defaultFilter
359− ]
360− [ NotificationConfig
361− ""
362− "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
363− [ReducedRedundancyLostObject]
364− defaultFilter
365− ]
366− [ NotificationConfig
367− "ObjectCreatedEvents"
368− "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
369− [ObjectCreated]
370− defaultFilter
371− ]
372− )
373− ]
303+ [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
304+ ]
305+ [])
306+ , ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
307+\ <CloudFunctionConfiguration>\
308+\ <Id>ObjectCreatedEvents</Id>\
309+\ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
310+\ <Event>s3:ObjectCreated:*</Event>\
311+\ </CloudFunctionConfiguration>\
312+\ <QueueConfiguration>\
313+\ <Id>1</Id>\
314+\ <Filter>\
315+\ <S3Key>\
316+\ <FilterRule>\
317+\ <Name>prefix</Name>\
318+\ <Value>images/</Value>\
319+\ </FilterRule>\
320+\ <FilterRule>\
321+\ <Name>suffix</Name>\
322+\ <Value>.jpg</Value>\
323+\ </FilterRule>\
324+\ </S3Key>\
325+\ </Filter>\
326+\ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
327+\ <Event>s3:ObjectCreated:Put</Event>\
328+\ </QueueConfiguration>\
329+\ <TopicConfiguration>\
330+\ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
331+\ <Event>s3:ReducedRedundancyLostObject</Event>\
332+\ </TopicConfiguration>\
333+\ <QueueConfiguration>\
334+\ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
335+\ <Event>s3:ObjectCreated:*</Event>\
336+\ </QueueConfiguration>)\
337+\</NotificationConfiguration>",
338+ Notification [ NotificationConfig
339+ "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
340+ [ObjectCreatedPut]
341+ (Filter $ FilterKey $ FilterRules
342+ [FilterRule "prefix" "images/",
343+ FilterRule "suffix" ".jpg"])
344+ , NotificationConfig
345+ "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
346+ [ObjectCreated] defaultFilter
347+ ]
348+ [ NotificationConfig
349+ "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
350+ [ReducedRedundancyLostObject] defaultFilter
351+ ]
352+ [ NotificationConfig
353+ "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
354+ [ObjectCreated] defaultFilter
355+ ])
356+ ]
374357
375358 forM_ cases $ \(xmldata, val) -> do
376359 result <- runExceptT $ runTestNS $ parseNotification xmldata
@@ -379,25 +362,20 @@ testParseNotification = do
379362-- | Tests parsing of both progress and stats
380363testParseSelectProgress :: Assertion
381364testParseSelectProgress = do
382− let cases =
383− [ ( [r|<?xml version="1.0" encoding="UTF-8"?>
365+ let cases = [ ([r|<?xml version="1.0" encoding="UTF-8"?>
384366<Progress>
385367 <BytesScanned>512</BytesScanned>
386368 <BytesProcessed>1024</BytesProcessed>
387369 <BytesReturned>1024</BytesReturned>
388−</Progress>|],
389− Progress 512 1024 1024
390− ),
391− ( [r|<?xml version="1.0" encoding="UTF-8"?>
370+</Progress>|] , Progress 512 1024 1024)
371+ , ([r|<?xml version="1.0" encoding="UTF-8"?>
392372<Stats>
393373 <BytesScanned>512</BytesScanned>
394374 <BytesProcessed>1024</BytesProcessed>
395375 <BytesReturned>1024</BytesReturned>
396−</Stats>|],
397− Progress 512 1024 1024
398− )
399− ]
400−
401− forM_ cases $ \(xmldata, progress) -> do
402− result <- runExceptT $ parseSelectProgress xmldata
403− eitherValidationErr result (@?= progress)
376+</Stats>|], Progress 512 1024 1024)
377+ ]
378+
379+ forM_ cases $ \(xmldata, progress) -> do
380+ result <- runExceptT $ parseSelectProgress xmldata
381+ eitherValidationErr result (@?= progress)
test/Spec.hs+82−83
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -14,17 +14,21 @@
1414-- limitations under the License.
1515--
1616
17−import qualified Data.ByteString as B
18−import qualified Data.List as L
19−import Lib.Prelude
20−import Network.Minio.API.Test
21−import Network.Minio.CopyObject
22−import Network.Minio.Data
23−import Network.Minio.Utils.Test
24−import Network.Minio.XmlGenerator.Test
25−import Network.Minio.XmlParser.Test
26−import Test.Tasty
27−import Test.Tasty.QuickCheck as QC
17+import Test.Tasty
18+import Test.Tasty.QuickCheck as QC
19+
20+import qualified Data.ByteString as B
21+import qualified Data.List as L
22+
23+import Lib.Prelude
24+
25+import Network.Minio.API.Test
26+import Network.Minio.CopyObject
27+import Network.Minio.Data
28+import Network.Minio.PutObject
29+import Network.Minio.Utils.Test
30+import Network.Minio.XmlGenerator.Test
31+import Network.Minio.XmlParser.Test
2832
2933main :: IO ()
3034main = defaultMain tests
@@ -47,87 +51,82 @@ properties = testGroup "Properties" [qcProps] -- [scProps]
4751-- ]
4852
4953qcProps :: TestTree
50−qcProps =
51− testGroup
52− "(checked by QuickCheck)"
53− [ QC.testProperty "selectPartSizes:" $
54− \n ->
55− let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
54+qcProps = testGroup "(checked by QuickCheck)"
55+ [ QC.testProperty "selectPartSizes:" $
56+ \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
57+
5658 -- check that pns increments from 1.
57− isPNumsAscendingFrom1 = all (uncurry (==)) $ zip pns [1 ..]
58− consPairs [] = []
59− consPairs [_] = []
60− consPairs (a : (b : c)) = (a, b) : consPairs (b : c)
59+ isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1..]
60+
61+ consPairs [] = []
62+ consPairs [_] = []
63+ consPairs (a:(b:c)) = (a, b):(consPairs (b:c))
64+
6165 -- check `offs` is monotonically increasing.
62− isOffsetsAsc = all (uncurry (<)) $ consPairs offs
66+ isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs
67+
6368 -- check sizes sums to n.
6469 isSumSizeOk = sum sizes == n
70+
6571 -- check sizes are constant except last
6672 isSizesConstantExceptLast =
67− all (uncurry (==)) (consPairs $ L.init sizes)
73+ all (\(a, b) -> a == b) (consPairs $ L.init sizes)
74+
6875 -- check each part except last is at least minPartSize;
6976 -- last part may be 0 only if it is the only part.
7077 nparts = length sizes
7178 isMinPartSizeOk =
72− if
73− | nparts > 1 -> -- last part can be smaller but > 0
74− all (>= minPartSize) (take (nparts - 1) sizes)
75− && all (> 0) (drop (nparts - 1) sizes)
76− | nparts == 1 -> -- size may be 0 here.
77− maybe True (\x -> x >= 0 && x <= minPartSize) $
78− listToMaybe sizes
79− | otherwise -> False
80− in n < 0
81− || ( isPNumsAscendingFrom1
82− && isOffsetsAsc
83− && isSumSizeOk
84− && isSizesConstantExceptLast
85− && isMinPartSizeOk
86− ),
87− QC.testProperty "selectCopyRanges:" $
88− \(start, end) ->
89− let (_, pairs) = L.unzip (selectCopyRanges (start, end))
90− -- is last part's snd offset end?
91− isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
92− -- is first part's fst offset start
93− isFirstPartOk = maybe False ((start ==) . fst) $ listToMaybe pairs
94− -- each pair is >=64MiB except last, and all those parts
95− -- have same size.
96− initSizes = maybe [] (map (\(a, b) -> b - a + 1) . init) (nonEmpty pairs)
97− isPartSizesOk =
98− all (>= minPartSize) initSizes
99− && maybe
100− True
101− (\k -> all (== k) initSizes)
102− (listToMaybe initSizes)
103− -- returned offsets are contiguous.
104− fsts = drop 1 $ map fst pairs
105− snds = take (length pairs - 1) $ map snd pairs
106− isContParts =
107− length fsts == length snds
108− && all (\(a, b) -> a == b + 1) (zip fsts snds)
109− in start < 0
110− || start > end
111− || (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts),
112− QC.testProperty "mkSSECKey:" $
113− \w8s ->
114− let bs = B.pack w8s
115− r = mkSSECKey bs
116− in case r of
117− Just _ -> B.length bs == 32
79+ if | nparts > 1 -> -- last part can be smaller but > 0
80+ all (>= minPartSize) (take (nparts - 1) sizes) &&
81+ all (\s -> s > 0) (drop (nparts - 1) sizes)
82+ | nparts == 1 -> -- size may be 0 here.
83+ maybe True (\x -> x >= 0 && x <= minPartSize) $
84+ headMay sizes
85+ | otherwise -> False
86+
87+ in n < 0 ||
88+ (isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk &&
89+ isSizesConstantExceptLast && isMinPartSizeOk)
90+
91+ , QC.testProperty "selectCopyRanges:" $
92+ \(start, end) ->
93+ let (_, pairs) = L.unzip (selectCopyRanges (start, end))
94+
95+ -- is last part's snd offset end?
96+ isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
97+ -- is first part's fst offset start
98+ isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
99+
100+ -- each pair is >=64MiB except last, and all those parts
101+ -- have same size.
102+ initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs
103+ isPartSizesOk = all (>= minPartSize) initSizes &&
104+ maybe True (\k -> all (== k) initSizes)
105+ (headMay initSizes)
106+
107+ -- returned offsets are contiguous.
108+ fsts = drop 1 $ map fst pairs
109+ snds = take (length pairs - 1) $ map snd pairs
110+ isContParts = length fsts == length snds &&
111+ and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
112+
113+ in start < 0 || start > end ||
114+ (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts)
115+
116+ , QC.testProperty "mkSSECKey:" $
117+ \w8s -> let bs = B.pack w8s
118+ r = mkSSECKey bs
119+ in case r of
120+ Just _ -> B.length bs == 32
118121 Nothing -> B.length bs /= 32
119− ]
122+ ]
120123
121124unitTests :: TestTree
122−unitTests =
123− testGroup
124− "Unit tests"
125− [ xmlGeneratorTests,
126− xmlParserTests,
127− bucketNameValidityTests,
128− objectNameValidityTests,
129− parseServerInfoJSONTest,
130− parseHealStatusTest,
131− parseHealStartRespTest,
132− limitedMapConcurrentlyTests
133− ]
125+unitTests = testGroup "Unit tests" [ xmlGeneratorTests, xmlParserTests
126+ , bucketNameValidityTests
127+ , objectNameValidityTests
128+ , parseServerInfoJSONTest
129+ , parseHealStatusTest
130+ , parseHealStartRespTest
131+ , limitedMapConcurrentlyTests
132+ ]
test/cert/private.keydeleted−28
@@ -1,28 +0,0 @@
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.crtdeleted−19
@@ -1,19 +0,0 @@
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-----