git.iko.soy/typeable/minio-hs
on 3ef237e help sign in
71 files changed+3607−9121
since splitsnapshots
unifiedsplit
.github/workflows/ci.yml−266 .gitignore−1 .travis.yml+40 CHANGELOG.md+1−115 CNAME−1 README.md+43−128 examples/GetConfig.hs → Setup.hs+3−15 docs/API.md+227−352 examples/AssumeRole.hs−47 examples/BucketExists.hs+8−5 examples/CopyObject.hs+21−17 examples/FileUploader.hs+31−31 examples/GetObject.hs+15−11 examples/HeadObject.hs+14−11 examples/Heal.hs−37 examples/ListBuckets.hs+8−6 examples/ListIncompleteUploads.hs+23−20 examples/ListObjects.hs+20−16 examples/MakeBucket.hs → examples/Makebucket.hs+14−10 examples/PresignedGetObject.hs+34−39 examples/PresignedPostPolicy.hs+42−45 examples/PresignedPutObject.hs+24−23 examples/PutObject.hs+18−16 examples/README.md−17 examples/RemoveIncompleteUpload.hs+13−10 examples/RemoveObject.hs+10−9 examples/RemoveBucket.hs → examples/Removebucket.hs+11−6 examples/SelectObject.hs−47 examples/ServerInfo.hs−30 examples/ServiceSendRestart.hs−29 examples/ServiceSendStop.hs−29 examples/ServiceStatus.hs−30 examples/SetConfig.hs−31 minio-hs.cabal+182−283 src/Lib/Prelude.hs+25−34 src/Network/Minio.hs+160−280 src/Network/Minio/API.hs+140−284 src/Network/Minio/APICommon.hs−80 src/Network/Minio/AdminAPI.hs−711 src/Network/Minio/CopyObject.hs−106 src/Network/Minio/Credentials.hs−77 src/Network/Minio/Credentials/AssumeRole.hs−266 src/Network/Minio/Credentials/Types.hs−90 src/Network/Minio/Data.hs+330−1038 src/Network/Minio/Data/ByteString.hs+19−20 src/Network/Minio/Data/Crypto.hs+28−39 src/Network/Minio/Data/Time.hs+14−18 src/Network/Minio/Errors.hs+45−59 src/Network/Minio/JsonParser.hs−49 src/Network/Minio/ListOps.hs+38−120 src/Network/Minio/PresignedOperations.hs+147−221 src/Network/Minio/PutObject.hs+160−102 src/Network/Minio/S3API.hs+281−544 src/Network/Minio/SelectAPI.hs−294 src/Network/Minio/Sign/V4.hs+199−446 src/Network/Minio/Utils.hs+87−167 src/Network/Minio/XmlCommon.hs−65 src/Network/Minio/XmlGenerator.hs+54−183 src/Network/Minio/XmlParser.hs+154−142 stack.yaml+4−4 stack.yaml.lock−19 test/LiveServer.hs+478−1135 test/Network/Minio/API/Test.hs+25−91 test/Network/Minio/JsonParser/Test.hs−63 test/Network/Minio/TestHelpers.hs−32 test/Network/Minio/Utils/Test.hs+16−14 test/Network/Minio/XmlGenerator/Test.hs+61−148 test/Network/Minio/XmlParser/Test.hs+274−319 test/Spec.hs+66−81 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
.gitignore−1
@@ -17,4 +17,3 @@ cabal.sandbox.config
1717*.eventlog
1818.stack-work/
1919cabal.project.local
20−*~
.travis.ymlnew file+40
@@ -0,0 +1,40 @@
1+# This is the simple Travis configuration, which is intended for use
2+# on applications which do not require cross-platform and
3+# multiple-GHC-version support. For more information and other
4+# options, see:
5+#
6+# https://docs.haskellstack.org/en/stable/travis_ci/
7+#
8+# Copy these contents into the root directory of your Github project in a file
9+# named .travis.yml
10+
11+# Use new container infrastructure to enable caching
12+sudo: false
13+
14+# Do not choose a language; we provide our own build tools.
15+language: generic
16+
17+# Caching so the next build will be fast too.
18+cache:
19+ directories:
20+ - $HOME/.stack
21+
22+# Ensure necessary system libraries are present
23+addons:
24+ apt:
25+ packages:
26+ - libgmp-dev
27+
28+before_install:
29+# Download and unpack the stack executable
30+- mkdir -p ~/.local/bin
31+- export PATH=$HOME/.local/bin:$PATH
32+- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
33+
34+install:
35+# Build dependencies
36+- stack --no-terminal --install-ghc test --only-dependencies
37+
38+script:
39+# Build the package, its tests, and its docs and run the tests
40+- stack --no-terminal test --haddock --no-haddock-deps --test-arguments --num-threads=1
CHANGELOG.md+1−115
@@ -1,120 +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−
35−## Version 1.5.2
36−
37−* Fix region `us-west-2` for AWS S3 (#139)
38−* Build examples in CI
39−* Disable live-server tests by default, but run them in CI
40−* Drop support for GHC 8.2.x
41−
42−## Version 1.5.1
43−
44−* Add support for GHC 8.8
45−
46−## Version 1.5.0
47−
48−* Switch to faster map data type - all previous usage of
49− Data.Map.Strict and Data.Set is replaced with Data.HashMap.Strict
50− and Data.HashSet.
51−* Add `oiUserMetadata` to parse and return user metadata stored with
52− an object.
53−* Add `GetObjectResponse` data type for the value returned by
54− `getObject`. It now contains parsed ObjectInfo along with the
55− conduit of object bytes.
56−
57−## Version 1.4.0
58−
59−* Expose runMinioRes and runMinioResWith (#129)
60−* Improve Haddocks (#127)
61−* Fix list objects APIs to return directory prefixes when run with
62− recurse set to False (#126)
63−* Use streaming signature for streaming payloads when on an insecure
64− connection (#123)
65−
66−## Version 1.3.1
67−
68−* Add TLS helpers to check if server uses TLS, and to disable
69− certificate validation for easier testing (#121)
70−
71−## Version 1.3.0
72−
73−* Retry requests that timeout using full-jitter backoff (#119)
74−* Accept GetObjectOptions in statObject (#112)
75−* Add encryption options to GetObjectOptions and PutObjectOptions (#111)
76−* Add missing Haddock documentation (#110)
77−* Add support for S3Select API (#108)
78−* New travis with support for multiple GHCs (#106)
79−* Fix region setting in presigned url functions (#107)
80−
81−## Version 1.2.0
82−
83−* Export Provider and findFirst to look for credentials (#103)
84−
85−## Version 1.1.0
86−
87−This version brings the following changes:
88−
89−* Adds experimental Admin APIs (#88, #91, #93, #94, #95, #100)
90−* Adds support for using Google Compute Storage service when S3
91− compatibility mode is enabled (#96, #99)
92−
93−This version also brings some breaking changes (via #101):
94−
95−* Adds IsString instance to load server address, and updates
96− initialization API to be more user friendly
97−* Drops usage of data-default package and exposes explicit default
98− values for various types used in the library.
99−
100−## Version 1.0.1
101−
102−This version brings the following (non-breaking) changes:
103−
104−* Remove dependency on text-format (#86)
105−* Remove direct dependency on exceptions (#87)
106−* Adds lower-bounds on dependencies.
107−
108−## Version 1.0.0
109−
110−This new release changes the following APIs to add new capabilities:
111−
112−* Copy Object API now supports more options for source and destination (#73)
113−* get/put Object functions now support a wider set of options via a
114− separate settings parameter (#71, #72)
115−* getBucketPolicy and setBucketPolicy APIs are added (#82)
116−* The library now uses UnliftIO (#83)
117−
1184## Version 0.3.2
1195
1206This release brings the following changes:
@@ -133,7 +19,7 @@ This release brings the following changes:
13319This is a bug-fix release:
13420
13521* Fix concurrency bug in `limitedMapConcurrently` (#53)
136−* Fix tests related to listing incomplete uploads to accommodate MinIO
22+* Fix tests related to listing incomplete uploads to accommodate Minio
13723 server's changed behaviour to not list incomplete uploads. Note that
13824 running these tests against AWS S3 are expected to fail. (#54)
13925
CNAMEdeleted−1
@@ -1 +0,0 @@
1−minio-hs.min.io
README.md+43−128
@@ -1,94 +1,64 @@
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.minio.io/slack?type=svg)](https://slack.minio.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://minio.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
66
7−## Installation
7+- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
88
9−### Add to your project
9+## Installation
1010
11−Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
11+```sh
12+git clone https://github.com/minio/minio-hs.git
1213
13−### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
14+cd minio-hs/
1415
15−#### For a cabal based environment
16+stack install
17+```
1618
17−Download the library source and change to the extracted directory:
19+Tests can be run with:
1820
19−``` sh
20−$ cabal get minio-hs
21−$ cd minio-hs-1.6.0/ # directory name could be different
22−```
21+```sh
2322
24−Then load the `ghci` REPL environment with the library and browse the available APIs:
23+stack test
2524
26−``` sh
27−$ cabal repl
28−ghci> :browse Network.Minio
2925```
3026
31−#### For a stack based environment
27+A section of the tests use the remote Minio Play server at
28+`https://play.minio.io:9000` by default. For library development,
29+using this remote server maybe slow. To run the tests against a
30+locally running Minio live server at `http://localhost:9000`, just set
31+the environment `MINIO_LOCAL` to any value (and unset it to switch
32+back to Play).
3233
33−From your home folder or any non-haskell project directory, just run:
34+Documentation can be locally built with:
3435
3536```sh
36−stack install minio-hs
37−```
3837
39−Then start an interpreter session and browse the available APIs with:
38+stack haddock
4039
41−```sh
42−$ stack ghci
43−> :browse Network.Minio
4440```
4541
46−## Examples
47−
48−The [examples](https://github.com/minio/minio-hs/tree/master/examples) folder contains many examples that you can try out and use to learn and to help with developing your own projects.
49−
50−### Quick-Start Example - File Uploader
51−
52−This example program connects to a MinIO object storage server, makes a bucket on the server and then uploads a file to the bucket.
53−
54−We will use the MinIO server running at https://play.min.io in this example. Feel free to use this service for testing and development. Access credentials are present in the library and are open to the public.
42+## Quick-Start Example - File Uploader
5543
5644### FileUploader.hs
5745``` haskell
5846#!/usr/bin/env stack
59−-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
60−
61−--
62−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
63−--
64−-- Licensed under the Apache License, Version 2.0 (the "License");
65−-- you may not use this file except in compliance with the License.
66−-- You may obtain a copy of the License at
67−--
68−-- http://www.apache.org/licenses/LICENSE-2.0
69−--
70−-- Unless required by applicable law or agreed to in writing, software
71−-- distributed under the License is distributed on an "AS IS" BASIS,
72−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
73−-- See the License for the specific language governing permissions and
74−-- limitations under the License.
75−--
47+-- stack --resolver lts-9.1 runghc --package minio-hs --package optparse-applicative --package filepath
7648
49+{-# Language OverloadedStrings, ScopedTypeVariables #-}
50+import Network.Minio
7751
78−{-# LANGUAGE OverloadedStrings #-}
79−{-# LANGUAGE ScopedTypeVariables #-}
80−import Network.Minio
81−
82−import Data.Monoid ((<>))
83−import Data.Text (pack)
84−import Options.Applicative
85−import System.FilePath.Posix
86−import UnliftIO (throwIO, try)
87−
88−import Prelude
52+import Control.Monad.Catch (catchIf)
53+import Control.Monad.IO.Class (liftIO)
54+import Data.Monoid ((<>))
55+import Data.Text (pack)
56+import Options.Applicative
57+import Prelude
58+import System.FilePath.Posix
8959
9060-- | The following example uses minio's play server at
91−-- https://play.min.io. The endpoint and associated
61+-- https://play.minio.io:9000. The endpoint and associated
9262-- credentials are provided via the libary constant,
9363--
9464-- > minioPlayCI :: ConnectInfo
@@ -98,7 +68,7 @@ import Prelude
9868fileNameArgs :: Parser FilePath
9969fileNameArgs = strArgument
10070 (metavar "FILENAME"
101− <> help "Name of file to upload to AWS S3 or a MinIO server")
71+ <> help "Name of file to upload to AWS S3 or a Minio server")
10272
10373cmdParser = info
10474 (helper <*> fileNameArgs)
@@ -107,30 +77,27 @@ cmdParser = info
10777 <> header
10878 "FileUploader - a simple file-uploader program using minio-hs")
10979
80+ignoreMinioErr :: ServiceErr -> Minio ()
81+ignoreMinioErr = return . const ()
82+
83+
11084main :: IO ()
11185main = do
11286 let bucket = "my-bucket"
11387
114− -- Parse command line argument
88+ -- Parse command line argument, namely --filename.
11589 filepath <- execParser cmdParser
11690 let object = pack $ takeBaseName filepath
11791
11892 res <- runMinio minioPlayCI $ do
11993 -- Make a bucket; catch bucket already exists exception if thrown.
120− bErr <- try $ makeBucket bucket Nothing
121−
122− -- If the bucket already exists, we would get a specific
123− -- `ServiceErr` exception thrown.
124− case bErr of
125− Left BucketAlreadyOwnedByYou -> return ()
126− Left e -> throwIO e
127− Right _ -> return ()
94+ catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
12895
129− -- Upload filepath to bucket; object name is derived from filepath.
130− fPutObject bucket object filepath defaultPutObjectOptions
96+ -- Upload filepath to bucket; object is derived from filepath.
97+ fPutObject bucket object filepath
13198
13299 case res of
133− Left e -> putStrLn $ "file upload failed due to " ++ show e
100+ Left e -> putStrLn $ "file upload failed due to " ++ (show e)
134101 Right () -> putStrLn "file upload succeeded."
135102```
136103
@@ -144,55 +111,3 @@ main = do
144111## Contribute
145112
146113[Contributors Guide](https://github.com/minio/minio-hs/blob/master/CONTRIBUTING.md)
147−
148−### Development
149−
150−#### Download the source
151−
152−```sh
153−$ git clone https://github.com/minio/minio-hs.git
154−$ cd minio-hs/
155−```
156−
157−#### Build the package:
158−
159−With `cabal`:
160−
161−```sh
162−$ # Configure cabal for development enabling all optional flags defined by the package.
163−$ cabal configure --enable-tests --test-show-details=direct -fexamples -fdev -flive-test
164−$ cabal build
165−```
166−
167−With `stack`:
168−
169−``` sh
170−$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
171−```
172−#### Running tests:
173−
174−A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000` with the credentials `access_key=minio` and `secret_key=minio123`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
175−
176−With `cabal`:
177−
178−```sh
179−$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
180−$ cabal test
181−```
182−
183−With `stack`:
184−
185−``` sh
186−$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
187−stack test --flag minio-hs:live-test --flag minio-hs:dev
188−```
189−
190−This will run all the test suites.
191−
192−#### Building documentation:
193−
194−```sh
195−$ cabal haddock
196−$ # OR
197−$ stack haddock
198−```
examples/GetConfig.hs → Setup.hsrenamed+3−15
@@ -1,8 +1,5 @@
1−#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
3−
41--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
2+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
63--
74-- Licensed under the Apache License, Version 2.0 (the "License");
85-- you may not use this file except in compliance with the License.
@@ -17,14 +14,5 @@
1714-- limitations under the License.
1815--
1916
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
23−
24−main :: IO ()
25−main = do
26− res <-
27− runMinio
28− minioPlayCI
29− getConfig
30− print res
17+import Distribution.Simple
18+main = defaultMain
docs/API.md+227−352
@@ -1,8 +1,8 @@
1−# MinIO Haskell SDK API Reference
1+# Minio Haskell SDK API Reference
22
3−## Initialize MinIO Client object.
3+## Initialize Minio Client object.
44
5−### MinIO - for public Play server
5+### Minio - for public Play server
66
77```haskell
88minioPlayCI :: ConnectInfo
@@ -20,20 +20,20 @@ awsCI { connectAccesskey = "your-access-key"
2020
2121```
2222
23−| Bucket operations | Object Operations | Presigned Operations |
24−|:--------------------------------------------------|:----------------------------------------------|:--------------------------------------------------|
25−| [`listBuckets`](#listBuckets) | [`getObject`](#getObject) | [`presignedGetObjectUrl`](#presignedGetObjectUrl) |
26−| [`makeBucket`](#makeBucket) | [`putObject`](#putObject) | [`presignedPutObjectUrl`](#presignedPutObjectUrl) |
27−| [`removeBucket`](#removeBucket) | [`fGetObject`](#fGetObject) | [`presignedPostPolicy`](#presignedPostPolicy) |
28−| [`listObjects`](#listObjects) | [`fPutObject`](#fPutObject) | |
29−| [`listObjectsV1`](#listObjectsV1) | [`copyObject`](#copyObject) | |
30−| [`listIncompleteUploads`](#listIncompleteUploads) | [`removeObject`](#removeObject) | |
31−| [`bucketExists`](#bucketExists) | [`selectObjectContent`](#selectObjectContent) | |
23+|Bucket operations|Object Operations|Presigned Operations|
24+|:---|:---|:---|
25+|[`listBuckets`](#listBuckets) |[`getObject`](#getObject)|[`presignedGetObjectUrl`](#presignedGetObjectUrl)|
26+|[`makeBucket`](#makeBucket)|[`putObject`](#putObject)|[`presignedPutObjectUrl`](#presignedPutObjectUrl)|
27+|[`removeBucket`](#removeBucket)|[`fGetObject`](#fGetObject)|[`presignedPostPolicy`](#presignedPostPolicy)|
28+|[`listObjects`](#listObjects)|[`fPutObject`](#fPutObject)||
29+|[`listObjectsV1`](#listObjectsV1)|[`copyObject`](#copyObject)||
30+|[`listIncompleteUploads`](#listIncompleteUploads)|[`removeObject`](#removeObject)||
31+|[`bucketExists`](#bucketExists)|||
3232
3333## 1. Connecting and running operations on the storage service
3434
35−The Haskell MinIO SDK provides high-level functionality to perform
36−operations on a MinIO server or any AWS S3-like API compatible storage
35+The Haskell Minio SDK provides high-level functionality to perform
36+operations on a Minio server or any AWS S3-like API compatible storage
3737service.
3838
3939### The `ConnectInfo` type
@@ -69,42 +69,42 @@ enable/disable the automatic region discovery behaviour.
6969
7070The parameters in the expression `awsWithRegion region autoDiscover` are:
7171
72−| Parameter | Type | Description |
73−|:---------------|:----------------------------|:---------------------------------------------------------------------------------------------------------------------------|
74−| `region` | _Region_ (alias for `Text`) | The region to connect to by default for all requests. |
75−| `autoDiscover` | _Bool_ | If `True`, region discovery will be enabled. If `False`, discovery is disabled, and all requests go the given region only. |
72+|Parameter|Type|Description|
73+|:---|:---|:---|
74+| `region` | _Region_ (alias for `Text`) | The region to connect to by default for all requests. |
75+| `autoDiscover` | _Bool_ | If `True`, region discovery will be enabled. If `False`, discovery is disabled, and all requests go the given region only.|
7676
7777#### minioPlayCI :: ConnectInfo
7878
7979This constructor provides connection and authentication information to
80−connect to the public MinIO Play server at
81−`https://play.min.io/`.
80+connect to the public Minio Play server at
81+`https://play.minio.io:9000/`.
8282
8383#### minioCI :: Text -> Int -> Bool -> ConnectInfo
8484
85−Use to connect to a MinIO server.
85+Use to connect to a Minio server.
8686
8787The parameters in the expression `minioCI host port isSecure` are:
8888
89−| Parameter | Type | Description |
90−|:-----------|:-------|:--------------------------------------------------------|
91−| `host` | _Text_ | Hostname of the MinIO or other S3-API compatible server |
92−| `port` | _Int_ | Port number to connect to |
93−| `isSecure` | _Bool_ | Does the server use HTTPS? |
89+|Parameter|Type|Description|
90+|:---|:---|:---|
91+| `host` | _Text_ | Hostname of the Minio or other S3-API compatible server |
92+| `port` | _Int_ | Port number to connect to|
93+| `isSecure` | _Bool_ | Does the server use HTTPS? |
9494
9595#### The ConnectInfo fields and Default instance
9696
9797The following table shows the fields in the `ConnectInfo` record-type:
9898
99−| Field | Type | Description |
100−|:----------------------------|:----------------------------|:--------------------------------------------------------------------------------------------------|
101−| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
102−| `connectPort` | _Int_ | Port number on which the server listens. Defaults to `9000`. |
103−| `connectAccessKey` | _Text_ | Access key to use in authentication. Defaults to `minio`. |
104−| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
105−| `connectIsSecure` | _Bool_ | Specifies if the server used TLS. Defaults to `False` |
106−| `connectRegion` | _Region_ (alias for `Text`) | Specifies the region to use. Defaults to 'us-east-1' |
107−| `connectAutoDiscoverRegion` | _Bool_ | Specifies if the library should automatically discover the region of a bucket. Defaults to `True` |
99+| Field | Type | Description |
100+|:---|:---|:---|
101+| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
102+| `connectPort` | _Int_ | Port number on which the server listens. Defaults to `9000`. |
103+| `connectAccessKey` | _Text_ | Access key to use in authentication. Defaults to `minio`. |
104+| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
105+| `connectIsSecure` | _Bool_ | Specifies if the server used TLS. Defaults to `False` |
106+| `connectRegion` | _Region_ (alias for `Text`) | Specifies the region to use. Defaults to 'us-east-1' |
107+| `connectAutoDiscoverRegion` | _Bool_ | Specifies if the library should automatically discover the region of a bucket. Defaults to `True`|
108108
109109The `def` value of type `ConnectInfo` has all the above default
110110values.
@@ -112,7 +112,7 @@ values.
112112### The Minio Monad
113113
114114This monad provides the required environment to perform requests
115−against a MinIO or other S3 API compatible server. It uses the
115+against a Minio or other S3 API compatible server. It uses the
116116connection information from the `ConnectInfo` value provided to it. It
117117performs connection pooling, bucket location caching, error handling
118118and resource clean-up actions.
@@ -148,17 +148,17 @@ Lists buckets.
148148
149149__Return Value__
150150
151−| Return type | Description |
152−|:---------------------|:----------------|
153−| _Minio [BucketInfo]_ | List of buckets |
151+|Return type |Description |
152+|:---|:---|
153+| _Minio [BucketInfo]_| List of buckets |
154154
155155
156156__BucketInfo record type__
157157
158−| Field | Type | Description |
159−|:-----------------|:---------------------------|:----------------------------|
160−| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
161−| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
158+|Field |Type |Description |
159+|:---|:---| :---|
160+| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
161+| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
162162
163163
164164<a name="makeBucket"></a>
@@ -171,10 +171,10 @@ __Parameters__
171171
172172In the expression `makeBucket bucketName region` the arguments are:
173173
174−| Param | Type | Description |
175−|--------------|-----------------------------|-----------------------------------------------------------------------------------------------------|
176−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
177−| `region` | _Maybe Region_ | Region where the bucket is to be created. If not specified, default to the region in `ConnectInfo`. |
174+| Param | Type | Description |
175+|---|---|---|
176+|`bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
177+| `region` | _Maybe Region_ | Region where the bucket is to be created. If not specified, default to the region in `ConnectInfo`.|
178178
179179__Example__
180180
@@ -185,6 +185,7 @@ main :: IO ()
185185main = do
186186 res <- runMinio minioPlayCI $ do
187187 makeBucket bucketName (Just "us-east-1")
188+
188189 case res of
189190 Left err -> putStrLn $ "Failed to make bucket: " ++ (show res)
190191 Right _ -> putStrLn $ "makeBucket successful."
@@ -200,9 +201,9 @@ __Parameters__
200201
201202In the expression `removeBucket bucketName` the arguments are:
202203
203−| Param | Type | Description |
204−|--------------|-----------------------------|--------------------|
205−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
204+| Param | Type | Description |
205+|---|---|---|
206+|`bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
206207
207208
208209__Example__
@@ -224,7 +225,7 @@ main = do
224225
225226
226227<a name="listObjects"></a>
227−### listObjects :: Bucket -> Maybe Text -> Bool -> C.ConduitM () ObjectInfo Minio ()
228+### listObjects :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
228229
229230List objects in the given bucket, implements version 2 of AWS S3 API.
230231
@@ -233,44 +234,34 @@ __Parameters__
233234In the expression `listObjects bucketName prefix recursive` the
234235arguments are:
235236
236−| Param | Type | Description |
237−|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
238−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
239−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
240−| `recursive` | _Bool_ | `True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
237+|Param |Type |Description |
238+|:---|:---| :---|
239+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
240+| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
241+| `recursive` | _Bool_ |`True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
241242
242243__Return Value__
243244
244−| Return type | Description |
245−|:------------------------------------|:------------------------------------------------------------------------|
246−| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
245+|Return type |Description |
246+|:---|:---|
247+| _C.Producer Minio ObjectInfo_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
247248
248249__ObjectInfo record type__
249250
250−| Field | Type | Description |
251−|:-------------|:----------------------------|:-------------------------------------|
252−| `oiObject` | _Object_ (alias for `Text`) | Name of object |
253−| `oiModTime` | _UTCTime_ | Last modified time of the object |
254−| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
255−| `oiSize` | _Int64_ | Size of the object in bytes |
256−| `oiMetadata` | _HashMap Text Text_ | Map of key-value user-metadata pairs |
251+|Field |Type |Description |
252+|:---|:---| :---|
253+|`oiObject` | _Object_ (alias for `Text`) | Name of object |
254+|`oiModTime` | _UTCTime_ | Last modified time of the object |
255+|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
256+|`oiSize` | _Int64_ | Size of the object in bytes |
257257
258258__Example__
259259
260260``` haskell
261−{-# LANGUAGE OverloadedStrings #-}
262−import Network.Minio
263−
264−import Conduit
265−import Prelude
261+{-# Language OverloadedStrings #-}
266262
267−
268−-- | The following example uses MinIO play server at
269−-- https://play.min.io. The endpoint and associated
270−-- credentials are provided via the libary constant,
271−--
272−-- > minioPlayCI :: ConnectInfo
273−--
263+import Data.Conduit (($$))
264+import Conduit.Combinators (sinkList)
274265
275266main :: IO ()
276267main = do
@@ -278,14 +269,15 @@ main = do
278269 bucket = "test"
279270
280271 -- Performs a recursive listing of all objects under bucket "test"
281− -- on play.min.io.
282− res <- runMinio minioPlayCI $
283− runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
272+ -- on play.minio.io.
273+ res <- runMinio minioPlayCI $ do
274+ listObjects bucket Nothing True $$ sinkList
284275 print res
276+
285277```
286278
287279<a name="listObjectsV1"></a>
288−### listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.ConduitM () ObjectInfo Minio ()
280+### listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
289281
290282List objects in the given bucket, implements version 1 of AWS S3 API. This API
291283is provided for legacy S3 compatible object storage endpoints.
@@ -295,43 +287,34 @@ __Parameters__
295287In the expression `listObjectsV1 bucketName prefix recursive` the
296288arguments are:
297289
298−| Param | Type | Description |
299−|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
300−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
301−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
302−| `recursive` | _Bool_ | `True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
290+|Param |Type |Description |
291+|:---|:---| :---|
292+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
293+| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
294+| `recursive` | _Bool_ |`True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
303295
304296__Return Value__
305297
306−| Return type | Description |
307−|:------------------------------------|:------------------------------------------------------------------------|
308−| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
298+|Return type |Description |
299+|:---|:---|
300+| _C.Producer Minio ObjectInfo_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
309301
310302__ObjectInfo record type__
311303
312−| Field | Type | Description |
313−|:------------|:----------------------------|:---------------------------------|
314−| `oiObject` | _Object_ (alias for `Text`) | Name of object |
315−| `oiModTime` | _UTCTime_ | Last modified time of the object |
316−| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
317−| `oiSize` | _Int64_ | Size of the object in bytes |
304+|Field |Type |Description |
305+|:---|:---| :---|
306+|`oiObject` | _Object_ (alias for `Text`) | Name of object |
307+|`oiModTime` | _UTCTime_ | Last modified time of the object |
308+|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
309+|`oiSize` | _Int64_ | Size of the object in bytes |
318310
319311__Example__
320312
321313``` haskell
322−{-# LANGUAGE OverloadedStrings #-}
323−import Network.Minio
324−
325−import Conduit
326−import Prelude
327−
314+{-# Language OverloadedStrings #-}
328315
329−-- | The following example uses MinIO play server at
330−-- https://play.min.io. The endpoint and associated
331−-- credentials are provided via the libary constant,
332−--
333−-- > minioPlayCI :: ConnectInfo
334−--
316+import Data.Conduit (($$))
317+import Conduit.Combinators (sinkList)
335318
336319main :: IO ()
337320main = do
@@ -339,10 +322,11 @@ main = do
339322 bucket = "test"
340323
341324 -- Performs a recursive listing of all objects under bucket "test"
342− -- on play.min.io.
343− res <- runMinio minioPlayCI $
344− runConduit $ listObjectsV1 bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
325+ -- on play.minio.io.
326+ res <- runMinio minioPlayCI $ do
327+ listObjectsV1 bucket Nothing True $$ sinkList
345328 print res
329+
346330```
347331
348332<a name="listIncompleteUploads"></a>
@@ -355,51 +339,43 @@ __Parameters__
355339In the expression `listIncompleteUploads bucketName prefix recursive`
356340the parameters are:
357341
358−| Param | Type | Description |
359−|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
360−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
361−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have. |
362−| `recursive` | _Bool_ | `True` indicates recursive style listing and `Talse` indicates directory style listing delimited by '/'. |
342+|Param |Type |Description |
343+|:---|:---| :---|
344+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
345+| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have. |
346+| `recursive` | _Bool_ |`True` indicates recursive style listing and `Talse` indicates directory style listing delimited by '/'. |
363347
364348__Return Value__
365349
366350|Return type |Description |
367351|:---|:---|
368−| _C.ConduitM () UploadInfo Minio ()_ | A Conduit Producer of `UploadInfo` values corresponding to each incomplete multipart upload |
352+| _C.Producer Minio UploadInfo_ | A Conduit Producer of `UploadInfo` values corresponding to each incomplete multipart upload |
369353
370354__UploadInfo record type__
371355
372−| Field | Type | Description |
373−|:-------------|:---------|:------------------------------------------|
374−| `uiKey` | _Object_ | Name of incompletely uploaded object |
375−| `uiUploadId` | _String_ | Upload ID of incompletely uploaded object |
376−| `uiSize` | _Int64_ | Size of incompletely uploaded object |
356+|Field |Type |Description |
357+|:---|:---| :---|
358+|`uiKey` | _Object_ |Name of incompletely uploaded object |
359+|`uiUploadId` | _String_ |Upload ID of incompletely uploaded object |
360+|`uiSize` | _Int64_ |Size of incompletely uploaded object |
377361
378362__Example__
379363
380364```haskell
381−{-# LANGUAGE OverloadedStrings #-}
382−import Network.Minio
383−
384−import Conduit
385−import Prelude
365+{-# Language OverloadedStrings #-}
386366
387−-- | The following example uses MinIO play server at
388−-- https://play.min.io. The endpoint and associated
389−-- credentials are provided via the libary constant,
390−--
391−-- > minioPlayCI :: ConnectInfo
392−--
367+import Data.Conduit (($$))
368+import Conduit.Combinators (sinkList)
393369
394370main :: IO ()
395371main = do
396372 let
397373 bucket = "test"
398374
399− -- Performs a recursive listing of incomplete uploads under bucket "test"
400− -- on a local MinIO server.
401− res <- runMinio minioPlayCI $
402− runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
375+ -- Performs a recursive listing of all incompletely uploaded objects
376+ -- under bucket "test" on play.minio.io.
377+ res <- runMinio minioPlayCI $ do
378+ listIncompleteUploads bucket Nothing True $$ sinkList
403379 print res
404380
405381```
@@ -407,126 +383,97 @@ main = do
407383## 3. Object operations
408384
409385<a name="getObject"></a>
410−### getObject :: Bucket -> Object -> GetObjectOptions -> Minio (C.ConduitM () ByteString Minio ())
386+### getObject :: Bucket -> Object -> Minio (C.ResumableSource Minio ByteString)
411387
412−Get an object from the S3 service, optionally object ranges can be provided as well.
388+Get an object from the service.
413389
414390__Parameters__
415391
416−In the expression `getObject bucketName objectName opts` the parameters
392+In the expression `getObject bucketName objectName` the parameters
417393are:
418394
419−| Param | Type | Description |
420−|:-------------|:----------------------------|:----------------------------------------------------------------------------|
421−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
422−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
423−| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
424−
425−__GetObjectOptions record type__
426−
427−| Field | Type | Description |
428−|:-----------------------|:--------------------------------|:------------------------------------------------------------------------------------------------------|
429−| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object |
430−| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
431−| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
432−| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
433−| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
395+|Param |Type |Description |
396+|:---|:---| :---|
397+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
398+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
434399
435400__Return Value__
436401
437402The return value can be incrementally read to process the contents of
438403the object.
439−| Return type | Description |
440−|:--------------------------------------------|:-----------------------------------------|
441−| _Minio (C.ConduitM () ByteString Minio ())_ | A Conduit source of `ByteString` values. |
404+|Return type |Description |
405+|:---|:---|
406+| _C.ResumableSource Minio ByteString_ | A Conduit ResumableSource of `ByteString` values. |
442407
443408__Example__
444409
445410```haskell
446−{-# LANGUAGE OverloadedStrings #-}
447−import Network.Minio
448−
449−import qualified Data.Conduit as C
450−import qualified Data.Conduit.Binary as CB
451−
452−import Prelude
411+{-# Language OverloadedStrings #-}
453412
454−-- | The following example uses MinIO play server at
455−-- https://play.min.io. The endpoint and associated
456−-- credentials are provided via the libary constant,
457−--
458−-- > minioPlayCI :: ConnectInfo
459−--
413+import Network.Minio
414+import Data.Conduit (($$+-))
415+import Data.Conduit.Binary (sinkLbs)
416+import qualified Data.ByteString.Lazy as LB
460417
461418main :: IO ()
462419main = do
463420 let
464− bucket = "my-bucket"
465− object = "my-object"
421+ bucket = "mybucket"
422+ object = "myobject"
423+
424+ -- Lists the parts in an incompletely uploaded object identified by
425+ -- bucket, object and upload ID.
466426 res <- runMinio minioPlayCI $ do
467− src <- getObject bucket object def
468− C.connect src $ CB.sinkFileCautious "/tmp/my-object"
427+ source <- getObject bucket object
428+ source $$+- sinkLbs
469429
470− case res of
471− Left e -> putStrLn $ "getObject failed." ++ (show e)
472− Right _ -> putStrLn "getObject succeeded."
430+ -- the following the prints the contents of the object.
431+ putStrLn $ either
432+ (("Failed to getObject: " ++) . show)
433+ (("Read an object of length: " ++) . show . LB.length)
434+ res
473435```
474436
475437<a name="putObject"></a>
476−### putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio () -> Maybe Int64 -> PutObjectOptions -> Minio ()
438+### putObject :: Bucket -> Object -> C.Producer Minio ByteString -> Maybe Int64 -> Minio ()
477439Uploads an object to a bucket in the service, from the given input
478−byte stream of optionally supplied length. Optionally you can also specify
479−additional metadata for the object.
440+byte stream of optionally supplied length
480441
481442__Parameters__
482443
483444In the expression `putObject bucketName objectName inputSrc` the parameters
484445are:
485446
486−| Param | Type | Description |
487−|:-------------|:------------------------------------|:------------------------------------------------------------------|
488−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
489−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
490−| `inputSrc` | _C.ConduitM () ByteString Minio ()_ | A Conduit producer of `ByteString` values |
491−| `size` | _Int64_ | Provide stream size (optional) |
492−| `opts` | _PutObjectOptions_ | Optional parameters to provide additional metadata for the object |
447+|Param |Type |Description |
448+|:---|:---| :---|
449+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
450+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
451+| `inputSrc` | _C.Producer Minio ByteString_ | A Conduit Producer of `ByteString` values |
493452
494453__Example__
495454
496455```haskell
497−{-# LANGUAGE OverloadedStrings #-}
498−import Network.Minio
499−
456+{-# Language OverloadedStrings #-}
457+import Network.Minio
500458import qualified Data.Conduit.Combinators as CC
501459
502−import Prelude
503−
504−-- | The following example uses MinIO play server at
505−-- https://play.min.io. The endpoint and associated
506−-- credentials are provided via the libary constant,
507−--
508−-- > minioPlayCI :: ConnectInfo
509−--
510−
511460main :: IO ()
512461main = do
513462 let
514− bucket = "test"
515− object = "obj"
516− localFile = "/etc/lsb-release"
517− kb15 = 15 * 1024
463+ bucket = "mybucket"
464+ object = "myobject"
465+ kb15 = 15 * 1024
518466
519− -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
520− res <- runMinio minioPlayCI $
521− putObject bucket object (CC.repeat "a") (Just kb15) def
522− case res of
523− Left e -> putStrLn $ "putObject failed." ++ show e
524− Right () -> putStrLn "putObject succeeded."
467+ res <- runMinio minioPlayCI $ do
468+ putObject bucket object (CC.repeat "a") (Just kb15)
525469
470+ case res of
471+ Left e -> putStrLn $ "Failed to putObject " ++ show bucket ++ "/" ++ show object
472+ Right _ -> putStrLn "PutObject was successful"
526473```
527474
528475<a name="fGetObject"></a>
529−### fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
476+### fGetObject :: Bucket -> Object -> FilePath -> Minio ()
530477Downloads an object from a bucket in the service, to the given file
531478
532479__Parameters__
@@ -534,23 +481,11 @@ __Parameters__
534481In the expression `fGetObject bucketName objectName inputFile` the parameters
535482are:
536483
537−| Param | Type | Description |
538−|:-------------|:----------------------------|:----------------------------------------------------------------------------|
539−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
540−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
541−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
542−| `opts` | _GetObjectOptions_ | Options for GET requests specifying additional options like If-Match, Range |
543−
544−
545−__GetObjectOptions record type__
546−
547−| Field | Type | Description |
548−|:-----------------------|:--------------------------------|:------------------------------------------------------------------------------------------------------|
549−| `gooRange` | `Maybe ByteRanges` | Represents the byte range of object. E.g ByteRangeFromTo 0 9 represents first ten bytes of the object |
550−| `gooIfMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object should match |
551−| `gooIfNoneMatch` | `Maybe ETag` (alias for `Text`) | (Optional) ETag of object shouldn't match |
552−| `gooIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since object wasn't modified |
553−| `gooIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since object was modified |
484+|Param |Type |Description |
485+|:---|:---| :---|
486+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
487+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
488+| `inputFile` | _FilePath_ | Path to the file to be uploaded |
554489
555490``` haskell
556491
@@ -561,8 +496,8 @@ import Data.Conduit (($$+-))
561496import Data.Conduit.Binary (sinkLbs)
562497import Prelude
563498
564−-- | The following example uses MinIO play server at
565−-- https://play.min.io. The endpoint and associated
499+-- | The following example uses minio's play server at
500+-- https://play.minio.io:9000. The endpoint and associated
566501-- credentials are provided via the libary constant,
567502--
568503-- > minioPlayCI :: ConnectInfo
@@ -576,7 +511,7 @@ main = do
576511 localFile = "/etc/lsb-release"
577512
578513 res <- runMinio minioPlayCI $ do
579− src <- fGetObject bucket object localFile def
514+ src <- fGetObject bucket object localFile
580515 (src $$+- sinkLbs)
581516
582517 case res of
@@ -593,11 +528,11 @@ __Parameters__
593528In the expression `fPutObject bucketName objectName inputFile` the parameters
594529are:
595530
596−| Param | Type | Description |
597−|:-------------|:----------------------------|:--------------------------------|
598−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
599−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
600−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
531+|Param |Type |Description |
532+|:---|:---| :---|
533+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
534+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
535+| `inputFile` | _FilePath_ | Path to the file to be uploaded |
601536
602537__Example__
603538
@@ -622,38 +557,31 @@ main = do
622557```
623558
624559<a name="copyObject"></a>
625−### copyObject :: DestinationInfo -> SourceInfo -> Minio ()
560+### copyObject :: Bucket -> Object -> CopyPartSource -> Minio ()
626561Copies content of an object from the service to another
627562
628563__Parameters__
629564
630−In the expression `copyObject dstInfo srcInfo` the parameters
565+In the expression `copyObject bucketName objectName cps` the parameters
631566are:
632567
633−| Param | Type | Description |
634−|:----------|:------------------|:----------------------------------------------------------|
635−| `dstInfo` | _DestinationInfo_ | A value representing properties of the destination object |
636−| `srcInfo` | _SourceInfo_ | A value representing properties of the source object |
637−
568+|Param |Type |Description |
569+|:---|:---| :---|
570+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
571+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
572+| `cps` | _CopyPartSource_ | A value representing properties of the source object |
638573
639−__SourceInfo record type__
640574
641−| Field | Type | Description |
642−|:-----------------------|:-----------------------|:----------------------------------------------------------------------------------------------------------|
643−| `srcBucket` | `Bucket` | Name of source bucket |
644−| `srcObject` | `Object` | Name of source object |
645−| `srcRange` | `Maybe (Int64, Int64)` | (Optional) Represents the byte range of source object. (0, 9) represents first ten bytes of source object |
646−| `srcIfMatch` | `Maybe Text` | (Optional) ETag source object should match |
647−| `srcIfNoneMatch` | `Maybe Text` | (Optional) ETag source object shouldn't match |
648−| `srcIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since source object wasn't modified |
649−| `srcIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since source object was modified |
575+__CopyPartSource record type__
650576
651−__Destination record type__
652−
653−| Field | Type | Description |
654−|:------------|:---------|:-----------------------------------------------------|
655−| `dstBucket` | `Bucket` | Name of destination bucket in server-side copyObject |
656−| `dstObject` | `Object` | Name of destination object in server-side copyObject |
577+|Field |Type |Description |
578+|:---|:---| :---|
579+| `cpSource` | `Text`| Name of source object formatted as "/srcBucket/srcObject" |
580+| `cpSourceRange` | `Maybe (Int64, Int64)` | Represents the byte range of source object. (0, 9) represents first ten bytes of source object|
581+| `cpSourceIfMatch` | `Maybe Text` | (Optional) ETag source object should match |
582+| `cpSourceIfNoneMatch` | `Maybe Text` | (Optional) ETag source object shouldn't match |
583+| `cpSourceIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since source object wasn't modified |
584+| `cpSourceIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since source object was modified |
657585
658586__Example__
659587
@@ -666,13 +594,13 @@ main = do
666594 let
667595 bucket = "mybucket"
668596 object = "myobject"
669− objectCopy = "obj-copy"
597+ srcObject = "/mybucket/srcObject"
670598
671599 res <- runMinio minioPlayCI $ do
672− copyObject def { dstBucket = bucket, dstObject = objectCopy } def { srcBucket = bucket, srcObject = object }
600+ copyObject bucket object def { cpSource = srcObject }
673601
674602 case res of
675− Left e -> putStrLn $ "Failed to copyObject " ++ show bucket ++ show "/" ++ show object
603+ Left e -> putStrLn $ "Failed to copyObject " ++ show srcObject"
676604 Right _ -> putStrLn "copyObject was successful"
677605```
678606
@@ -685,10 +613,10 @@ __Parameters__
685613In the expression `removeObject bucketName objectName` the parameters
686614are:
687615
688−| Param | Type | Description |
689−|:-------------|:----------------------------|:-------------------|
690−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
691−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
616+|Param |Type |Description |
617+|:---|:---| :---|
618+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
619+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
692620
693621__Example__
694622
@@ -719,10 +647,10 @@ __Parameters__
719647In the expression `removeIncompleteUpload bucketName objectName` the parameters
720648are:
721649
722−| Param | Type | Description |
723−|:-------------|:----------------------------|:-------------------|
724−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
725−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
650+|Param |Type |Description |
651+|:---|:---| :---|
652+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
653+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
726654
727655__Example__
728656
@@ -744,59 +672,6 @@ main = do
744672 Right _ -> putStrLn "Removed incomplete upload successfully"
745673```
746674
747−<a name="selectObjectContent"></a>
748−### selectObjectContent :: Bucket -> Object -> SelectRequest -> Minio (ConduitT () EventMessage Minio ())
749−Removes an ongoing multipart upload of an object from the service
750−
751−__Parameters__
752−
753−In the expression `selectObjectContent bucketName objectName selReq`
754−the parameters are:
755−
756−| Param | Type | Description |
757−|:-------------|:----------------------------|:--------------------------|
758−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
759−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
760−| `selReq` | _SelectRequest_ | Select request parameters |
761−
762−__SelectRequest record__
763−
764−This record is created using `selectRequest`. Please refer to the Haddocks for further information.
765−
766−__Return Value__
767−
768−The return value can be used to read individual `EventMessage`s in the response. Please refer to the Haddocks for further information.
769−
770−|Return type | Description |
771−|:---|:---|
772−| _Minio (C.conduitT () EventMessage Minio ())_ | A Conduit source of `EventMessage` values. |
773−
774−__Example__
775−
776−```haskell
777−{-# Language OverloadedStrings #-}
778−import Network.Minio
779−
780−import qualified Conduit as C
781−
782−main :: IO ()
783−main = do
784− let
785− bucket = "mybucket"
786− object = "myobject"
787−
788− res <- runMinio minioPlayCI $ do
789− let sr = selectRequest "Select * from s3object"
790− defaultCsvInput defaultCsvOutput
791− res <- selectObjectContent bucket object sr
792− C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
793−
794− case res of
795− Left _ -> putStrLn "Failed!"
796− Right _ -> putStrLn "Success!"
797−```
798−
799−
800675<a name="BucketExists"></a>
801676### bucketExists :: Bucket -> Minio Bool
802677Checks if a bucket exists.
@@ -805,9 +680,9 @@ __Parameters__
805680
806681In the expression `bucketExists bucketName` the parameters are:
807682
808−| Param | Type | Description |
809−|:-------------|:----------------------------|:-------------------|
810−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
683+|Param |Type |Description |
684+|:---|:---| :---|
685+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
811686
812687
813688## 4. Presigned operations
@@ -829,22 +704,22 @@ __Parameters__
829704In the expression `presignedGetObjectUrl bucketName objectName expiry queryParams headers`
830705the parameters are:
831706
832−| Param | Type | Description |
833−|:--------------|:---------------------------------------------------------------|:------------------------------------------------|
834−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
835−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
836−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
837−| `queryParams` | _Query_ (from package `http-types:Network.HTTP.Types`) | Query parameters to add to the URL |
838−| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
707+|Param |Type |Description |
708+|:---|:---| :---|
709+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
710+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
711+| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
712+| `queryParams` | _Query_ (from package `http-types:Network.HTTP.Types`) | Query parameters to add to the URL |
713+| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
839714
840715__Return Value__
841716
842717Returns the generated URL - it will include authentication
843718information.
844719
845−| Return type | Description |
846−|:-------------|:------------------------|
847−| _ByteString_ | Generated presigned URL |
720+|Return type |Description |
721+|:---|:---|
722+| _ByteString_ | Generated presigned URL |
848723
849724__Example__
850725
@@ -887,21 +762,21 @@ __Parameters__
887762In the expression `presignedPutObjectUrl bucketName objectName expiry headers`
888763the parameters are:
889764
890−| Param | Type | Description |
891−|:-------------|:---------------------------------------------------------------|:------------------------------------------------|
892−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
893−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
894−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
895−| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
765+|Param |Type |Description |
766+|:---|:---| :---|
767+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
768+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
769+| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
770+| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
896771
897772__Return Value__
898773
899774Returns the generated URL - it will include authentication
900775information.
901776
902−| Return type | Description |
903−|:-------------|:------------------------|
904−| _ByteString_ | Generated presigned URL |
777+|Return type |Description |
778+|:---|:---|
779+| _ByteString_ | Generated presigned URL |
905780
906781__Example__
907782
@@ -929,7 +804,7 @@ main = do
929804```
930805
931806<a name="presignedPostPolicy"></a>
932−### presignedPostPolicy :: PostPolicy -> Minio (ByteString, HashMap Text ByteString)
807+### presignedPostPolicy :: PostPolicy -> Minio (ByteString, Map.Map Text ByteString)
933808
934809Generate a presigned URL and POST policy to upload files via a POST
935810request. This is intended for browser uploads and generates form data
@@ -941,10 +816,10 @@ The `PostPolicy` argument is created using the `newPostPolicy` function:
941816
942817In the expression `newPostPolicy expirationTime conditions` the parameters are:
943818
944−| Param | Type | Description |
945−|:-----------------|:--------------------------------------------------|:---------------------------------------------|
946−| `expirationTime` | _UTCTime_ (from package `time:Data.Time.UTCTime`) | The expiration time for the policy |
947−| `conditions` | _[PostPolicyConditions]_ | List of conditions to be added to the policy |
819+|Param | Type| Description |
820+|:---|:---|:---|
821+| `expirationTime` | _UTCTime_ (from package `time:Data.Time.UTCTime`) | The expiration time for the policy |
822+| `conditions` | _[PostPolicyConditions]_ | List of conditions to be added to the policy |
948823
949824The policy conditions are created using various helper functions -
950825please refer to the Haddocks for details.
@@ -966,7 +841,7 @@ import Network.Minio
966841
967842import qualified Data.ByteString as B
968843import qualified Data.ByteString.Char8 as Char8
969−import qualified Data.HashMap.Strict as H
844+import qualified Data.Map.Strict as Map
970845import qualified Data.Text.Encoding as Enc
971846import qualified Data.Time as Time
972847
@@ -1006,7 +881,7 @@ main = do
1006881 let
1007882 formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
1008883 "'", v, "'"]
1009− formOptions = B.intercalate " " $ map formFn $ H.toList formData
884+ formOptions = B.intercalate " " $ map formFn $ Map.toList formData
1010885
1011886
1012887 return $ B.intercalate " " $
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+8−5
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,17 +16,20 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−{-# LANGUAGE OverloadedStrings #-}
2019
21−import Control.Monad.IO.Class (liftIO)
20+{-# Language OverloadedStrings #-}
2221import Network.Minio
22+
23+import Control.Monad.IO.Class (liftIO)
2324import Prelude
2425
2526-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
27+-- https://play.minio.io:9000. 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"
examples/CopyObject.hs+21−17
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,40 +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
21−import Network.Minio
22−import UnliftIO.Exception (catch, throwIO)
23+import Control.Monad.Catch (catchIf)
24+import qualified Data.Text as T
25+import Prelude
2326
2427-- | The following example uses minio's play server at
25−-- https://play.min.io. The endpoint and associated
28+-- https://play.minio.io:9000. The endpoint and associated
2629-- credentials are provided via the libary constant,
2730--
2831-- > minioPlayCI :: ConnectInfo
32+--
33+
34+ignoreMinioErr :: ServiceErr -> Minio ()
35+ignoreMinioErr = return . const ()
36+
2937main :: IO ()
3038main = do
31− let bucket = "test"
39+ let
40+ bucket = "test"
3241 object = "obj"
3342 objectCopy = "obj-copy"
3443 localFile = "/etc/lsb-release"
3544
3645 res1 <- runMinio minioPlayCI $ do
3746 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
38− catch
39− (makeBucket bucket Nothing)
40− ( \e -> case e of
41− BucketAlreadyOwnedByYou -> return ()
42− _ -> throwIO e
43− )
47+ catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
4448
4549 -- 2. Upload a file to bucket/object.
46− fPutObject bucket object localFile defaultPutObjectOptions
50+ fPutObject bucket object localFile
4751
4852 -- 3. Copy bucket/object to bucket/objectCopy.
49− copyObject
50− defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
51− defaultSourceInfo {srcBucket = bucket, srcObject = object}
53+ copyObject bucket objectCopy def {
54+ cpSource = T.concat ["/", bucket, "/", object]
55+ }
5256
5357 case res1 of
54− Left e -> putStrLn $ "copyObject failed." ++ show e
58+ Left e -> putStrLn $ "copyObject failed." ++ (show e)
5559 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+31−31
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
2+-- stack --resolver lts-9.1 runghc --package minio-hs --package optparse-applicative --package filepath
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,39 +16,43 @@
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 Control.Monad.Catch (catchIf)
26+import Control.Monad.IO.Class (liftIO)
27+import Data.Monoid ((<>))
28+import Data.Text (pack)
29+import Options.Applicative
30+import Prelude
31+import System.FilePath.Posix
2832
2933-- | The following example uses minio's play server at
30−-- https://play.min.io. The endpoint and associated
34+-- https://play.minio.io:9000. 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")
45+
46+cmdParser = info
47+ (helper <*> fileNameArgs)
48+ (fullDesc
49+ <> progDesc "FileUploader"
50+ <> header
51+ "FileUploader - a simple file-uploader program using minio-hs")
52+
53+ignoreMinioErr :: ServiceErr -> Minio ()
54+ignoreMinioErr = return . const ()
4255
43−cmdParser :: ParserInfo FilePath
44−cmdParser =
45− info
46− (helper <*> fileNameArgs)
47− ( fullDesc
48− <> progDesc "FileUploader"
49− <> header
50− "FileUploader - a simple file-uploader program using minio-hs"
51− )
5256
5357main :: IO ()
5458main = do
@@ -60,15 +64,11 @@ main = do
6064
6165 res <- runMinio minioPlayCI $ do
6266 -- Make a bucket; catch bucket already exists exception if thrown.
63− bErr <- try $ makeBucket bucket Nothing
64− case bErr of
65− Left BucketAlreadyOwnedByYou -> return ()
66− Left e -> throwIO e
67− Right _ -> return ()
67+ catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
6868
6969 -- Upload filepath to bucket; object is derived from filepath.
70− fPutObject bucket object filepath defaultPutObjectOptions
70+ fPutObject bucket object filepath
7171
7272 case res of
73− Left e -> putStrLn $ "file upload failed due to " ++ show e
73+ Left e -> putStrLn $ "file upload failed due to " ++ (show e)
7474 Right () -> putStrLn "file upload succeeded."
examples/GetObject.hs+15−11
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,26 +16,30 @@
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
22−import qualified Data.Conduit.Binary as CB
23−import Network.Minio
24−import Prelude
23+import Data.Conduit (($$+-))
24+import Data.Conduit.Binary (sinkLbs)
25+import Prelude
2526
2627-- | The following example uses minio's play server at
27−-- https://play.min.io. The endpoint and associated
28+-- https://play.minio.io:9000. The endpoint and associated
2829-- credentials are provided via the libary constant,
2930--
3031-- > minioPlayCI :: ConnectInfo
32+--
33+
3134main :: IO ()
3235main = do
33− let bucket = "my-bucket"
36+ let
37+ bucket = "my-bucket"
3438 object = "my-object"
3539 res <- runMinio minioPlayCI $ do
36− src <- getObject bucket object defaultGetObjectOptions
37− C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
40+ src <- getObject bucket object
41+ (src $$+- sinkLbs)
3842
3943 case res of
40− Left e -> putStrLn $ "getObject failed." ++ show e
44+ Left e -> putStrLn $ "getObject failed." ++ (show e)
4145 Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs+14−11
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,25 +16,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
26−-- https://play.min.io. The endpoint and associated
27+-- https://play.minio.io:9000. 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.hsdeleted−37
@@ -1,37 +0,0 @@
1−#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
3−
4−--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6−--
7−-- Licensed under the Apache License, Version 2.0 (the "License");
8−-- you may not use this file except in compliance with the License.
9−-- You may obtain a copy of the License at
10−--
11−-- http://www.apache.org/licenses/LICENSE-2.0
12−--
13−-- Unless required by applicable law or agreed to in writing, software
14−-- distributed under the License is distributed on an "AS IS" BASIS,
15−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16−-- See the License for the specific language governing permissions and
17−-- limitations under the License.
18−--
19−
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
23−
24−main :: IO ()
25−main = do
26− res <- runMinio minioPlayCI $
27− do
28− hsr <-
29− startHeal
30− Nothing
31− Nothing
32− HealOpts
33− { hoRecursive = True,
34− hoDryRun = False
35− }
36− getHealStatus Nothing Nothing (hsrClientToken hsr)
37− print res
examples/ListBuckets.hs+8−6
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,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
26−-- https://play.min.io. The endpoint and associated
27+-- https://play.minio.io:9000. 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+23−20
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,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
21−import Conduit
22−import Network.Minio
23−import Prelude
23+import Data.Conduit (($$))
24+import Data.Conduit.Combinators (sinkList)
25+import Prelude
2426
2527-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
28+-- https://play.minio.io:9000. 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 incomplete uploads under bucket "test"
3540 -- on a local minio server.
36− res <-
37− runMinio minioPlayCI $
38− runConduit $
39− listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
41+ res <- runMinio minioPlayCI $
42+ listIncompleteUploads bucket Nothing True $$ sinkList
4043 print res
4144
42−{-
43− Following is the output of the above program on a local MinIO server.
45+ {-
46+ Following is the output of the above program on a local Minio server.
4447
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−-}
48+ Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
49+ , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
50+ , uiInitTime = 2017-03-01 10:16:25.698 UTC
51+ , uiSize = 17731794
52+ }
53+ ]
54+ -}
examples/ListObjects.hs+20−16
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,31 +16,35 @@
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 Data.Conduit as C
24+import qualified Data.Conduit.Combinators as CC
25+import Prelude
2026
21−import Conduit
22−import Network.Minio
23−import Prelude
2427
2528-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
29+-- https://play.minio.io:9000. The endpoint and associated
2730-- credentials are provided via the libary constant,
2831--
2932-- > minioPlayCI :: ConnectInfo
33+--
34+
3035main :: IO ()
3136main = do
32− let bucket = "test"
37+ let
38+ bucket = "test"
3339
3440 -- Performs a recursive listing of all objects under bucket "test"
35− -- on play.min.io.
36− res <-
37− runMinio minioPlayCI $
38− runConduit $
39− listObjects bucket Nothing True .| mapM_C (liftIO . print)
41+ -- on play.minio.io.
42+ res <- runMinio minioPlayCI $
43+ listObjects bucket Nothing True C.$$ CC.sinkList
4044 print res
4145
42−{-
43− Following is the output of the above program on a local MinIO server.
46+ {-
47+ Following is the output of the above program on a local Minio server.
4448
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−-}
49+ Right [ObjectInfo {oiObject = "ADVANCED.png", oiModTime = 2017-02-10 05:33:24.816 UTC, oiETag = "\"a69f3af6bbb06fe1d42ac910ec30482f\"", oiSize = 94026},ObjectInfo {oiObject = "obj", oiModTime = 2017-02-10 08:49:26.777 UTC, oiETag = "\"715a872a253a3596652c1490081b4b6a-1\"", oiSize = 15728640}]
50+ -}
examples/MakeBucket.hs → examples/Makebucket.hsrenamed+14−10
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,21 +16,25 @@
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
25−-- https://play.min.io. The endpoint and associated
27+-- https://play.minio.io:9000. 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"
32− res <-
33− runMinio minioPlayCI $
34− -- N B the region provided for makeBucket is optional.
35− makeBucket bucket (Just "us-east-1")
35+ let
36+ bucket = "my-bucket"
37+ res <- runMinio minioPlayCI $
38+ -- N B the region provided for makeBucket is optional.
39+ makeBucket bucket (Just "us-east-1")
3640 print res
examples/PresignedGetObject.hs+34−39
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,41 +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 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
29−-- https://play.min.io. The endpoint and associated
30+-- https://play.minio.io:9000. 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..."
48− putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
49− liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
50+ putObject bucket object (CC.repeat "a") (Just kb15)
51+ liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
5052
5153 -- Extract Etag of uploaded object
52− oi <- statObject bucket object defaultGetObjectOptions
53− let etag = oiETag oi
54+ (ObjectInfo _ _ etag _) <- statObject bucket object
5455
5556 -- Set header to add an if-match constraint - this makes sure
5657 -- the fetching fails if the object is changed on the server
@@ -59,29 +60,23 @@ main = do
5960 -- Generate a URL with 7 days expiry time - note that the headers
6061 -- used above must be added to the request with the signed URL
6162 -- generated.
62− url <-
63− presignedGetObjectUrl
64− "my-bucket"
65− "my-object"
66− (7 * 24 * 3600)
67− queryParam
68− headers
63+ url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)
64+ queryParam headers
6965
7066 return (headers, etag, url)
7167
7268 case res of
7369 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
74− Right (headers, _, url) -> do
70+ Right (headers, etag, url) -> do
71+
7572 -- We generate a curl command to demonstrate usage of the signed
7673 -- 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, "'"]]
74+ let
75+ hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
76+ curlCmd = B.intercalate " " $
77+ ["curl --fail"] ++ map hdrOpt headers ++
78+ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
8379
84− putStrLn $
85− "The following curl command would use the presigned "
86− ++ "URL to fetch the object and write it to \"/tmp/myfile\":"
80+ putStrLn $ "The following curl command would use the presigned " ++
81+ "URL to fetch the object and write it to \"/tmp/myfile\":"
8782 B.putStrLn curlCmd
examples/PresignedPostPolicy.hs+42−45
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,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.Map.Strict as Map
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
29−-- https://play.min.io. The endpoint and associated
30+-- https://play.minio.io:9000. 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 = "my-object"
42+
43+ -- set an expiration time of 10 days
44+ expireTime = Time.addUTCTime (3600 * 24 * 10) now
45+
46+ -- create a policy with expiration time and conditions - since the
47+ -- conditions are validated, newPostPolicy returns an Either value
48+ policyE = newPostPolicy expireTime
49+ [ -- set the object name condition
50+ ppCondKey "photos/my-object"
51+ -- set the bucket name condition
52+ , ppCondBucket "my-bucket"
53+ -- set the size range of object as 1B to 10MiB
54+ , ppCondContentLengthRange 1 (10*1024*1024)
55+ -- set content type as jpg image
56+ , ppCondContentType "image/jpeg"
57+ -- on success set the server response code to 200
58+ , ppCondSuccessActionStatus 200
59+ ]
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 $ Map.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+24−23
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,43 +16,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
26−-- https://play.min.io. The endpoint and associated
27+-- https://play.minio.io:9000. 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+18−16
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,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
2123import qualified Data.Conduit.Combinators as CC
22−import Network.Minio
23−import Prelude
2424
2525-- | The following example uses minio's play server at
26−-- https://play.min.io. The endpoint and associated
26+-- https://play.minio.io:9000. The endpoint and associated
2727-- credentials are provided via the libary constant,
2828--
2929-- > minioPlayCI :: ConnectInfo
30+--
31+
3032main :: IO ()
3133main = do
32− let bucket = "test"
34+ let
35+ bucket = "test"
3336 object = "obj"
3437 localFile = "/etc/lsb-release"
3538 kb15 = 15 * 1024
3639
37− -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
38− res1 <-
39− runMinio minioPlayCI $
40− putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
40+ -- Eg 1. Upload a stream of repeating "a" using putObject.
41+ res1 <- runMinio minioPlayCI $
42+ putObject bucket object (CC.repeat "a") (Just kb15)
4143 case res1 of
42− Left e -> putStrLn $ "putObject failed." ++ show e
44+ Left e -> putStrLn $ "putObject failed." ++ show e
4345 Right () -> putStrLn "putObject succeeded."
4446
45− -- Eg 2. Upload a file using fPutObject with default options.
46− res2 <-
47− runMinio minioPlayCI $
48− fPutObject bucket object localFile defaultPutObjectOptions
47+
48+ -- Eg 2. Upload a file using fPutObject.
49+ res2 <- runMinio minioPlayCI $
50+ fPutObject bucket object localFile
4951 case res2 of
50− Left e -> putStrLn $ "fPutObject failed." ++ show e
52+ Left e -> putStrLn $ "fPutObject failed." ++ show e
5153 Right () -> putStrLn "fPutObject succeeded."
examples/README.mddeleted−17
@@ -1,17 +0,0 @@
1−# Examples
2−
3−The examples in this directory illustrate usage of various APIs provided by this library. Each file is self-contained and can be run like a script directly.
4−
5−To build the examples, the build flag `examples` needs to be turned on:
6−
7−```sh
8−stack build --flag minio-hs:examples
9−```
10−
11−Now to run and example script [BucketExists.hs](https://github.com/minio/minio-hs/blob/master/examples/BucketExists.hs):
12−
13−```sh
14−stack exec BucketExists
15−```
16−
17−The CI system is configured to build these examples with every change, so they should be current.
examples/RemoveIncompleteUpload.hs+13−10
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,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
25−-- https://play.min.io. The endpoint and associated
26+-- https://play.minio.io:9000. 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+10−9
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,19 +16,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/RemoveBucket.hs → examples/Removebucket.hsrenamed+11−6
@@ -1,8 +1,8 @@
11#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
2+-- stack --resolver lts-9.1 runghc --package minio-hs
33
44--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
5+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
66--
77-- Licensed under the Apache License, Version 2.0 (the "License");
88-- you may not use this file except in compliance with the License.
@@ -16,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
25−-- https://play.min.io. The endpoint and associated
27+-- https://play.minio.io:9000. 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/SelectObject.hsdeleted−47
@@ -1,47 +0,0 @@
1−#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
3−
4−--
5−-- MinIO Haskell SDK, (C) 2019 MinIO, Inc.
6−--
7−-- Licensed under the Apache License, Version 2.0 (the "License");
8−-- you may not use this file except in compliance with the License.
9−-- You may obtain a copy of the License at
10−--
11−-- http://www.apache.org/licenses/LICENSE-2.0
12−--
13−-- Unless required by applicable law or agreed to in writing, software
14−-- distributed under the License is distributed on an "AS IS" BASIS,
15−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16−-- See the License for the specific language governing permissions and
17−-- limitations under the License.
18−--
19−{-# LANGUAGE OverloadedStrings #-}
20−
21−import qualified Conduit as C
22−import Control.Monad (unless)
23−import Network.Minio
24−import Prelude
25−
26−main :: IO ()
27−main = do
28− let bucket = "selectbucket"
29− object = "1.csv"
30− content =
31− "Name,Place,Temperature\n"
32− <> "James,San Jose,76\n"
33− <> "Alicia,San Leandro,88\n"
34− <> "Mark,San Carlos,90\n"
35−
36− res <- runMinio minioPlayCI $ do
37− exists <- bucketExists bucket
38− unless exists $
39− makeBucket bucket Nothing
40−
41− C.liftIO $ putStrLn "Uploading csv object"
42− putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
43−
44− let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
45− res <- selectObjectContent bucket object sr
46− C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
47− print res
examples/ServerInfo.hsdeleted−30
@@ -1,30 +0,0 @@
1−#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
3−
4−--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6−--
7−-- Licensed under the Apache License, Version 2.0 (the "License");
8−-- you may not use this file except in compliance with the License.
9−-- You may obtain a copy of the License at
10−--
11−-- http://www.apache.org/licenses/LICENSE-2.0
12−--
13−-- Unless required by applicable law or agreed to in writing, software
14−-- distributed under the License is distributed on an "AS IS" BASIS,
15−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16−-- See the License for the specific language governing permissions and
17−-- limitations under the License.
18−--
19−
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
23−
24−main :: IO ()
25−main = do
26− res <-
27− runMinio
28− minioPlayCI
29− getServerInfo
30− print res
examples/ServiceSendRestart.hsdeleted−29
@@ -1,29 +0,0 @@
1−#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
3−
4−--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6−--
7−-- Licensed under the Apache License, Version 2.0 (the "License");
8−-- you may not use this file except in compliance with the License.
9−-- You may obtain a copy of the License at
10−--
11−-- http://www.apache.org/licenses/LICENSE-2.0
12−--
13−-- Unless required by applicable law or agreed to in writing, software
14−-- distributed under the License is distributed on an "AS IS" BASIS,
15−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16−-- See the License for the specific language governing permissions and
17−-- limitations under the License.
18−--
19−
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
23−
24−main :: IO ()
25−main = do
26− res <-
27− runMinio minioPlayCI $
28− serviceSendAction ServiceActionRestart
29− print res
examples/ServiceSendStop.hsdeleted−29
@@ -1,29 +0,0 @@
1−#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
3−
4−--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6−--
7−-- Licensed under the Apache License, Version 2.0 (the "License");
8−-- you may not use this file except in compliance with the License.
9−-- You may obtain a copy of the License at
10−--
11−-- http://www.apache.org/licenses/LICENSE-2.0
12−--
13−-- Unless required by applicable law or agreed to in writing, software
14−-- distributed under the License is distributed on an "AS IS" BASIS,
15−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16−-- See the License for the specific language governing permissions and
17−-- limitations under the License.
18−--
19−
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
23−
24−main :: IO ()
25−main = do
26− res <-
27− runMinio minioPlayCI $
28− serviceSendAction ServiceActionStop
29− print res
examples/ServiceStatus.hsdeleted−30
@@ -1,30 +0,0 @@
1−#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
3−
4−--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6−--
7−-- Licensed under the Apache License, Version 2.0 (the "License");
8−-- you may not use this file except in compliance with the License.
9−-- You may obtain a copy of the License at
10−--
11−-- http://www.apache.org/licenses/LICENSE-2.0
12−--
13−-- Unless required by applicable law or agreed to in writing, software
14−-- distributed under the License is distributed on an "AS IS" BASIS,
15−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16−-- See the License for the specific language governing permissions and
17−-- limitations under the License.
18−--
19−
20−import Network.Minio
21−import Network.Minio.AdminAPI
22−import Prelude
23−
24−main :: IO ()
25−main = do
26− res <-
27− runMinio
28− minioPlayCI
29− serviceStatus
30− print res
examples/SetConfig.hsdeleted−31
@@ -1,31 +0,0 @@
1−#!/usr/bin/env stack
2−-- stack --resolver lts-14.11 runghc --package minio-hs
3−
4−--
5−-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6−--
7−-- Licensed under the Apache License, Version 2.0 (the "License");
8−-- you may not use this file except in compliance with the License.
9−-- You may obtain a copy of the License at
10−--
11−-- http://www.apache.org/licenses/LICENSE-2.0
12−--
13−-- Unless required by applicable law or agreed to in writing, software
14−-- distributed under the License is distributed on an "AS IS" BASIS,
15−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16−-- See the License for the specific language governing permissions and
17−-- limitations under the License.
18−--
19−{-# LANGUAGE OverloadedStrings #-}
20−
21−import Network.Minio
22−import Network.Minio.AdminAPI
23−import Prelude
24−
25−main :: IO ()
26−main = do
27− res <- runMinio minioPlayCI $
28− do
29− let config = "{\"version\":\"25\",\"credential\":{\"accessKey\":\"minio\",\"secretKey\":\"minio123\"},\"region\":\"\",\"browser\":\"on\",\"worm\":\"off\",\"domain\":\"\",\"storageclass\":{\"standard\":\"\",\"rrs\":\"\"},\"cache\":{\"drives\":[],\"expiry\":90,\"exclude\":[]},\"notify\":{\"amqp\":{\"2\":{\"enable\":false,\"url\":\"amqp://guest:guest@localhost:5672/\",\"exchange\":\"minio\",\"routingKey\":\"minio\",\"exchangeType\":\"direct\",\"deliveryMode\":0,\"mandatory\":false,\"immediate\":false,\"durable\":false,\"internal\":false,\"noWait\":false,\"autoDeleted\":false}},\"elasticsearch\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"url\":\"http://localhost:9200\",\"index\":\"minio_events\"}},\"kafka\":{\"1\":{\"enable\":false,\"brokers\":null,\"topic\":\"\"}},\"mqtt\":{\"1\":{\"enable\":false,\"broker\":\"\",\"topic\":\"\",\"qos\":0,\"clientId\":\"\",\"username\":\"\",\"password\":\"\",\"reconnectInterval\":0,\"keepAliveInterval\":0}},\"mysql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"dsnString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"nats\":{\"1\":{\"enable\":false,\"address\":\"\",\"subject\":\"\",\"username\":\"\",\"password\":\"\",\"token\":\"\",\"secure\":false,\"pingInterval\":0,\"streaming\":{\"enable\":false,\"clusterID\":\"\",\"clientID\":\"\",\"async\":false,\"maxPubAcksInflight\":0}}},\"postgresql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"connectionString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"redis\":{\"test1\":{\"enable\":true,\"format\":\"namespace\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_ns\"},\"test2\":{\"enable\":true,\"format\":\"access\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_log\"}},\"webhook\":{\"1\":{\"enable\":true,\"endpoint\":\"http://localhost:3000\"},\"2\":{\"enable\":true,\"endpoint\":\"http://localhost:3001\"}}}}"
30− setConfig config
31− print res
minio-hs.cabal+182−283
@@ -1,353 +1,252 @@
1−cabal-version: 2.4
21name: minio-hs
3−version: 1.7.0
4−synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
2+version: 0.3.2
3+synopsis: A Minio Haskell Library for Amazon S3 compatible cloud
54 storage.
6−description: The MinIO Haskell client library provides simple APIs to
7− access MinIO, Amazon S3 and other API compatible cloud
5+description: The Minio Haskell client library provides simple APIs to
6+ access Minio, Amazon S3 and other API compatible cloud
87 storage servers.
98homepage: https://github.com/minio/minio-hs#readme
109license: Apache-2.0
1110license-file: LICENSE
12−author: MinIO Dev Team
13−maintainer: dev@min.io
11+author: Aditya Manthramurthy, Krishnan Parthasarathi
12+maintainer: dev@minio.io
1413category: Network, AWS, Object Storage
1514build-type: Simple
1615stability: Experimental
17−extra-doc-files:
16+extra-source-files:
1817 CHANGELOG.md
1918 CONTRIBUTING.md
2019 docs/API.md
21− README.md
22−extra-source-files:
2320 examples/*.hs
21+ README.md
2422 stack.yaml
25−tested-with: GHC == 8.10.7
26− , GHC == 9.0.2
27− , GHC == 9.2.8
28− , GHC == 9.4.8
29− , GHC == 9.6.5
30− , GHC == 9.8.2
31−
32−source-repository head
33− type: git
34− location: https://github.com/minio/minio-hs.git
3523
36−Flag dev
37− Description: Build package in development mode
38− Default: False
39− Manual: True
24+cabal-version: >=1.10
4025
41−common base-settings
26+library
27+ hs-source-dirs: src
4228 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−
67− default-language: Haskell2010
68−
69− default-extensions: BangPatterns
70− , DerivingStrategies
71− , FlexibleContexts
72− , FlexibleInstances
73− , LambdaCase
74− , MultiParamTypeClasses
75− , MultiWayIf
76− , OverloadedStrings
77− , RankNTypes
78− , ScopedTypeVariables
79− , TupleSections
80−
29+ exposed-modules: Network.Minio
30+ , Network.Minio.S3API
8131 other-modules: Lib.Prelude
8232 , Network.Minio.API
83− , Network.Minio.APICommon
8433 , Network.Minio.Data
8534 , Network.Minio.Data.ByteString
8635 , Network.Minio.Data.Crypto
8736 , Network.Minio.Data.Time
88− , Network.Minio.CopyObject
8937 , Network.Minio.Errors
9038 , Network.Minio.ListOps
9139 , Network.Minio.PresignedOperations
9240 , Network.Minio.PutObject
93− , Network.Minio.SelectAPI
9441 , Network.Minio.Sign.V4
9542 , Network.Minio.Utils
9643 , Network.Minio.XmlGenerator
9744 , Network.Minio.XmlParser
98− , Network.Minio.XmlCommon
99− , Network.Minio.JsonParser
100− , Network.Minio.Credentials.Types
101− , Network.Minio.Credentials.AssumeRole
102− , Network.Minio.Credentials
103−
104− mixins: base hiding (Prelude)
105− , relude (Relude as Prelude)
106− , relude
107−
10845 build-depends: base >= 4.7 && < 5
109− , relude >= 0.7 && < 2
110− , aeson >= 1.2 && < 3
111− , base64-bytestring >= 1.0
112− , binary >= 0.8.5.0
113− , bytestring >= 0.10
114− , case-insensitive >= 1.2
115− , conduit >= 1.3
116− , conduit-extra >= 1.3
117− , crypton-connection
118− , cryptonite >= 0.25
119− , cryptonite-conduit >= 0.2
120− , data-default-class
121− , digest >= 0.0.1
122− , directory
123− , filepath >= 1.4
124− , http-client >= 0.5
125− , http-client-tls
126− , http-conduit >= 2.3
127− , http-types >= 0.12
128− , ini
129− , memory >= 0.14
130− , network-uri
131− , resourcet >= 1.2
132− , retry
133− , text >= 1.2
134− , time >= 1.9
135− , time-units ^>= 1.0.0
136− , transformers >= 0.5
137− , unliftio >= 0.2 && < 0.3
138− , unliftio-core >= 0.2 && < 0.3
139− , unordered-containers >= 0.2
140− , xml-conduit >= 1.8
141−
142−library
143− import: base-settings
144− hs-source-dirs: src
145− exposed-modules: Network.Minio
146− , Network.Minio.AdminAPI
147− , Network.Minio.S3API
46+ , protolude >= 0.1.6
47+ , aeson
48+ , async
49+ , base64-bytestring
50+ , bytestring
51+ , case-insensitive
52+ , conduit
53+ , conduit-combinators
54+ , conduit-extra
55+ , containers
56+ , cryptonite
57+ , cryptonite-conduit
58+ , data-default
59+ , exceptions
60+ , filepath
61+ , http-client
62+ , http-conduit
63+ , http-types
64+ , lifted-async
65+ , lifted-base
66+ , memory
67+ , monad-control
68+ , resourcet
69+ , text
70+ , text-format
71+ , time
72+ , transformers
73+ , transformers-base
74+ , vector
75+ , xml-conduit
76+ default-language: Haskell2010
77+ default-extensions: FlexibleContexts
78+ , FlexibleInstances
79+ , BangPatterns
80+ , MultiParamTypeClasses
81+ , MultiWayIf
82+ , NoImplicitPrelude
83+ , OverloadedStrings
84+ , RankNTypes
85+ , ScopedTypeVariables
86+ , TypeFamilies
87+ , TupleSections
14888
14989Flag live-test
150− Description: Build the test suite that runs against a live MinIO server
151− Default: False
90+ Default: True
15291 Manual: True
15392
15493test-suite minio-hs-live-server-test
155− import: base-settings
15694 type: exitcode-stdio-1.0
15795 hs-source-dirs: test, src
15896 main-is: LiveServer.hs
15997 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
160− other-modules: Network.Minio
98+ default-language: Haskell2010
99+ default-extensions: BangPatterns
100+ , FlexibleContexts
101+ , FlexibleInstances
102+ , OverloadedStrings
103+ , NoImplicitPrelude
104+ , MultiParamTypeClasses
105+ , MultiWayIf
106+ , ScopedTypeVariables
107+ , RankNTypes
108+ , TupleSections
109+ , TypeFamilies
110+ other-modules: Lib.Prelude
111+ , Network.Minio
112+ , Network.Minio.API
113+ , Network.Minio.Data
114+ , Network.Minio.Data.ByteString
115+ , Network.Minio.Data.Crypto
116+ , Network.Minio.Data.Time
117+ , Network.Minio.Errors
118+ , Network.Minio.ListOps
119+ , Network.Minio.PresignedOperations
120+ , Network.Minio.PutObject
161121 , Network.Minio.S3API
162− , Network.Minio.AdminAPI
163− , Network.Minio.API.Test
164− , Network.Minio.JsonParser.Test
165− , Network.Minio.TestHelpers
122+ , Network.Minio.Sign.V4
123+ , Network.Minio.Utils
166124 , Network.Minio.Utils.Test
125+ , Network.Minio.API.Test
126+ , Network.Minio.XmlGenerator
167127 , Network.Minio.XmlGenerator.Test
128+ , Network.Minio.XmlParser
168129 , Network.Minio.XmlParser.Test
169− , Network.Minio.Credentials
170− build-depends: minio-hs
171− , raw-strings-qq
130+ build-depends: base
131+ , minio-hs
132+ , protolude >= 0.1.6
133+ , aeson
134+ , async
135+ , base64-bytestring
136+ , bytestring
137+ , case-insensitive
138+ , conduit
139+ , conduit-combinators
140+ , conduit-extra
141+ , containers
142+ , cryptonite
143+ , cryptonite-conduit
144+ , data-default
145+ , directory
146+ , exceptions
147+ , filepath
148+ , http-client
149+ , http-conduit
150+ , http-types
151+ , lifted-async
152+ , lifted-base
153+ , memory
154+ , monad-control
155+ , QuickCheck
156+ , resourcet
172157 , tasty
173158 , tasty-hunit
174159 , tasty-quickcheck
175160 , tasty-smallcheck
176− , QuickCheck
161+ , temporary
162+ , text
163+ , text-format
164+ , time
165+ , transformers
166+ , transformers-base
167+ , vector
168+ , xml-conduit
177169 if !flag(live-test)
178170 buildable: False
179171
180172test-suite minio-hs-test
181− import: base-settings
182173 type: exitcode-stdio-1.0
183174 hs-source-dirs: test, src
184175 main-is: Spec.hs
185− build-depends: minio-hs
186− , raw-strings-qq
176+ build-depends: base
177+ , minio-hs
178+ , protolude >= 0.1.6
179+ , aeson
180+ , async
181+ , base64-bytestring
182+ , bytestring
183+ , case-insensitive
184+ , conduit
185+ , conduit-combinators
186+ , conduit-extra
187+ , containers
188+ , cryptonite
189+ , cryptonite-conduit
190+ , data-default
191+ , directory
192+ , exceptions
193+ , filepath
194+ , http-client
195+ , http-conduit
196+ , http-types
197+ , lifted-async
198+ , lifted-base
199+ , memory
200+ , monad-control
187201 , QuickCheck
202+ , resourcet
188203 , tasty
189204 , tasty-hunit
190205 , tasty-quickcheck
191206 , tasty-smallcheck
207+ , temporary
208+ , text
209+ , text-format
210+ , time
211+ , transformers
212+ , transformers-base
213+ , vector
214+ , xml-conduit
192215 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
216+ default-language: Haskell2010
217+ default-extensions: BangPatterns
218+ , FlexibleContexts
219+ , FlexibleInstances
220+ , OverloadedStrings
221+ , NoImplicitPrelude
222+ , MultiParamTypeClasses
223+ , MultiWayIf
224+ , ScopedTypeVariables
225+ , RankNTypes
226+ , TupleSections
227+ , TypeFamilies
193228 other-modules: Lib.Prelude
194229 , Network.Minio
230+ , Network.Minio.API
231+ , Network.Minio.Data
232+ , Network.Minio.Data.ByteString
233+ , Network.Minio.Data.Crypto
234+ , Network.Minio.Data.Time
235+ , Network.Minio.Errors
236+ , Network.Minio.ListOps
237+ , Network.Minio.PresignedOperations
238+ , Network.Minio.PutObject
195239 , Network.Minio.S3API
196− , Network.Minio.AdminAPI
197− , Network.Minio.TestHelpers
198− , Network.Minio.API.Test
199− , Network.Minio.JsonParser.Test
240+ , Network.Minio.Sign.V4
241+ , Network.Minio.Utils
200242 , Network.Minio.Utils.Test
243+ , Network.Minio.API.Test
244+ , Network.Minio.XmlGenerator
201245 , Network.Minio.XmlGenerator.Test
246+ , Network.Minio.XmlParser
202247 , Network.Minio.XmlParser.Test
203− , Network.Minio.Credentials
204248
205−Flag examples
206− Description: Build the examples
207− Default: False
208− Manual: True
209−
210−common examples-settings
211− ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
212− default-language: Haskell2010
213− default-extensions: OverloadedStrings
214− build-depends: base >= 4.7 && < 5
215− , bytestring
216− , case-insensitive
217− , conduit
218− , conduit-extra
219− , filepath
220− , minio-hs
221− , optparse-applicative
222− , text
223− , time
224− , unliftio
225− , unordered-containers
226− hs-source-dirs: examples
227− if !flag(examples)
228− buildable: False
229−
230−executable BucketExists
231− import: examples-settings
232− scope: private
233− main-is: BucketExists.hs
234−
235−executable CopyObject
236− import: examples-settings
237− scope: private
238− main-is: CopyObject.hs
239−
240−executable FileUploader
241− import: examples-settings
242− scope: private
243− main-is: FileUploader.hs
244−
245−executable GetConfig
246− import: examples-settings
247− scope: private
248− main-is: GetConfig.hs
249−
250−executable GetObject
251− import: examples-settings
252− scope: private
253− main-is: GetObject.hs
254−
255−executable HeadObject
256− import: examples-settings
257− scope: private
258− main-is: HeadObject.hs
259249
260−executable Heal
261− import: examples-settings
262− scope: private
263− main-is: Heal.hs
264−
265−executable ListBuckets
266− import: examples-settings
267− scope: private
268− main-is: ListBuckets.hs
269−
270−executable ListIncompleteUploads
271− import: examples-settings
272− scope: private
273− main-is: ListIncompleteUploads.hs
274−
275−executable ListObjects
276− import: examples-settings
277− scope: private
278− main-is: ListObjects.hs
279−
280−executable MakeBucket
281− import: examples-settings
282− scope: private
283− main-is: MakeBucket.hs
284−
285−executable PresignedGetObject
286− import: examples-settings
287− scope: private
288− main-is: PresignedGetObject.hs
289−
290−executable PresignedPostPolicy
291− import: examples-settings
292− scope: private
293− main-is: PresignedPostPolicy.hs
294−
295−executable PresignedPutObject
296− import: examples-settings
297− scope: private
298− main-is: PresignedPutObject.hs
299−
300−executable PutObject
301− import: examples-settings
302− scope: private
303− main-is: PutObject.hs
304−
305−executable RemoveBucket
306− import: examples-settings
307− scope: private
308− main-is: RemoveBucket.hs
309−
310−executable RemoveIncompleteUpload
311− import: examples-settings
312− scope: private
313− main-is: RemoveIncompleteUpload.hs
314−
315−executable RemoveObject
316− import: examples-settings
317− scope: private
318− main-is: RemoveObject.hs
319−
320−executable SelectObject
321− import: examples-settings
322− scope: private
323− main-is: SelectObject.hs
324−
325−executable ServerInfo
326− import: examples-settings
327− scope: private
328− main-is: ServerInfo.hs
329−
330−executable ServiceSendRestart
331− import: examples-settings
332− scope: private
333− main-is: ServiceSendRestart.hs
334−
335−executable ServiceSendStop
336− import: examples-settings
337− scope: private
338− main-is: ServiceSendStop.hs
339−
340−executable ServiceStatus
341− import: examples-settings
342− scope: private
343− main-is: ServiceStatus.hs
344−
345−executable SetConfig
346− import: examples-settings
347− scope: private
348− main-is: SetConfig.hs
349−
350−executable AssumeRole
351− import: examples-settings
352− scope: private
353− main-is: AssumeRole.hs
250+source-repository head
251+ type: git
252+ location: https://github.com/minio/minio-hs
src/Lib/Prelude.hs+25−34
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,41 +15,32 @@
1515--
1616
1717module Lib.Prelude
18− ( module Exports,
19− both,
20− showBS,
21− toStrictBS,
22− fromStrictBS,
23− lastMay,
24− )
25−where
26−
27−import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
28−import qualified Data.ByteString.Lazy as LB
29−import Data.Time as Exports
30− ( UTCTime (..),
31− diffUTCTime,
32− )
33−import UnliftIO as Exports
34− ( Handler,
35− catch,
36− catches,
37− throwIO,
38− try,
39− )
18+ ( module Exports
19+ , both
4020
41−-- | Apply a function on both elements of a pair
42−both :: (a -> b) -> (a, a) -> (b, b)
43−both f (a, b) = (f a, f b)
21+ , format
22+ , formatBS
23+ ) where
24+
25+import Protolude as Exports
26+
27+import Data.Time as Exports (UTCTime(..), diffUTCTime)
28+import Control.Monad.Trans.Maybe as Exports (runMaybeT, MaybeT(..))
4429
45−showBS :: (Show a) => a -> ByteString
46−showBS a = encodeUtf8 (show a :: Text)
30+import Control.Monad.Catch as Exports (throwM, MonadThrow, MonadCatch)
4731
48−toStrictBS :: LByteString -> ByteString
49−toStrictBS = LB.toStrict
32+import Data.Text.Format as Exports (Shown(..))
33+import qualified Data.Text.Format as TF
34+import Data.Text.Format.Params (Params)
5035
51−fromStrictBS :: ByteString -> LByteString
52−fromStrictBS = LB.fromStrict
36+format :: Params ps => TF.Format -> ps -> Text
37+format f args = toS $ TF.format f args
5338
54−lastMay :: [a] -> Maybe a
55−lastMay a = last <$> nonEmpty a
39+formatBS :: Params ps => TF.Format -> ps -> ByteString
40+formatBS f args = toS $ TF.format f args
41+
42+-- import Data.Tuple as Exports (uncurry)
43+
44+-- | Apply a function on both elements of a pair
45+both :: (a -> b) -> (a, a) -> (b, b)
46+both f (a, b) = (f a, f b)
src/Network/Minio.hs+160−280
@@ -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.
@@ -14,239 +14,143 @@
1414-- limitations under the License.
1515--
1616
17−-- |
18−-- Module: Network.Minio
19−-- Copyright: (c) 2017-2023 MinIO Dev Team
20−-- License: Apache 2.0
21−-- Maintainer: MinIO Dev Team <dev@min.io>
22−--
23−-- Types and functions to conveniently access S3 compatible object
24−-- storage servers like MinIO.
17+
18+
2519module 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
20+ (
21+
22+ -- * Connecting to object storage
23+ ---------------------------------
24+ ConnectInfo(..)
25+ , awsCI
26+
27+ -- ** Connection helpers
28+ ------------------------
29+ , awsWithRegionCI
30+ , minioPlayCI
31+ , minioCI
32+
33+ -- * Minio Monad
34+ ----------------
35+ -- | The Minio monad provides connection-reuse, bucket-location
36+ -- caching, resource management and simpler error handling
37+ -- functionality. All actions on object storage are performed within
38+ -- this Monad.
39+
40+ , Minio
41+ , runMinio
42+ , def
43+
44+ -- * Bucket Operations
45+ ----------------------
46+
47+ -- ** Creation, removal and querying
48+ , Bucket
49+ , makeBucket
50+ , removeBucket
51+ , bucketExists
52+ , Region
53+ , getLocation
54+
55+ -- ** Listing
56+ , BucketInfo(..)
57+ , listBuckets
58+ , ObjectInfo(..)
59+ , listObjects
60+ , listObjectsV1
61+ , UploadId
62+ , UploadInfo(..)
63+ , listIncompleteUploads
64+ , ObjectPartInfo(..)
65+ , listIncompleteParts
66+
67+ -- ** Notifications
68+ , Notification(..)
69+ , NotificationConfig(..)
70+ , Arn
71+ , Event(..)
72+ , Filter(..)
73+ , FilterKey(..)
74+ , FilterRules(..)
75+ , FilterRule(..)
76+ , getBucketNotification
77+ , putBucketNotification
78+ , removeAllBucketNotification
79+
80+ -- * Object Operations
81+ ----------------------
82+
83+ , Object
84+
85+ -- ** File operations
86+ , fGetObject
87+ , fPutObject
88+
89+ -- ** Conduit-based streaming operations
90+ , putObject
91+ , getObject
92+
93+ -- ** Server-side copying
94+ , CopyPartSource(..)
95+ , copyObject
96+
97+ -- ** Querying
98+ , statObject
99+
100+ -- ** Object removal functions
101+ , removeObject
102+ , removeIncompleteUpload
103+
104+ -- * Presigned Operations
105+ -------------------------
106+ , UrlExpiry
107+ , presignedPutObjectUrl
108+ , presignedGetObjectUrl
109+ , presignedHeadObjectUrl
110+
111+ -- ** Utilities for POST (browser) uploads
112+ , PostPolicy
113+ , PostPolicyError(..)
114+ , newPostPolicy
115+ , presignedPostPolicy
116+ , showPostPolicy
117+
118+ -- *** Utilities to specify Post Policy conditions
119+ , PostPolicyCondition
120+ , ppCondBucket
121+ , ppCondContentLengthRange
122+ , ppCondContentType
123+ , ppCondKey
124+ , ppCondKeyStartsWith
125+ , ppCondSuccessActionStatus
126+
127+ -- * Error handling
128+ -----------------------
129+ -- | Data types representing various errors that may occur while working
130+ -- with an object storage service.
131+ , MinioErr(..)
132+ , MErrV(..)
133+ , ServiceErr(..)
134+
135+) where
233136
234137{-
235−This module exports the high-level MinIO API for object storage.
138+This module exports the high-level Minio API for object storage.
236139-}
237140
238−import qualified Data.Conduit as C
239−import qualified Data.Conduit.Binary as CB
141+import qualified Data.Conduit as C
142+import qualified Data.Conduit.Binary as CB
240143import 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
144+import Data.Default (def)
145+import qualified Data.Map as Map
146+
147+import Lib.Prelude
148+
149+import Network.Minio.Data
150+import Network.Minio.Errors
151+import Network.Minio.ListOps
152+import Network.Minio.PutObject
153+import Network.Minio.S3API
250154
251155-- | Lists buckets.
252156listBuckets :: Minio [BucketInfo]
@@ -255,65 +159,43 @@ listBuckets = getService
255159-- | Fetch the object and write it to the given file safely. The
256160-- object is first written to a temporary file in the same directory
257161-- and then moved to the given path.
258−fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
259−fGetObject bucket object fp opts = do
260− src <- getObject bucket object opts
261− C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
162+fGetObject :: Bucket -> Object -> FilePath -> Minio ()
163+fGetObject bucket object fp = do
164+ src <- getObject bucket object
165+ src C.$$+- CB.sinkFileCautious fp
262166
263167-- | Upload the given file to the given object.
264−fPutObject ::
265− Bucket ->
266− Object ->
267− FilePath ->
268− PutObjectOptions ->
269− Minio ()
270−fPutObject bucket object f opts =
271− void $ putObjectInternal bucket object opts $ ODFile f Nothing
168+fPutObject :: Bucket -> Object -> FilePath -> Minio ()
169+fPutObject bucket object f = void $ putObjectInternal bucket object $
170+ ODFile f Nothing
272171
273172-- | Put an object from a conduit source. The size can be provided if
274173-- known; this helps the library select optimal part sizes to perform
275174-- a multipart upload. If not specified, it is assumed that the object
276175-- 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 ()
284−putObject bucket object src sizeMay opts =
285− void $ putObjectInternal bucket object opts $ ODStream src sizeMay
286−
287−-- | Perform a server-side copy operation to create an object based on
288−-- the destination specification in DestinationInfo from the source
289−-- specification in SourceInfo. This function performs a multipart
290−-- copy operation if the new object is to be greater than 5GiB in
291−-- size.
292−copyObject :: DestinationInfo -> SourceInfo -> Minio ()
293−copyObject dstInfo srcInfo =
294− void $
295− copyObjectInternal
296− (dstBucket dstInfo)
297− (dstObject dstInfo)
298− srcInfo
176+putObject :: Bucket -> Object -> C.Producer Minio ByteString
177+ -> Maybe Int64 -> Minio ()
178+putObject bucket object src sizeMay =
179+ void $ putObjectInternal bucket object $ ODStream src sizeMay
180+
181+-- | Perform a server-side copy operation to create an object with the
182+-- given bucket and object name from the source specification in
183+-- CopyPartSource. This function performs a multipart copy operation
184+-- if the new object is to be greater than 5GiB in size.
185+copyObject :: Bucket -> Object -> CopyPartSource -> Minio ()
186+copyObject bucket object cps = void $ copyObjectInternal bucket object cps
299187
300188-- | Remove an object from the object store.
301189removeObject :: Bucket -> Object -> Minio ()
302190removeObject = deleteObject
303191
304−-- | Get an object from the object store.
305−getObject ::
306− Bucket ->
307− Object ->
308− GetObjectOptions ->
309− Minio GetObjectResponse
310−getObject bucket object opts =
311− getObject' bucket object [] $ gooToHeaders opts
192+-- | Get an object from the object store as a resumable source (conduit).
193+getObject :: Bucket -> Object -> Minio (C.ResumableSource Minio ByteString)
194+getObject bucket object = snd <$> getObject' bucket object [] []
312195
313−-- | Get an object's metadata from the object store. It accepts the
314−-- same options as GetObject.
315−statObject :: Bucket -> Object -> GetObjectOptions -> Minio ObjectInfo
316−statObject b o opts = headObject b o $ gooToHeaders opts
196+-- | Get an object's metadata from the object store.
197+statObject :: Bucket -> Object -> Minio ObjectInfo
198+statObject = headObject
317199
318200-- | Creates a new bucket in the object store. The Region can be
319201-- optionally specified. If not specified, it will use the region
@@ -323,23 +205,21 @@ makeBucket :: Bucket -> Maybe Region -> Minio ()
323205makeBucket bucket regionMay = do
324206 region <- maybe (asks $ connectRegion . mcConnInfo) return regionMay
325207 putBucket bucket region
326− addToRegionCache bucket region
208+ modify (Map.insert bucket region)
327209
328210-- | Removes a bucket from the object store.
329211removeBucket :: Bucket -> Minio ()
330212removeBucket bucket = do
331213 deleteBucket bucket
332− deleteFromRegionCache bucket
214+ modify (Map.delete bucket)
333215
334216-- | Query the object store if a given bucket is present.
335217bucketExists :: Bucket -> Minio Bool
336218bucketExists = headBucket
337219
220+
338221-- | Removes an ongoing multipart upload of an object.
339222removeIncompleteUpload :: Bucket -> Object -> Minio ()
340223removeIncompleteUpload bucket object = do
341− uploads <-
342− C.runConduit $
343− listIncompleteUploads bucket (Just object) False
344− C..| CC.sinkList
224+ uploads <- listIncompleteUploads bucket (Just object) False C.$$ CC.sinkList
345225 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs+140−284
@@ -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,340 +15,196 @@
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
18+ (
19+ connect
20+ , RequestInfo(..)
21+ , runMinio
22+ , executeRequest
23+ , mkStreamRequest
24+ , getLocation
25+
26+ , isValidBucketName
27+ , checkBucketNameValidity
28+ , isValidObjectName
29+ , checkObjectNameValidity
30+ ) where
31+
4032import qualified Data.Conduit as C
41−import qualified Data.HashMap.Strict as H
33+import Data.Conduit.Binary (sourceHandleRange)
34+import Data.Default (def)
35+import qualified Data.Map as Map
36+import qualified Data.Char as C
4237import 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)
38+import qualified Data.ByteString as B
39+
40+import Network.HTTP.Conduit (Response)
4841import qualified Network.HTTP.Conduit as NC
49−import Network.HTTP.Types (simpleQueryToQuery)
5042import qualified Network.HTTP.Types as HT
5143import 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
44+
45+import Lib.Prelude
46+
47+import Network.Minio.Data
48+import Network.Minio.Data.Crypto
49+import Network.Minio.Errors
50+import Network.Minio.Sign.V4
51+import Network.Minio.Utils
52+import Network.Minio.XmlParser
53+
54+sha256Header :: ByteString -> HT.Header
55+sha256Header = ("x-amz-content-sha256", )
56+
57+getPayloadSHA256Hash :: (MonadIO m) => Payload -> m ByteString
58+getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
59+getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
60+ sourceHandleRange h
61+ (return . fromIntegral $ off)
62+ (return . fromIntegral $ size)
63+
64+getRequestBody :: Payload -> NC.RequestBody
65+getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
66+getRequestBody (PayloadH h off size) =
67+ NC.requestBodySource (fromIntegral size) $
68+ sourceHandleRange h
69+ (return . fromIntegral $ off)
70+ (return . fromIntegral $ size)
71+
5972
6073-- | Fetch bucket location (region)
6174getLocation :: Bucket -> Minio Region
6275getLocation bucket = do
63− resp <-
64− executeRequest $
65− defaultS3ReqInfo
66− { riBucket = Just bucket,
67− riQueryParams = [("location", Nothing)],
68− riNeedsLocation = False
69− }
76+ resp <- executeRequest $ def {
77+ riBucket = Just bucket
78+ , riQueryParams = [("location", Nothing)]
79+ , riNeedsLocation = False
80+ }
7081 parseLocation $ NC.responseBody resp
7182
83+
7284-- | Looks for region in RegionMap and updates it using getLocation if
7385-- absent.
74−discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
86+discoverRegion :: RequestInfo -> Minio (Maybe Region)
7587discoverRegion ri = runMaybeT $ do
7688 bucket <- MaybeT $ return $ riBucket ri
77− regionMay <- lift $ lookupRegionCache bucket
78− maybe
79− ( do
80− l <- lift $ getLocation bucket
81− lift $ addToRegionCache bucket l
82− return l
83− )
84− return
85− regionMay
86−
87−-- | Returns the region to be used for the request.
88−getRegion :: S3ReqInfo -> Minio (Maybe Region)
89−getRegion ri = do
90− ci <- asks mcConnInfo
91−
92− -- getService/makeBucket/getLocation -- don't need location
93− if
94− | not $ riNeedsLocation ri ->
95− return $ Just $ connectRegion ci
96− -- if autodiscovery of location is disabled by user
97− | not $ connectAutoDiscoverRegion ci ->
98− return $ Just $ connectRegion ci
99− -- discover the region for the request
100− | otherwise -> discoverRegion ri
101−
102−getRegionHost :: Region -> Minio Text
103−getRegionHost r = do
104− ci <- asks mcConnInfo
89+ regionMay <- gets (Map.lookup bucket)
90+ maybe (do
91+ l <- lift $ getLocation bucket
92+ modify $ Map.insert bucket l
93+ return l
94+ ) return regionMay
10595
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
11396
114−-- | Computes the appropriate host, path and region for the request.
115−--
116−-- For AWS, always use virtual bucket style, unless bucket has periods. For
117−-- MinIO and other non-AWS, default to path style.
118−getHostPathRegion :: S3ReqInfo -> Minio (Text, ByteString, Maybe Region)
119−getHostPathRegion ri = do
120− ci <- asks mcConnInfo
121− regionMay <- getRegion ri
122− case riBucket ri of
123− Nothing ->
124− -- Implies a ListBuckets request.
125− return (connectHost ci, "/", regionMay)
126− Just bucket -> do
127− regionHost <- case regionMay of
128− Nothing -> return $ connectHost ci
129− Just "" -> return $ connectHost ci
130− Just r -> getRegionHost r
131− let pathStyle =
132− ( regionHost,
133− getS3Path (riBucket ri) (riObject ri),
134− regionMay
135− )
136− virtualStyle =
137− ( bucket <> "." <> regionHost,
138− encodeUtf8 $ "/" <> fromMaybe "" (riObject ri),
139− regionMay
140− )
141− ( if isAWSConnectInfo ci
142− then
143− return $
144− if bucketHasPeriods bucket
145− then pathStyle
146− else virtualStyle
147− else return pathStyle
148− )
149−
150−-- | requestSTSCredential requests temporary credentials using the Security Token
151−-- Service API. The returned credential will include a populated 'SessionToken'
152−-- and an 'ExpiryTime'.
153−requestSTSCredential :: (STSCredentialProvider p) => p -> IO (CredentialValue, ExpiryTime)
154−requestSTSCredential p = do
155− endpoint <- maybe (throwIO $ MErrValidation MErrVSTSEndpointNotFound) return $ getSTSEndpoint p
156− let endPt = NC.parseRequest_ $ toString endpoint
157− settings
158− | NC.secure endPt = NC.tlsManagerSettings
159− | otherwise = defaultManagerSettings
160−
161− mgr <- NC.newManager settings
162− liftIO $ retrieveSTSCredentials p ("", 0, False) mgr
163−
164−buildRequest :: S3ReqInfo -> Minio NC.Request
97+buildRequest :: RequestInfo -> Minio NC.Request
16598buildRequest ri = do
16699 maybe (return ()) checkBucketNameValidity $ riBucket ri
167100 maybe (return ()) checkObjectNameValidity $ riObject ri
168101
169102 ci <- asks mcConnInfo
170103
171− (host, path, regionMay) <- getHostPathRegion ri
172−
173− let ci' = ci {connectHost = host}
174− hostHeader = (hHost, getHostAddr ci')
175− ri' =
176− ri
177− { riHeaders = hostHeader : riHeaders ri,
178− riRegion = regionMay
179− }
180− -- Does not contain body and auth info.
181− baseRequest =
182− NC.defaultRequest
183− { NC.method = riMethod ri',
184− NC.secure = connectIsSecure ci',
185− NC.host = encodeUtf8 $ connectHost ci',
186− NC.port = connectPort ci',
187− NC.path = path,
188− NC.requestHeaders = riHeaders ri',
189− NC.queryString = HT.renderQuery False $ riQueryParams ri'
190− }
191−
192− timeStamp <- liftIO Time.getCurrentTime
193−
194− mgr <- asks mcConnManager
195− cv <- liftIO $ getCredential (connectCreds ci') (getEndpoint ci') mgr
196−
197− let sp =
198− SignParams
199− (coerce $ cvAccessKey cv)
200− (coerce $ cvSecretKey cv)
201− (coerce $ cvSessionToken cv)
202− ServiceS3
203− timeStamp
204− (riRegion ri')
205− (riPresignExpirySecs ri')
206− Nothing
207−
208− -- Cases to handle:
209− --
210− -- 0. Handle presign URL case.
211− --
212− -- 1. Connection is secure: use unsigned payload
213− --
214− -- 2. Insecure connection, streaming signature is enabled via use of
215− -- conduit payload: use streaming signature for request.
216− --
217− -- 3. Insecure connection, non-conduit payload: compute payload
218− -- sha256hash, buffer request in memory and perform request.
219−
220− if
221− | isJust (riPresignExpirySecs ri') ->
222− -- case 0 from above.
223− do
224− let signPairs = signV4QueryParams sp baseRequest
225− qpToAdd = simpleQueryToQuery signPairs
226− existingQueryParams = HT.parseQuery (NC.queryString baseRequest)
227− updatedQueryParams = existingQueryParams ++ qpToAdd
228− return $ NClient.setQueryString updatedQueryParams baseRequest
229− | isStreamingPayload (riPayload ri') && not (connectIsSecure ci') ->
230− -- case 2 from above.
231− do
232− (pLen, pSrc) <- case riPayload ri of
233− PayloadC l src -> return (l, src)
234− _ -> throwIO MErrVUnexpectedPayload
235− let reqFn = signV4Stream pLen sp baseRequest
236− return $ reqFn pSrc
237− | otherwise ->
238− do
239− sp' <-
240− ( if connectIsSecure ci'
241− then -- case 1 described above.
242− return sp
243− else
244− ( -- case 3 described above.
245− do
246− pHash <- getPayloadSHA256Hash $ riPayload ri'
247− return $ sp {spPayloadHash = Just pHash}
248− )
249− )
250−
251− let signHeaders = signV4 sp' baseRequest
252− return $
253− baseRequest
254− { NC.requestHeaders =
255− NC.requestHeaders baseRequest ++ signHeaders,
256− NC.requestBody = getRequestBody (riPayload ri')
257− }
258−
259−retryAPIRequest :: Minio a -> Minio a
260−retryAPIRequest apiCall = do
261− resE <-
262− retrying retryPolicy (const shouldRetry) $
263− const $
264− try apiCall
265− either throwIO return resE
266− where
267− -- Retry using the full-jitter backoff method for up to 10 mins
268− -- total
269− retryPolicy =
270− limitRetriesByCumulativeDelay tenMins $
271− fullJitterBackoff oneMilliSecond
272− oneMilliSecond = 1000 -- in microseconds
273− tenMins = 10 * 60 * 1000000 -- in microseconds
274− -- retry on connection related failure
275− shouldRetry :: Either NC.HttpException a -> Minio Bool
276− shouldRetry resE =
277− case resE of
278− -- API request returned successfully
279− Right _ -> return False
280− -- API request failed with a retryable exception
281− Left httpExn@(NC.HttpExceptionRequest _ exn) ->
282− case (exn :: NC.HttpExceptionContent) of
283− NC.ResponseTimeout -> return True
284− NC.ConnectionTimeout -> return True
285− NC.ConnectionFailure _ -> return True
286− -- We received an unexpected exception
287− _ -> throwIO httpExn
288− -- We received an unexpected exception
289− Left someOtherExn -> throwIO someOtherExn
290−
291−executeRequest :: S3ReqInfo -> Minio (Response LByteString)
104+ -- getService/makeBucket/getLocation -- don't need
105+ -- location
106+ region <- if | not $ riNeedsLocation ri ->
107+ return $ Just $ connectRegion ci
108+
109+ -- if autodiscovery of location is disabled by user
110+ | not $ connectAutoDiscoverRegion ci ->
111+ return $ Just $ connectRegion ci
112+
113+ -- discover the region for the request
114+ | otherwise -> discoverRegion ri
115+
116+ regionHost <- case region of
117+ Nothing -> return $ connectHost ci
118+ Just r -> if "amazonaws.com" `T.isSuffixOf` connectHost ci
119+ then maybe
120+ (throwM $ MErrVRegionNotSupported r)
121+ return
122+ (Map.lookup r awsRegionMap)
123+ else return $ connectHost ci
124+
125+ sha256Hash <- if | connectIsSecure ci ->
126+ -- if secure connection
127+ return "UNSIGNED-PAYLOAD"
128+
129+ -- otherwise compute sha256
130+ | otherwise -> getPayloadSHA256Hash (riPayload ri)
131+
132+ let hostHeader = (hHost, formatBS "{}:{}" [connectHost ci,
133+ show $ connectPort ci])
134+
135+ newRi = ri { riPayloadHash = Just sha256Hash
136+ , riHeaders = hostHeader
137+ : sha256Header sha256Hash
138+ : riHeaders ri
139+ , riRegion = region
140+ }
141+ newCi = ci { connectHost = regionHost }
142+
143+ signHeaders <- liftIO $ signV4 newCi newRi Nothing
144+
145+ return NC.defaultRequest {
146+ NC.method = riMethod newRi
147+ , NC.secure = connectIsSecure newCi
148+ , NC.host = encodeUtf8 $ connectHost newCi
149+ , NC.port = connectPort newCi
150+ , NC.path = getPathFromRI newRi
151+ , NC.queryString = HT.renderQuery False $ riQueryParams newRi
152+ , NC.requestHeaders = riHeaders newRi ++ mkHeaderFromPairs signHeaders
153+ , NC.requestBody = getRequestBody (riPayload newRi)
154+ }
155+
156+executeRequest :: RequestInfo -> Minio (Response LByteString)
292157executeRequest ri = do
293158 req <- buildRequest ri
294159 mgr <- asks mcConnManager
295− retryAPIRequest $ httpLbs req mgr
160+ httpLbs req mgr
161+
296162
297−mkStreamRequest ::
298− S3ReqInfo ->
299− Minio (Response (C.ConduitM () ByteString Minio ()))
163+mkStreamRequest :: RequestInfo
164+ -> Minio (Response (C.ResumableSource Minio ByteString))
300165mkStreamRequest ri = do
301166 req <- buildRequest ri
302167 mgr <- asks mcConnManager
303− retryAPIRequest $ http req mgr
168+ http req mgr
304169
305170-- Bucket name validity check according to AWS rules.
306171isValidBucketName :: Bucket -> Bool
307172isValidBucketName bucket =
308− not
309− ( or
310− [ len < 3 || len > 63,
311− any labelCheck labels,
312− any labelCharsCheck labels,
313− isIPCheck
314− ]
315− )
173+ not (or [ len < 3 || len > 63
174+ , or (map labelCheck labels)
175+ , or (map labelCharsCheck labels)
176+ , isIPCheck
177+ ])
316178 where
317179 len = T.length bucket
318180 labels = T.splitOn "." bucket
181+
319182 -- does label `l` fail basic checks of length and start/end?
320183 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
184+
321185 -- 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
186+ labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
187+ x == '-' ||
188+ C.isDigit x)) l
189+
333190 -- does label `l` have non-digit characters?
334191 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
335192 labelAsNums = map (not . labelNonDigits) labels
193+
336194 -- check if bucket name looks like an IP
337195 isIPCheck = and labelAsNums && length labelAsNums == 4
338196
339197-- Throws exception iff bucket name is invalid according to AWS rules.
340−checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
198+checkBucketNameValidity :: MonadThrow m => Bucket -> m ()
341199checkBucketNameValidity bucket =
342− unless (isValidBucketName bucket) $
343− throwIO $
344− MErrVInvalidBucketName bucket
200+ when (not $ isValidBucketName bucket) $
201+ throwM $ MErrVInvalidBucketName bucket
345202
346203isValidObjectName :: Object -> Bool
347204isValidObjectName object =
348205 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
349206
350−checkObjectNameValidity :: (MonadIO m) => Object -> m ()
207+checkObjectNameValidity :: MonadThrow m => Object -> m ()
351208checkObjectNameValidity object =
352− unless (isValidObjectName object) $
353− throwIO $
354− MErrVInvalidObjectName object
209+ when (not $ isValidObjectName object) $
210+ throwM $ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hsdeleted−80
@@ -1,80 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−module Network.Minio.APICommon where
18−
19−import qualified Conduit as C
20−import qualified Data.ByteString as BS
21−import qualified Data.ByteString.Lazy as LB
22−import Data.Conduit.Binary (sourceHandleRange)
23−import qualified Data.Text as T
24−import Lib.Prelude
25−import qualified Network.HTTP.Conduit as NC
26−import qualified Network.HTTP.Types as HT
27−import Network.Minio.Data
28−import Network.Minio.Data.Crypto
29−import Network.Minio.Errors
30−
31−sha256Header :: ByteString -> HT.Header
32−sha256Header = ("x-amz-content-sha256",)
33−
34−-- | This function throws an error if the payload is a conduit (as it
35−-- will not be possible to re-read the conduit after it is consumed).
36−getPayloadSHA256Hash :: Payload -> Minio ByteString
37−getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
38−getPayloadSHA256Hash (PayloadH h off size) =
39− hashSHA256FromSource $
40− sourceHandleRange
41− h
42− (return . fromIntegral $ off)
43− (return . fromIntegral $ size)
44−getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
45−
46−getRequestBody :: Payload -> NC.RequestBody
47−getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
48−getRequestBody (PayloadH h off size) =
49− NC.requestBodySource size $
50− sourceHandleRange
51− h
52− (return . fromIntegral $ off)
53− (return . fromIntegral $ size)
54−getRequestBody (PayloadC n src) = NC.requestBodySource n src
55−
56−mkStreamingPayload :: Payload -> Payload
57−mkStreamingPayload payload =
58− case payload of
59− PayloadBS bs ->
60− PayloadC
61− (fromIntegral $ BS.length bs)
62− (C.sourceLazy $ LB.fromStrict bs)
63− PayloadH h off len ->
64− PayloadC len $
65− sourceHandleRange
66− h
67− (return . fromIntegral $ off)
68− (return . fromIntegral $ len)
69− _ -> payload
70−
71−isStreamingPayload :: Payload -> Bool
72−isStreamingPayload (PayloadC _ _) = True
73−isStreamingPayload _ = False
74−
75−-- | Checks if the connect info is for Amazon S3.
76−isAWSConnectInfo :: ConnectInfo -> Bool
77−isAWSConnectInfo ci = ".amazonaws.com" `T.isSuffixOf` connectHost ci
78−
79−bucketHasPeriods :: Bucket -> Bool
80−bucketHasPeriods b = isJust $ T.find (== '.') b
src/Network/Minio/AdminAPI.hsdeleted−711
@@ -1,711 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2018-2023 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−module Network.Minio.AdminAPI
18− ( -- * MinIO Admin API
19−
20− --------------------
21−
22− -- | Provides MinIO admin API and related types. It is in
23− -- experimental state.
24− DriveInfo (..),
25− ErasureInfo (..),
26− Backend (..),
27− ConnStats (..),
28− HttpStats (..),
29− ServerProps (..),
30− CountNAvgTime (..),
31− StorageClass (..),
32− StorageInfo (..),
33− SIData (..),
34− ServerInfo (..),
35− getServerInfo,
36− HealOpts (..),
37− HealResultItem (..),
38− HealStatus (..),
39− HealStartResp (..),
40− startHeal,
41− forceStartHeal,
42− getHealStatus,
43− SetConfigResult (..),
44− NodeSummary (..),
45− setConfig,
46− getConfig,
47− ServerVersion (..),
48− ServiceStatus (..),
49− serviceStatus,
50− ServiceAction (..),
51− serviceSendAction,
52− )
53−where
54−
55−import Data.Aeson
56− ( FromJSON,
57− ToJSON,
58− Value (Object),
59− eitherDecode,
60− object,
61− pairs,
62− parseJSON,
63− toEncoding,
64− toJSON,
65− withObject,
66− withText,
67− (.:),
68− (.:?),
69− (.=),
70− )
71−import qualified Data.Aeson as A
72−import Data.Aeson.Types (typeMismatch)
73−import qualified Data.ByteString as B
74−import qualified Data.ByteString.Lazy as LBS
75−import qualified Data.Text as T
76−import Data.Time (NominalDiffTime, getCurrentTime)
77−import Lib.Prelude
78−import Network.HTTP.Conduit (Response)
79−import qualified Network.HTTP.Conduit as NC
80−import qualified Network.HTTP.Types as HT
81−import Network.HTTP.Types.Header (hHost)
82−import Network.Minio.APICommon
83−import Network.Minio.Credentials
84−import Network.Minio.Data
85−import Network.Minio.Errors
86−import Network.Minio.Sign.V4
87−import Network.Minio.Utils
88−
89−data DriveInfo = DriveInfo
90− { diUuid :: Text,
91− diEndpoint :: Text,
92− diState :: Text
93− }
94− deriving stock (Show, Eq)
95−
96−instance FromJSON DriveInfo where
97− parseJSON = withObject "DriveInfo" $ \v ->
98− DriveInfo
99− <$> v
100− .: "uuid"
101− <*> v
102− .: "endpoint"
103− <*> v
104− .: "state"
105−
106−data StorageClass = StorageClass
107− { scParity :: Int,
108− scData :: Int
109− }
110− deriving stock (Show, Eq)
111−
112−data ErasureInfo = ErasureInfo
113− { eiOnlineDisks :: Int,
114− eiOfflineDisks :: Int,
115− eiStandard :: StorageClass,
116− eiReducedRedundancy :: StorageClass,
117− eiSets :: [[DriveInfo]]
118− }
119− deriving stock (Show, Eq)
120−
121−instance FromJSON ErasureInfo where
122− parseJSON = withObject "ErasureInfo" $ \v -> do
123− onlineDisks <- v .: "OnlineDisks"
124− offlineDisks <- v .: "OfflineDisks"
125− stdClass <-
126− StorageClass
127− <$> v
128− .: "StandardSCData"
129− <*> v
130− .: "StandardSCParity"
131− rrClass <-
132− StorageClass
133− <$> v
134− .: "RRSCData"
135− <*> v
136− .: "RRSCParity"
137− sets <- v .: "Sets"
138− return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
139−
140−data Backend
141− = BackendFS
142− | BackendErasure ErasureInfo
143− deriving stock (Show, Eq)
144−
145−instance FromJSON Backend where
146− parseJSON = withObject "Backend" $ \v -> do
147− typ <- v .: "Type"
148− case typ :: Int of
149− 1 -> return BackendFS
150− 2 -> BackendErasure <$> parseJSON (Object v)
151− _ -> typeMismatch "BackendType" (Object v)
152−
153−data ConnStats = ConnStats
154− { csTransferred :: Int64,
155− csReceived :: Int64
156− }
157− deriving stock (Show, Eq)
158−
159−instance FromJSON ConnStats where
160− parseJSON = withObject "ConnStats" $ \v ->
161− ConnStats
162− <$> v
163− .: "transferred"
164− <*> v
165− .: "received"
166−
167−data ServerProps = ServerProps
168− { spUptime :: NominalDiffTime,
169− spVersion :: Text,
170− spCommitId :: Text,
171− spRegion :: Text,
172− spSqsArns :: [Text]
173− }
174− deriving stock (Show, Eq)
175−
176−instance FromJSON ServerProps where
177− parseJSON = withObject "SIServer" $ \v -> do
178− uptimeNs <- v .: "uptime"
179− let uptime = uptimeNs / 1e9
180− ver <- v .: "version"
181− commitId <- v .: "commitID"
182− region <- v .: "region"
183− arn <- v .: "sqsARN"
184− return $ ServerProps uptime ver commitId region arn
185−
186−data StorageInfo = StorageInfo
187− { siUsed :: Int64,
188− siBackend :: Backend
189− }
190− deriving stock (Show, Eq)
191−
192−instance FromJSON StorageInfo where
193− parseJSON = withObject "StorageInfo" $ \v ->
194− StorageInfo
195− <$> v
196− .: "Used"
197− <*> v
198− .: "Backend"
199−
200−data CountNAvgTime = CountNAvgTime
201− { caCount :: Int64,
202− caAvgDuration :: Text
203− }
204− deriving stock (Show, Eq)
205−
206−instance FromJSON CountNAvgTime where
207− parseJSON = withObject "CountNAvgTime" $ \v ->
208− CountNAvgTime
209− <$> v
210− .: "count"
211− <*> v
212− .: "avgDuration"
213−
214−data HttpStats = HttpStats
215− { hsTotalHeads :: CountNAvgTime,
216− hsSuccessHeads :: CountNAvgTime,
217− hsTotalGets :: CountNAvgTime,
218− hsSuccessGets :: CountNAvgTime,
219− hsTotalPuts :: CountNAvgTime,
220− hsSuccessPuts :: CountNAvgTime,
221− hsTotalPosts :: CountNAvgTime,
222− hsSuccessPosts :: CountNAvgTime,
223− hsTotalDeletes :: CountNAvgTime,
224− hsSuccessDeletes :: CountNAvgTime
225− }
226− deriving stock (Show, Eq)
227−
228−instance FromJSON HttpStats where
229− parseJSON = withObject "HttpStats" $ \v ->
230− HttpStats
231− <$> v
232− .: "totalHEADs"
233− <*> v
234− .: "successHEADs"
235− <*> v
236− .: "totalGETs"
237− <*> v
238− .: "successGETs"
239− <*> v
240− .: "totalPUTs"
241− <*> v
242− .: "successPUTs"
243− <*> v
244− .: "totalPOSTs"
245− <*> v
246− .: "successPOSTs"
247− <*> v
248− .: "totalDELETEs"
249− <*> v
250− .: "successDELETEs"
251−
252−data SIData = SIData
253− { sdStorage :: StorageInfo,
254− sdConnStats :: ConnStats,
255− sdHttpStats :: HttpStats,
256− sdProps :: ServerProps
257− }
258− deriving stock (Show, Eq)
259−
260−instance FromJSON SIData where
261− parseJSON = withObject "SIData" $ \v ->
262− SIData
263− <$> v
264− .: "storage"
265− <*> v
266− .: "network"
267− <*> v
268− .: "http"
269− <*> v
270− .: "server"
271−
272−data ServerInfo = ServerInfo
273− { siError :: Text,
274− siAddr :: Text,
275− siData :: SIData
276− }
277− deriving stock (Show, Eq)
278−
279−instance FromJSON ServerInfo where
280− parseJSON = withObject "ServerInfo" $ \v ->
281− ServerInfo
282− <$> v
283− .: "error"
284− <*> v
285− .: "addr"
286− <*> v
287− .: "data"
288−
289−data ServerVersion = ServerVersion
290− { svVersion :: Text,
291− svCommitId :: Text
292− }
293− deriving stock (Show, Eq)
294−
295−instance FromJSON ServerVersion where
296− parseJSON = withObject "ServerVersion" $ \v ->
297− ServerVersion
298− <$> v
299− .: "version"
300− <*> v
301− .: "commitID"
302−
303−data ServiceStatus = ServiceStatus
304− { ssVersion :: ServerVersion,
305− ssUptime :: NominalDiffTime
306− }
307− deriving stock (Show, Eq)
308−
309−instance FromJSON ServiceStatus where
310− parseJSON = withObject "ServiceStatus" $ \v -> do
311− serverVersion <- v .: "serverVersion"
312− uptimeNs <- v .: "uptime"
313− let uptime = uptimeNs / 1e9
314− return $ ServiceStatus serverVersion uptime
315−
316−data ServiceAction
317− = ServiceActionRestart
318− | ServiceActionStop
319− deriving stock (Show, Eq)
320−
321−instance ToJSON ServiceAction where
322− toJSON a = object ["action" .= serviceActionToText a]
323−
324−serviceActionToText :: ServiceAction -> Text
325−serviceActionToText a = case a of
326− ServiceActionRestart -> "restart"
327− ServiceActionStop -> "stop"
328−
329−adminPath :: ByteString
330−adminPath = "/minio/admin"
331−
332−data HealStartResp = HealStartResp
333− { hsrClientToken :: Text,
334− hsrClientAddr :: Text,
335− hsrStartTime :: UTCTime
336− }
337− deriving stock (Show, Eq)
338−
339−instance FromJSON HealStartResp where
340− parseJSON = withObject "HealStartResp" $ \v ->
341− HealStartResp
342− <$> v
343− .: "clientToken"
344− <*> v
345− .: "clientAddress"
346− <*> v
347− .: "startTime"
348−
349−data HealOpts = HealOpts
350− { hoRecursive :: Bool,
351− hoDryRun :: Bool
352− }
353− deriving stock (Show, Eq)
354−
355−instance ToJSON HealOpts where
356− toJSON (HealOpts r d) =
357− object ["recursive" .= r, "dryRun" .= d]
358− toEncoding (HealOpts r d) =
359− pairs ("recursive" .= r <> "dryRun" .= d)
360−
361−instance FromJSON HealOpts where
362− parseJSON = withObject "HealOpts" $ \v ->
363− HealOpts
364− <$> v
365− .: "recursive"
366− <*> v
367− .: "dryRun"
368−
369−data HealItemType
370− = HealItemMetadata
371− | HealItemBucket
372− | HealItemBucketMetadata
373− | HealItemObject
374− deriving stock (Show, Eq)
375−
376−instance FromJSON HealItemType where
377− parseJSON = withText "HealItemType" $ \v -> case v of
378− "metadata" -> return HealItemMetadata
379− "bucket" -> return HealItemBucket
380− "object" -> return HealItemObject
381− "bucket-metadata" -> return HealItemBucketMetadata
382− _ -> typeMismatch "HealItemType" (A.String v)
383−
384−data NodeSummary = NodeSummary
385− { nsName :: Text,
386− nsErrSet :: Bool,
387− nsErrMessage :: Text
388− }
389− deriving stock (Show, Eq)
390−
391−instance FromJSON NodeSummary where
392− parseJSON = withObject "NodeSummary" $ \v ->
393− NodeSummary
394− <$> v
395− .: "name"
396− <*> v
397− .: "errSet"
398− <*> v
399− .: "errMsg"
400−
401−data SetConfigResult = SetConfigResult
402− { scrStatus :: Bool,
403− scrNodeSummary :: [NodeSummary]
404− }
405− deriving stock (Show, Eq)
406−
407−instance FromJSON SetConfigResult where
408− parseJSON = withObject "SetConfigResult" $ \v ->
409− SetConfigResult
410− <$> v
411− .: "status"
412− <*> v
413− .: "nodeResults"
414−
415−data HealResultItem = HealResultItem
416− { hriResultIdx :: Int,
417− hriType :: HealItemType,
418− hriBucket :: Bucket,
419− hriObject :: Object,
420− hriDetail :: Text,
421− hriParityBlocks :: Maybe Int,
422− hriDataBlocks :: Maybe Int,
423− hriDiskCount :: Int,
424− hriSetCount :: Int,
425− hriObjectSize :: Int,
426− hriBefore :: [DriveInfo],
427− hriAfter :: [DriveInfo]
428− }
429− deriving stock (Show, Eq)
430−
431−instance FromJSON HealResultItem where
432− parseJSON = withObject "HealResultItem" $ \v ->
433− HealResultItem
434− <$> v
435− .: "resultId"
436− <*> v
437− .: "type"
438− <*> v
439− .: "bucket"
440− <*> v
441− .: "object"
442− <*> v
443− .: "detail"
444− <*> v
445− .:? "parityBlocks"
446− <*> v
447− .:? "dataBlocks"
448− <*> v
449− .: "diskCount"
450− <*> v
451− .: "setCount"
452− <*> v
453− .: "objectSize"
454− <*> ( do
455− before <- v .: "before"
456− before .: "drives"
457− )
458− <*> ( do
459− after <- v .: "after"
460− after .: "drives"
461− )
462−
463−data HealStatus = HealStatus
464− { hsSummary :: Text,
465− hsStartTime :: UTCTime,
466− hsSettings :: HealOpts,
467− hsNumDisks :: Int,
468− hsFailureDetail :: Maybe Text,
469− hsItems :: Maybe [HealResultItem]
470− }
471− deriving stock (Show, Eq)
472−
473−instance FromJSON HealStatus where
474− parseJSON = withObject "HealStatus" $ \v ->
475− HealStatus
476− <$> v
477− .: "Summary"
478− <*> v
479− .: "StartTime"
480− <*> v
481− .: "Settings"
482− <*> v
483− .: "NumDisks"
484− <*> v
485− .:? "Detail"
486− <*> v
487− .: "Items"
488−
489−healPath :: Maybe Bucket -> Maybe Text -> ByteString
490−healPath bucket prefix = do
491− if isJust bucket
492− then
493− encodeUtf8 $
494− "v1/heal/"
495− <> fromMaybe "" bucket
496− <> "/"
497− <> fromMaybe "" prefix
498− else encodeUtf8 ("v1/heal/" :: Text)
499−
500−-- | Get server version and uptime.
501−serviceStatus :: Minio ServiceStatus
502−serviceStatus = do
503− rsp <-
504− executeAdminRequest
505− AdminReqInfo
506− { ariMethod = HT.methodGet,
507− ariPayload = PayloadBS B.empty,
508− ariPayloadHash = Nothing,
509− ariPath = "v1/service",
510− ariHeaders = [],
511− ariQueryParams = []
512− }
513−
514− let rspBS = NC.responseBody rsp
515− case eitherDecode rspBS of
516− Right ss -> return ss
517− Left err -> throwIO $ MErrVJsonParse $ T.pack err
518−
519−-- | Send service restart or stop action to MinIO server.
520−serviceSendAction :: ServiceAction -> Minio ()
521−serviceSendAction action = do
522− let payload = PayloadBS $ LBS.toStrict $ A.encode action
523− void $
524− executeAdminRequest
525− AdminReqInfo
526− { ariMethod = HT.methodPost,
527− ariPayload = payload,
528− ariPayloadHash = Nothing,
529− ariPath = "v1/service",
530− ariHeaders = [],
531− ariQueryParams = []
532− }
533−
534−-- | Get the current config file from server.
535−getConfig :: Minio ByteString
536−getConfig = do
537− rsp <-
538− executeAdminRequest
539− AdminReqInfo
540− { ariMethod = HT.methodGet,
541− ariPayload = PayloadBS B.empty,
542− ariPayloadHash = Nothing,
543− ariPath = "v1/config",
544− ariHeaders = [],
545− ariQueryParams = []
546− }
547− return $ LBS.toStrict $ NC.responseBody rsp
548−
549−-- | Set a new config to the server.
550−setConfig :: ByteString -> Minio SetConfigResult
551−setConfig config = do
552− rsp <-
553− executeAdminRequest
554− AdminReqInfo
555− { ariMethod = HT.methodPut,
556− ariPayload = PayloadBS config,
557− ariPayloadHash = Nothing,
558− ariPath = "v1/config",
559− ariHeaders = [],
560− ariQueryParams = []
561− }
562−
563− let rspBS = NC.responseBody rsp
564− case eitherDecode rspBS of
565− Right scr -> return scr
566− Left err -> throwIO $ MErrVJsonParse $ T.pack err
567−
568−-- | Get the progress of currently running heal task, this API should be
569−-- invoked right after `startHeal`. `token` is obtained after `startHeal`
570−-- which should be used to get the heal status.
571−getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
572−getHealStatus bucket prefix token = do
573− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
574− let qparams = HT.queryTextToQuery [("clientToken", Just token)]
575− rsp <-
576− executeAdminRequest
577− AdminReqInfo
578− { ariMethod = HT.methodPost,
579− ariPayload = PayloadBS B.empty,
580− ariPayloadHash = Nothing,
581− ariPath = healPath bucket prefix,
582− ariHeaders = [],
583− ariQueryParams = qparams
584− }
585− let rspBS = NC.responseBody rsp
586− case eitherDecode rspBS of
587− Right hs -> return hs
588− Left err -> throwIO $ MErrVJsonParse $ T.pack err
589−
590−doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
591−doHeal bucket prefix opts forceStart = do
592− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
593− let payload = PayloadBS $ LBS.toStrict $ A.encode opts
594− let qparams =
595− bool
596− []
597− (HT.queryTextToQuery [("forceStart", Just "true")])
598− forceStart
599−
600− rsp <-
601− executeAdminRequest
602− AdminReqInfo
603− { ariMethod = HT.methodPost,
604− ariPayload = payload,
605− ariPayloadHash = Nothing,
606− ariPath = healPath bucket prefix,
607− ariHeaders = [],
608− ariQueryParams = qparams
609− }
610−
611− let rspBS = NC.responseBody rsp
612− case eitherDecode rspBS of
613− Right hsr -> return hsr
614− Left err -> throwIO $ MErrVJsonParse $ T.pack err
615−
616−-- | Start a heal sequence that scans data under given (possible empty)
617−-- `bucket` and `prefix`. The `recursive` bool turns on recursive
618−-- traversal under the given path. `dryRun` does not mutate on-disk data,
619−-- but performs data validation. Two heal sequences on overlapping paths
620−-- may not be initiated. The progress of a heal should be followed using
621−-- the `HealStatus` API. The server accumulates results of the heal
622−-- traversal and waits for the client to receive and acknowledge
623−-- them using the status API
624−startHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
625−startHeal bucket prefix opts = doHeal bucket prefix opts False
626−
627−-- | Similar to start a heal sequence, but force start a new heal sequence
628−-- even if an active heal is under progress.
629−forceStartHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
630−forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
631−
632−-- | Fetches information for all cluster nodes, such as server
633−-- properties, storage information, network statistics, etc.
634−getServerInfo :: Minio [ServerInfo]
635−getServerInfo = do
636− rsp <-
637− executeAdminRequest
638− AdminReqInfo
639− { ariMethod = HT.methodGet,
640− ariPayload = PayloadBS B.empty,
641− ariPayloadHash = Nothing,
642− ariPath = "v1/info",
643− ariHeaders = [],
644− ariQueryParams = []
645− }
646− let rspBS = NC.responseBody rsp
647− case eitherDecode rspBS of
648− Right si -> return si
649− Left err -> throwIO $ MErrVJsonParse $ T.pack err
650−
651−executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
652−executeAdminRequest ari = do
653− req <- buildAdminRequest ari
654− mgr <- asks mcConnManager
655− httpLbs req mgr
656−
657−buildAdminRequest :: AdminReqInfo -> Minio NC.Request
658−buildAdminRequest areq = do
659− ci <- asks mcConnInfo
660− sha256Hash <-
661− if connectIsSecure ci
662− then -- if secure connection
663− return "UNSIGNED-PAYLOAD"
664− else -- otherwise compute sha256
665− getPayloadSHA256Hash (ariPayload areq)
666−
667− timeStamp <- liftIO getCurrentTime
668−
669− mgr <- asks mcConnManager
670− cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671−
672− let hostHeader = (hHost, getHostAddr ci)
673− newAreq =
674− areq
675− { ariPayloadHash = Just sha256Hash,
676− ariHeaders =
677− hostHeader
678− : sha256Header sha256Hash
679− : ariHeaders areq
680− }
681− signReq = toRequest ci newAreq
682− sp =
683− SignParams
684− (coerce $ cvAccessKey cv)
685− (coerce $ cvSecretKey cv)
686− (coerce $ cvSessionToken cv)
687− ServiceS3
688− timeStamp
689− Nothing
690− Nothing
691− (ariPayloadHash newAreq)
692− signHeaders = signV4 sp signReq
693−
694− -- Update signReq with Authorization header containing v4 signature
695− return
696− signReq
697− { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
698− }
699− where
700− toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
701− toRequest ci aReq =
702− NC.defaultRequest
703− { NC.method = ariMethod aReq,
704− NC.secure = connectIsSecure ci,
705− NC.host = encodeUtf8 $ connectHost ci,
706− NC.port = connectPort ci,
707− NC.path = B.intercalate "/" [adminPath, ariPath aReq],
708− NC.requestHeaders = ariHeaders aReq,
709− NC.queryString = HT.renderQuery False $ ariQueryParams aReq,
710− NC.requestBody = getRequestBody (ariPayload aReq)
711− }
src/Network/Minio/CopyObject.hsdeleted−106
@@ -1,106 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−module Network.Minio.CopyObject where
18−
19−import qualified Data.List as List
20−import Lib.Prelude
21−import Network.Minio.Data
22−import Network.Minio.Errors
23−import Network.Minio.S3API
24−import Network.Minio.Utils
25−
26−-- | Copy an object using single or multipart copy strategy.
27−copyObjectInternal ::
28− Bucket ->
29− Object ->
30− SourceInfo ->
31− Minio ETag
32−copyObjectInternal b' o srcInfo = do
33− let sBucket = srcBucket srcInfo
34− sObject = srcObject srcInfo
35−
36− -- get source object size with a head request
37− oi <- headObject sBucket sObject []
38− let srcSize = oiSize oi
39−
40− -- check that byte offsets are valid if specified in cps
41− let rangeMay = srcRange srcInfo
42− range = maybe (0, srcSize) identity rangeMay
43− startOffset = fst range
44− endOffset = snd range
45−
46− when
47− ( isJust rangeMay
48− && ( (startOffset < 0)
49− || (endOffset < startOffset)
50− || (endOffset >= srcSize)
51− )
52− )
53− $ throwIO
54− $ MErrVInvalidSrcObjByteRange range
55−
56− -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
57− -- 2. If startOffset /= 0 use multipart copy
58− let destSize =
59− (\(a, b) -> b - a + 1) $
60− maybe (0, srcSize - 1) identity rangeMay
61−
62− if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
63− then multiPartCopyObject b' o srcInfo srcSize
64− else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} []
65−
66−-- | Given the input byte range of the source object, compute the
67−-- splits for a multipart copy object procedure. Minimum part size
68−-- used is minPartSize.
69−selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
70−selectCopyRanges (st, end) =
71− zip pns $
72− zipWith (\x y -> (st + x, st + x + y - 1)) startOffsets partSizes
73− where
74− size = end - st + 1
75− (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
76−
77−-- | Perform a multipart copy object action. Since we cannot verify
78−-- existing parts based on the source object, there is no resuming
79−-- copy action support.
80−multiPartCopyObject ::
81− Bucket ->
82− Object ->
83− SourceInfo ->
84− Int64 ->
85− Minio ETag
86−multiPartCopyObject b o cps srcSize = do
87− uid <- newMultipartUpload b o []
88−
89− let byteRange = maybe (0, srcSize - 1) identity $ srcRange cps
90− partRanges = selectCopyRanges byteRange
91− partSources =
92− map
93− (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end)}))
94− partRanges
95− dstInfo = defaultDestinationInfo {dstBucket = b, dstObject = o}
96−
97− copiedParts <-
98− limitedMapConcurrently
99− 10
100− ( \(pn, cps') -> do
101− (etag, _) <- copyObjectPart dstInfo cps' uid pn []
102− return (pn, etag)
103− )
104− partSources
105−
106− completeMultipartUpload b o uid copiedParts
src/Network/Minio/Credentials.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+330−1038
@@ -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,276 +13,141 @@
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 GeneralizedNewtypeDeriving, TypeFamilies #-}
2218module Network.Minio.Data where
2319
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
20+import Control.Monad.Base
21+import qualified Control.Monad.Catch as MC
22+import Control.Monad.Trans.Control
23+import Control.Monad.Trans.Resource
24+
3625import 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
26+import Data.Default (Default(..))
27+import qualified Data.Map as Map
4228import 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
29+import Data.Time (formatTime, defaultTimeLocale)
30+import Network.HTTP.Client (defaultManagerSettings)
4931import qualified Network.HTTP.Conduit as NC
50−import Network.HTTP.Types
51− ( ByteRange,
52− Header,
53− Method,
54− Query,
55− hRange,
56− )
32+import Network.HTTP.Types (Method, Header, Query)
5733import 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
74−
75−-- | max obj size is 5TiB
76−maxObjectSize :: Int64
77−maxObjectSize = 5 * 1024 * 1024 * oneMiB
34+import Network.Minio.Errors
35+import Text.XML
7836
79−-- | minimum size of parts used in multipart operations.
80−minPartSize :: Int64
81−minPartSize = 64 * oneMiB
37+import GHC.Show (Show(..))
8238
83−oneMiB :: Int64
84−oneMiB = 1024 * 1024
39+import Lib.Prelude
8540
86−-- | maximum number of parts that can be uploaded for a single object.
87−maxMultipartParts :: Int64
88−maxMultipartParts = 10000
8941
9042-- TODO: Add a type which provides typed constants for region. this
9143-- type should have a IsString instance to infer the appropriate
9244-- constant.
93−
9445-- | awsRegionMap - library constant
95−awsRegionMap :: H.HashMap Text Text
96−awsRegionMap =
97− H.fromList
98− [ ("us-east-1", "s3.us-east-1.amazonaws.com"),
99− ("us-east-2", "s3.us-east-2.amazonaws.com"),
100− ("us-west-1", "s3.us-west-1.amazonaws.com"),
101− ("us-west-2", "s3.us-west-2.amazonaws.com"),
102− ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
103− ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
104− ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
105− ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
106− ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
107− ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
108− ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
109− ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
110− ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
111− ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
112− ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
113− ("eu-west-3", "s3.eu-west-3.amazonaws.com"),
114− ("eu-central-1", "s3.eu-central-1.amazonaws.com"),
115− ("eu-central-2", "s3.eu-central-2.amazonaws.com"),
116− ("eu-south-1", "s3.eu-south-1.amazonaws.com"),
117− ("eu-south-2", "s3.eu-south-2.amazonaws.com"),
118− ("af-south-1", "s3.af-south-1.amazonaws.com"),
119− ("ap-east-1", "s3.ap-east-1.amazonaws.com"),
120− ("cn-north-1", "s3.cn-north-1.amazonaws.com.cn"),
121− ("cn-northwest-1", "s3.cn-northwest-1.amazonaws.com.cn"),
122− ("eu-north-1", "s3.eu-north-1.amazonaws.com"),
123− ("me-south-1", "s3.me-south-1.amazonaws.com"),
124− ("me-central-1", "s3.me-central-1.amazonaws.com"),
125− ("us-gov-east-1", "s3.us-gov-east-1.amazonaws.com"),
126− ("us-gov-west-1", "s3.us-gov-west-1.amazonaws.com"),
127− ("sa-east-1", "s3.sa-east-1.amazonaws.com")
128− ]
129−
130−-- | Connection Info data type. To create a 'ConnectInfo' value,
131−-- enable the @OverloadedStrings@ language extension and use the
132−-- `IsString` instance to provide a URL, for example:
46+awsRegionMap :: Map.Map Text Text
47+awsRegionMap = Map.fromList [
48+ ("us-east-1", "s3.amazonaws.com")
49+ , ("us-east-2", "s3-us-east-2.amazonaws.com")
50+ , ("us-west-1", "s3-us-west-1.amazonaws.com")
51+ , ("us-east-2", "s3-us-west-2.amazonaws.com")
52+ , ("ca-central-1", "s3-ca-central-1.amazonaws.com")
53+ , ("ap-south-1", "s3-ap-south-1.amazonaws.com")
54+ , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
55+ , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
56+ , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
57+ , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
58+ , ("eu-west-1", "s3-eu-west-1.amazonaws.com")
59+ , ("eu-west-2", "s3-eu-west-2.amazonaws.com")
60+ , ("eu-central-1", "s3-eu-central-1.amazonaws.com")
61+ , ("sa-east-1", "s3-sa-east-1.amazonaws.com")
62+ ]
63+
64+-- | Connection Info data type. To create a 'ConnectInfo' value, use one
65+-- of the provided smart constructors or override fields of the
66+-- Default instance.
67+data ConnectInfo = ConnectInfo {
68+ connectHost :: Text
69+ , connectPort :: Int
70+ , connectAccessKey :: Text
71+ , connectSecretKey :: Text
72+ , connectIsSecure :: Bool
73+ , connectRegion :: Region
74+ , connectAutoDiscoverRegion :: Bool
75+ } deriving (Eq, Show)
76+
77+-- | Connects to a Minio server located at @localhost:9000@ with access
78+-- key /minio/ and secret key /minio123/. It is over __HTTP__ by
79+-- default.
80+instance Default ConnectInfo where
81+ def = ConnectInfo "localhost" 9000 "minio" "minio123" False "us-east-1" True
82+
83+-- | Default AWS ConnectInfo. Connects to "us-east-1". Credentials
84+-- should be supplied before use, for e.g.:
13385--
134−-- > let c :: ConnectInfo = "https://play.min.io"
135−data ConnectInfo = ConnectInfo
136− { connectHost :: Text,
137− connectPort :: Int,
138− connectCreds :: Creds,
139− connectIsSecure :: Bool,
140− connectRegion :: Region,
141− connectAutoDiscoverRegion :: Bool,
142− connectDisableTLSCertValidation :: Bool
86+-- > awsCI {
87+-- > connectAccessKey = "my-access-key"
88+-- > , connectSecretKey = "my-secret-key"
89+-- > }
90+awsCI :: ConnectInfo
91+awsCI = def {
92+ connectHost = "s3.amazonaws.com"
93+ , connectPort = 443
94+ , connectAccessKey = ""
95+ , connectSecretKey = ""
96+ , connectIsSecure = True
14397 }
14498
145−getEndpoint :: ConnectInfo -> Endpoint
146−getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
147−
148−instance IsString ConnectInfo where
149− fromString str =
150− let req = NC.parseRequest_ str
151− in ConnectInfo
152− { connectHost = TE.decodeUtf8 $ NC.host req,
153− connectPort = NC.port req,
154− connectCreds = CredsStatic $ CredentialValue mempty mempty mempty,
155− connectIsSecure = NC.secure req,
156− connectRegion = "",
157− connectAutoDiscoverRegion = True,
158− connectDisableTLSCertValidation = False
159− }
160−
161−-- | A 'CredentialLoader' is an action that may return a 'CredentialValue'.
162−-- Loaders may be chained together using 'findFirst'.
99+-- | AWS ConnectInfo with a specified region. It can optionally
100+-- disable the automatic discovery of a bucket's region via the
101+-- Boolean argument.
163102--
164−-- @since 1.7.0
165−type CredentialLoader = IO (Maybe CredentialValue)
166−
167−-- | Combines the given list of loaders, by calling each one in
168−-- order until a 'CredentialValue' is returned.
169−findFirst :: [CredentialLoader] -> IO (Maybe CredentialValue)
170−findFirst [] = return Nothing
171−findFirst (f : fs) = do
172− c <- f
173− maybe (findFirst fs) (return . Just) c
174−
175−-- | This action returns a 'CredentialValue' populated from
176−-- @~\/.aws\/credentials@
177−fromAWSConfigFile :: CredentialLoader
178−fromAWSConfigFile = do
179− credsE <- runExceptT $ do
180− homeDir <- lift getHomeDirectory
181− let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
182− fileExists <- lift $ doesFileExist awsCredsFile
183− bool (throwE "FileNotFound") (return ()) fileExists
184− ini <- ExceptT $ Ini.readIniFile awsCredsFile
185− akey <-
186− ExceptT $
187− return $
188− Ini.lookupValue "default" "aws_access_key_id" ini
189− skey <-
190− ExceptT $
191− return $
192− Ini.lookupValue "default" "aws_secret_access_key" ini
193− return $ CredentialValue (coerce akey) (fromString $ T.unpack skey) Nothing
194− return $ either (const Nothing) Just credsE
195−
196−-- | This action returns a 'CredentialValue` populated from @AWS_ACCESS_KEY_ID@
197−-- and @AWS_SECRET_ACCESS_KEY@ environment variables.
198−fromAWSEnv :: CredentialLoader
199−fromAWSEnv = runMaybeT $ do
200− akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
201− skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
202− return $ CredentialValue (fromString akey) (fromString skey) Nothing
203−
204−-- | This action returns a 'CredentialValue' populated from @MINIO_ACCESS_KEY@
205−-- and @MINIO_SECRET_KEY@ environment variables.
206−fromMinioEnv :: CredentialLoader
207−fromMinioEnv = runMaybeT $ do
208− akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
209− skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
210− return $ CredentialValue (fromString akey) (fromString skey) Nothing
211−
212−-- | setCredsFrom retrieves access credentials from the first action in the
213−-- given list that succeeds and sets it in the 'ConnectInfo'.
214−setCredsFrom :: [CredentialLoader] -> ConnectInfo -> IO ConnectInfo
215−setCredsFrom ps ci = do
216− pMay <- findFirst ps
217− maybe
218− (throwIO MErrVMissingCredentials)
219− (return . (`setCreds` ci))
220− pMay
221−
222−-- | setCreds sets the given `CredentialValue` in the `ConnectInfo`.
223−setCreds :: CredentialValue -> ConnectInfo -> ConnectInfo
224−setCreds cv connInfo =
225− connInfo
226− { connectCreds = CredsStatic cv
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}
236−
237−-- | Set the S3 region parameter in the `ConnectInfo`
238−setRegion :: Region -> ConnectInfo -> ConnectInfo
239−setRegion r connInfo =
240− connInfo
241− { connectRegion = r,
242− connectAutoDiscoverRegion = False
243− }
244−
245−-- | Check if the connection to object storage server is secure
246−-- (i.e. uses TLS)
247−isConnectInfoSecure :: ConnectInfo -> Bool
248−isConnectInfoSecure = connectIsSecure
249−
250−-- | Disable TLS certificate validation completely! This makes TLS
251−-- insecure! Use only for testing with self-signed or temporary
252−-- certificates. Note that this option has no effect, if you provide
253−-- your own Manager in `mkMinioConn`.
254−disableTLSCertValidation :: ConnectInfo -> ConnectInfo
255−disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
256−
257−getHostAddr :: ConnectInfo -> ByteString
258−getHostAddr ci = getHostHeader (encodeUtf8 $ connectHost ci, connectPort ci)
259−
260−-- | Default Google Compute Storage ConnectInfo. Works only for
261−-- "Simple Migration" use-case with interoperability mode enabled on
262−-- GCP console. For more information -
263−-- https://cloud.google.com/storage/docs/migrating
103+-- > awsWithRegionCI "us-west-1" False {
104+-- > connectAccessKey = "my-access-key"
105+-- > , connectSecretKey = "my-secret-key"
106+-- > }
264107--
265−-- Credentials should be supplied before use.
266−gcsCI :: ConnectInfo
267−gcsCI =
268− setRegion
269− "us"
270− "https://storage.googleapis.com"
108+-- This restricts all operations to the "us-west-1" region and does
109+-- not perform any bucket location requests.
110+awsWithRegionCI :: Region -> Bool -> ConnectInfo
111+awsWithRegionCI region autoDiscoverRegion =
112+ let host = maybe "s3.amazonaws.com" identity $
113+ Map.lookup region awsRegionMap
114+ in awsCI {
115+ connectHost = host
116+ , connectRegion = region
117+ , connectAutoDiscoverRegion = autoDiscoverRegion
118+ }
271119
272−-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
273−-- should be supplied before use.
274−awsCI :: ConnectInfo
275−awsCI = "https://s3.amazonaws.com"
276120
277−-- | <https://play.min.io MinIO Play Server>
121+-- | <https://play.minio.io:9000 Minio Play Server>
278122-- ConnectInfo. Credentials are already filled in.
279123minioPlayCI :: 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"
124+minioPlayCI = def {
125+ connectHost = "play.minio.io"
126+ , connectPort = 9000
127+ , connectAccessKey = "Q3AM3UQ867SPQQA43P2F"
128+ , connectSecretKey = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
129+ , connectIsSecure = True
130+ , connectAutoDiscoverRegion = False
131+ }
132+
133+-- | ConnectInfo for Minio server. Takes hostname, port and a Boolean
134+-- to enable TLS.
135+--
136+-- > minioCI "minio.example.com" 9000 True {
137+-- > connectAccessKey = "my-access-key"
138+-- > , connectSecretKey = "my-secret-key"
139+-- > }
140+--
141+-- This connects to a Minio server at the given hostname and port over
142+-- HTTPS.
143+minioCI :: Text -> Int -> Bool -> ConnectInfo
144+minioCI host port isSecure = def {
145+ connectHost = host
146+ , connectPort = port
147+ , connectRegion = "us-east-1"
148+ , connectIsSecure = isSecure
149+ , connectAutoDiscoverRegion = False
150+ }
286151
287152-- |
288153-- Represents a bucket in the object store
@@ -292,156 +157,25 @@ type Bucket = Text
292157-- Represents an object name
293158type Object = Text
294159
295−-- | Represents a region
160+-- |
161+-- Represents a region
162+-- TODO: This could be a Sum Type with all defined regions for AWS.
296163type Region = Text
297164
298−-- | A type alias to represent an Entity-Tag returned by S3-compatible APIs.
165+-- | A type alias to represent an Entity-Tag returned by S3-compatible
166+-- APIs.
299167type ETag = Text
300168
301−-- | Data type to represent an object encryption key. Create one using
302−-- the `mkSSECKey` function.
303−newtype SSECKey = SSECKey BA.ScrubbedBytes
304− deriving stock (Eq, Show)
305−
306−-- | Validates that the given ByteString is 32 bytes long and creates
307−-- an encryption key.
308−mkSSECKey :: (MonadThrow m) => ByteString -> m SSECKey
309−mkSSECKey keyBytes
310− | B.length keyBytes /= 32 =
311− throwM MErrVInvalidEncryptionKeyLength
312− | otherwise =
313− return $ SSECKey $ BA.convert keyBytes
314−
315−-- | Data type to represent Server-Side-Encryption settings
316−data SSE where
317− -- | Specifies SSE S3 encryption - server manages encryption keys
318− SSE :: SSE
319− -- | Specifies that KMS service should be used. The first argument
320− -- to the constructor is the Key Id to be used by the server (if
321− -- not specified, the default KMS key id is used). The second
322− -- argument is the optional KMS context that must have a
323− -- `A.ToJSON` instance - please refer to the AWS S3 documentation
324− -- for detailed information.
325− SSEKMS :: (A.ToJSON a) => Maybe ByteString -> Maybe a -> SSE
326− -- | Specifies server-side encryption with customer provided
327− -- key. The argument is the encryption key to be used.
328− SSEC :: SSECKey -> SSE
329−
330−toPutObjectHeaders :: SSE -> [HT.Header]
331−toPutObjectHeaders sseArg =
332− let sseHeader = "x-amz-server-side-encryption"
333− sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
334− sseKmsContextHeader = sseHeader <> "-context"
335− ssecAlgo = sseHeader <> "-customer-algorithm"
336− ssecKey = sseHeader <> "-customer-key"
337− ssecKeyMD5 = ssecKey <> "-MD5"
338− in case sseArg of
339− SSE -> [(sseHeader, "AES256")]
340− SSEKMS keyIdMay ctxMay ->
341− [(sseHeader, "aws:kms")]
342− ++ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay
343− ++ maybe [] (\k -> [(sseKmsContextHeader, LB.toStrict $ A.encode k)]) ctxMay
344− SSEC (SSECKey sb) ->
345− [ (ssecAlgo, "AES256"),
346− (ssecKey, encodeToBase64 sb),
347− (ssecKeyMD5, hashMD5ToBase64 sb)
348− ]
349−
350−-- | Data type for options in PutObject call. Start with the empty
351−-- `defaultPutObjectOptions` and use various the various poo*
352−-- accessors.
353−data PutObjectOptions = PutObjectOptions
354− { -- | Set a standard MIME type describing the format of the object.
355− pooContentType :: Maybe Text,
356− -- | Set what content encodings have been applied to the object and thus
357− -- what decoding mechanisms must be applied to obtain the media-type
358− -- referenced by the Content-Type header field.
359− pooContentEncoding :: Maybe Text,
360− -- | Set presentational information for the object.
361− pooContentDisposition :: Maybe Text,
362− -- | Set to specify caching behavior for the object along the
363− -- request/reply chain.
364− pooCacheControl :: Maybe Text,
365− -- | Set to describe the language(s) intended for the audience.
366− pooContentLanguage :: Maybe Text,
367− -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
368− -- performance needs, storage class is @STANDARD@ by default (i.e
369− -- when Nothing is passed).
370− pooStorageClass :: Maybe Text,
371− -- | Set user defined metadata to store with the object.
372− pooUserMetadata :: [(Text, Text)],
373− -- | Set number of worker threads used to upload an object.
374− pooNumThreads :: Maybe Word,
375− -- | Set object encryption parameters for the request.
376− pooSSE :: Maybe SSE
377− }
378−
379−-- | Provide default `PutObjectOptions`.
380−defaultPutObjectOptions :: PutObjectOptions
381−defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing [] Nothing Nothing
382−
383−pooToHeaders :: PutObjectOptions -> [HT.Header]
384−pooToHeaders poo =
385− userMetadata
386− ++ mapMaybe tupToMaybe (zip names values)
387− ++ maybe [] toPutObjectHeaders (pooSSE poo)
388− where
389− tupToMaybe (k, Just v) = Just (k, v)
390− tupToMaybe (_, Nothing) = Nothing
391− userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
392− names =
393− [ "content-type",
394− "content-encoding",
395− "content-disposition",
396− "content-language",
397− "cache-control",
398− "x-amz-storage-class"
399− ]
400− values =
401− map
402− (fmap encodeUtf8 . (poo &))
403− [ pooContentType,
404− pooContentEncoding,
405− pooContentDisposition,
406− pooContentLanguage,
407− pooCacheControl,
408− pooStorageClass
409− ]
410−
411169-- |
412170-- BucketInfo returned for list buckets call
413−data BucketInfo = BucketInfo
414− { biName :: Bucket,
415− biCreationDate :: UTCTime
416− }
417− deriving stock (Show, Eq)
171+data BucketInfo = BucketInfo {
172+ biName :: Bucket
173+ , biCreationDate :: UTCTime
174+ } deriving (Show, Eq)
418175
419176-- | A type alias to represent a part-number for multipart upload
420177type PartNumber = Int16
421178
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−
445179-- | A type alias to represent an upload-id for multipart upload
446180type UploadId = Text
447181
@@ -450,264 +184,163 @@ type PartTuple = (PartNumber, ETag)
450184
451185-- | Represents result from a listing of object parts of an ongoing
452186-- multipart upload.
453−data ListPartsResult = ListPartsResult
454− { lprHasMore :: Bool,
455− lprNextPart :: Maybe Int,
456− lprParts :: [ObjectPartInfo]
457− }
458− deriving stock (Show, Eq)
187+data ListPartsResult = ListPartsResult {
188+ lprHasMore :: Bool
189+ , lprNextPart :: Maybe Int
190+ , lprParts :: [ObjectPartInfo]
191+ } deriving (Show, Eq)
192+
459193
460194-- | Represents information about an object part in an ongoing
461195-- multipart upload.
462−data ObjectPartInfo = ObjectPartInfo
463− { opiNumber :: PartNumber,
464− opiETag :: ETag,
465− opiSize :: Int64,
466− opiModTime :: UTCTime
467− }
468− deriving stock (Show, Eq)
196+data ObjectPartInfo = ObjectPartInfo {
197+ opiNumber :: PartNumber
198+ , opiETag :: ETag
199+ , opiSize :: Int64
200+ , opiModTime :: UTCTime
201+ } deriving (Show, Eq)
469202
470203-- | Represents result from a listing of incomplete uploads to a
471204-- 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)
205+data ListUploadsResult = ListUploadsResult {
206+ lurHasMore :: Bool
207+ , lurNextKey :: Maybe Text
208+ , lurNextUpload :: Maybe Text
209+ , lurUploads :: [(Object, UploadId, UTCTime)]
210+ , lurCPrefixes :: [Text]
211+ } deriving (Show, Eq)
480212
481213-- | 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)
214+data UploadInfo = UploadInfo {
215+ uiKey :: Object
216+ , uiUploadId :: UploadId
217+ , uiInitTime :: UTCTime
218+ , uiSize :: Int64
219+ } deriving (Show, Eq)
489220
490221-- | 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)
222+data ListObjectsResult = ListObjectsResult {
223+ lorHasMore :: Bool
224+ , lorNextToken :: Maybe Text
225+ , lorObjects :: [ObjectInfo]
226+ , lorCPrefixes :: [Text]
227+ } deriving (Show, Eq)
498228
499229-- | 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)
230+data ListObjectsV1Result = ListObjectsV1Result {
231+ lorHasMore' :: Bool
232+ , lorNextMarker :: Maybe Text
233+ , lorObjects' :: [ObjectInfo]
234+ , lorCPrefixes' :: [Text]
235+ } deriving (Show, Eq)
507236
508237-- | Represents information about an object.
509−data ObjectInfo = ObjectInfo
510− { -- | Object key
511− oiObject :: Object,
512− -- | Modification time of the object
513− oiModTime :: UTCTime,
514− -- | ETag of the object
515− oiETag :: ETag,
516− -- | Size of the object in bytes
517− oiSize :: Int64,
518− -- | A map of user-metadata
519− -- pairs stored with an
520− -- object (keys will not
521− -- have the @X-Amz-Meta-@
522− -- prefix).
523− oiUserMetadata :: H.HashMap Text Text,
524− -- | A map of metadata
525− -- key-value pairs (not
526− -- including the
527− -- user-metadata pairs)
528− oiMetadata :: H.HashMap Text Text
529− }
530− deriving stock (Show, Eq)
531−
532−-- | Represents source object in server-side copy object
533−data SourceInfo = SourceInfo
534− { -- | Bucket containing the source object
535− srcBucket :: Text,
536− -- | Source object key
537− srcObject :: Text,
538− -- | Source object
539− -- byte-range
540− -- (inclusive)
541− srcRange :: Maybe (Int64, Int64),
542− -- | ETag condition on source -
543− -- object is copied only if the
544− -- source object's ETag matches
545− -- this value.
546− srcIfMatch :: Maybe Text,
547− -- | ETag not match condition
548− -- on source - object is copied
549− -- if ETag does not match this
550− -- value.
551− srcIfNoneMatch :: Maybe Text,
552− -- | Copy source object only
553− -- if the source has been
554− -- modified since this time.
555− srcIfModifiedSince :: Maybe UTCTime,
556− -- | Copy source object only
557− -- if the source has been
558− -- un-modified since this
559− -- given time.
560− srcIfUnmodifiedSince :: Maybe UTCTime
561− }
562− deriving stock (Show, Eq)
563−
564−-- | Provide a default for `SourceInfo`
565−defaultSourceInfo :: SourceInfo
566−defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing
567−
568−-- | Represents destination object in server-side copy object
569−data DestinationInfo = DestinationInfo
570− { -- | Destination bucket
571− dstBucket :: Text,
572− -- | Destination object key
573− dstObject :: Text
574− }
575− deriving stock (Show, Eq)
576−
577−-- | Provide a default for `DestinationInfo`
578−defaultDestinationInfo :: DestinationInfo
579−defaultDestinationInfo = DestinationInfo "" ""
580−
581−-- | Data type for options when getting an object from the
582−-- service. Start with the empty `defaultGetObjectOptions` and modify
583−-- it using the goo* functions.
584−data GetObjectOptions = GetObjectOptions
585− { -- | Set object's data of given offset begin and end,
586− -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
587− gooRange :: Maybe ByteRange,
588− -- | Set matching ETag condition, GetObject which matches the following
589− -- ETag.
590− gooIfMatch :: Maybe ETag,
591− -- | Set matching ETag none condition, GetObject which does not match
592− -- the following ETag.
593− gooIfNoneMatch :: Maybe ETag,
594− -- | Set object unmodified condition, GetObject unmodified since given time.
595− gooIfUnmodifiedSince :: Maybe UTCTime,
596− -- | Set object modified condition, GetObject modified since given time.
597− gooIfModifiedSince :: Maybe UTCTime,
598− -- | Specify SSE-C key
599− gooSSECKey :: Maybe SSECKey
600− }
601−
602−-- | Provide default `GetObjectOptions`.
603−defaultGetObjectOptions :: GetObjectOptions
604−defaultGetObjectOptions =
605− GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
606−
607−gooToHeaders :: GetObjectOptions -> [HT.Header]
608−gooToHeaders goo =
609− rangeHdr
610− ++ zip names values
611− ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
238+data ObjectInfo = ObjectInfo {
239+ oiObject :: Object
240+ , oiModTime :: UTCTime
241+ , oiETag :: ETag
242+ , oiSize :: Int64
243+ } deriving (Show, Eq)
244+
245+data CopyPartSource = CopyPartSource {
246+ -- | formatted like "\/sourceBucket\/sourceObject"
247+ cpSource :: Text
248+ -- | (0, 9) means first ten bytes of the source object
249+ , cpSourceRange :: Maybe (Int64, Int64)
250+ , cpSourceIfMatch :: Maybe Text
251+ , cpSourceIfNoneMatch :: Maybe Text
252+ , cpSourceIfUnmodifiedSince :: Maybe UTCTime
253+ , cpSourceIfModifiedSince :: Maybe UTCTime
254+ } deriving (Show, Eq)
255+
256+instance Default CopyPartSource where
257+ def = CopyPartSource "" def def def def def
258+
259+cpsToHeaders :: CopyPartSource -> [HT.Header]
260+cpsToHeaders cps = ("x-amz-copy-source", encodeUtf8 $ cpSource cps) :
261+ rangeHdr ++ zip names values
612262 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
630−
631−-- | Data type returned by 'getObject' representing the object being
632−-- retrieved. Use the @gor*@ functions to access its contents.
633−data GetObjectResponse = GetObjectResponse
634− { -- | ObjectInfo of the object being retrieved.
635− gorObjectInfo :: ObjectInfo,
636− -- | A conduit of the bytes of the object.
637− gorObjectStream :: C.ConduitM () ByteString Minio ()
638− }
263+ names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",
264+ "x-amz-copy-source-if-unmodified-since",
265+ "x-amz-copy-source-if-modified-since"]
266+ values = mapMaybe (fmap encodeUtf8 . (cps &))
267+ [cpSourceIfMatch, cpSourceIfNoneMatch,
268+ fmap formatRFC1123 . cpSourceIfUnmodifiedSince,
269+ fmap formatRFC1123 . cpSourceIfModifiedSince]
270+ rangeHdr = ("x-amz-copy-source-range",)
271+ . HT.renderByteRanges
272+ . (:[])
273+ . uncurry HT.ByteRangeFromTo
274+ <$> map (both fromIntegral) (maybeToList $ cpSourceRange cps)
275+
276+-- | Extract the source bucket and source object name. TODO: validate
277+-- the bucket and object name extracted.
278+cpsToObject :: CopyPartSource -> Maybe (Bucket, Object)
279+cpsToObject cps = do
280+ [_, bucket, object] <- Just splits
281+ return (bucket, object)
282+ where
283+ splits = T.splitOn "/" $ cpSource cps
639284
640285-- | A data-type for events that can occur in the object storage
641286-- server. Reference:
642287-- 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"
288+data Event = ObjectCreated
289+ | ObjectCreatedPut
290+ | ObjectCreatedPost
291+ | ObjectCreatedCopy
292+ | ObjectCreatedMultipartUpload
293+ | ObjectRemoved
294+ | ObjectRemovedDelete
295+ | ObjectRemovedDeleteMarkerCreated
296+ | ReducedRedundancyLostObject
297+ deriving (Eq)
298+
299+instance Show Event where
300+ show ObjectCreated = "s3:ObjectCreated:*"
301+ show ObjectCreatedPut = "s3:ObjectCreated:Put"
302+ show ObjectCreatedPost = "s3:ObjectCreated:Post"
303+ show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
304+ show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
305+ show ObjectRemoved = "s3:ObjectRemoved:*"
306+ show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
307+ show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
308+ show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
665309
666310textToEvent :: Text -> Maybe Event
667311textToEvent 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
312+ "s3:ObjectCreated:*" -> Just ObjectCreated
313+ "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
314+ "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
315+ "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
316+ "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
317+ "s3:ObjectRemoved:*" -> Just ObjectRemoved
318+ "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
675319 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
676− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
677− _ -> Nothing
320+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
321+ _ -> Nothing
322+
678323
679−-- | Filter data type - part of notification configuration
680−newtype Filter = Filter
324+data Filter = Filter
681325 { fFilter :: FilterKey
682− }
683− deriving stock (Show, Eq)
326+ } deriving (Show, Eq)
684327
685−-- | defaultFilter is empty, used to create a notification
686−-- configuration.
687−defaultFilter :: Filter
688−defaultFilter = Filter defaultFilterKey
328+instance Default Filter where
329+ def = Filter def
689330
690−-- | FilterKey contains FilterRules, and is part of a Filter.
691−newtype FilterKey = FilterKey
331+data FilterKey = FilterKey
692332 { fkKey :: FilterRules
693− }
694− deriving stock (Show, Eq)
333+ } deriving (Show, Eq)
695334
696−-- | defaultFilterKey is empty, used to create notification
697−-- configuration.
698−defaultFilterKey :: FilterKey
699−defaultFilterKey = FilterKey defaultFilterRules
335+instance Default FilterKey where
336+ def = FilterKey def
700337
701−-- | FilterRules represents a collection of `FilterRule`s.
702−newtype FilterRules = FilterRules
338+data FilterRules = FilterRules
703339 { frFilterRules :: [FilterRule]
704− }
705− deriving stock (Show, Eq)
340+ } deriving (Show, Eq)
706341
707−-- | defaultFilterRules is empty, used to create notification
708−-- configuration.
709−defaultFilterRules :: FilterRules
710−defaultFilterRules = FilterRules []
342+instance Default FilterRules where
343+ def = FilterRules []
711344
712345-- | A filter rule that can act based on the suffix or prefix of an
713346-- object. As an example, let's create two filter rules:
@@ -715,28 +348,25 @@ defaultFilterRules = FilterRules []
715348-- > let suffixRule = FilterRule "suffix" ".jpg"
716349-- > let prefixRule = FilterRule "prefix" "images/"
717350--
718−-- The @suffixRule@ restricts the notification to be triggered only
719−-- for objects having a suffix of ".jpg", and the @prefixRule@
351+-- The `suffixRule` restricts the notification to be triggered only
352+-- for objects having a suffix of ".jpg", and the `prefixRule`
720353-- restricts it to objects having a prefix of "images/".
721354data FilterRule = FilterRule
722− { frName :: Text,
723− frValue :: Text
724− }
725− deriving stock (Show, Eq)
355+ { frName :: Text
356+ , frValue :: Text
357+ } deriving (Show, Eq)
726358
727−-- | Arn is an alias of Text
728359type Arn = Text
729360
730361-- | A data-type representing the configuration for a particular
731362-- notification system. It could represent a Queue, Topic or Lambda
732363-- Function configuration.
733364data NotificationConfig = NotificationConfig
734− { ncId :: Text,
735− ncArn :: Arn,
736− ncEvents :: [Event],
737− ncFilter :: Filter
738− }
739− deriving stock (Show, Eq)
365+ { ncId :: Text
366+ , ncArn :: Arn
367+ , ncEvents :: [Event]
368+ , ncFilter :: Filter
369+ } deriving (Show, Eq)
740370
741371-- | A data-type to represent bucket notification configuration. It is
742372-- a collection of queue, topic or lambda function configurations. The
@@ -744,448 +374,110 @@ data NotificationConfig = NotificationConfig
744374-- described at
745375-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
746376data Notification = Notification
747− { nQueueConfigurations :: [NotificationConfig],
748− nTopicConfigurations :: [NotificationConfig],
749− nCloudFunctionConfigurations :: [NotificationConfig]
750− }
751− deriving stock (Show, Eq)
752−
753−-- | The default notification configuration is empty.
754−defaultNotification :: Notification
755−defaultNotification = Notification [] [] []
756−
757−--------------------------------------------------------------------------
758−-- Select API Related Types
759−--------------------------------------------------------------------------
760−
761−-- | SelectRequest represents the Select API call. Use the
762−-- `selectRequest` function to create a value of this type.
763−data SelectRequest = SelectRequest
764− { srExpression :: Text,
765− srExpressionType :: ExpressionType,
766− srInputSerialization :: InputSerialization,
767− srOutputSerialization :: OutputSerialization,
768− srRequestProgressEnabled :: Maybe Bool
769− }
770− deriving stock (Show, Eq)
771−
772−data ExpressionType = SQL
773− deriving stock (Show, Eq)
774−
775−-- | InputSerialization represents format information of the input
776−-- object being queried. Use one of the smart constructors such as
777−-- `defaultCsvInput` as a starting value, and add compression info
778−-- using `setInputCompressionType`
779−data InputSerialization = InputSerialization
780− { isCompressionType :: Maybe CompressionType,
781− isFormatInfo :: InputFormatInfo
782− }
783− deriving stock (Show, Eq)
784−
785−-- | Data type representing the compression setting in a Select
786−-- request.
787−data CompressionType
788− = CompressionTypeNone
789− | CompressionTypeGzip
790− | CompressionTypeBzip2
791− deriving stock (Show, Eq)
792−
793−-- | Data type representing input object format information in a
794−-- Select request.
795−data InputFormatInfo
796− = InputFormatCSV CSVInputProp
797− | InputFormatJSON JSONInputProp
798− | InputFormatParquet
799− deriving stock (Show, Eq)
800−
801−-- | defaultCsvInput returns InputSerialization with default CSV
802−-- format, and without any compression setting.
803−defaultCsvInput :: InputSerialization
804−defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
805−
806−-- | linesJsonInput returns InputSerialization with JSON line based
807−-- format with no compression setting.
808−linesJsonInput :: InputSerialization
809−linesJsonInput =
810− InputSerialization
811− Nothing
812− (InputFormatJSON $ JSONInputProp JSONTypeLines)
813−
814−-- | documentJsonInput returns InputSerialization with JSON document
815−-- based format with no compression setting.
816−documentJsonInput :: InputSerialization
817−documentJsonInput =
818− InputSerialization
819− Nothing
820− (InputFormatJSON $ JSONInputProp JSONTypeDocument)
821−
822−-- | defaultParquetInput returns InputSerialization with Parquet
823−-- format, and no compression setting.
824−defaultParquetInput :: InputSerialization
825−defaultParquetInput = InputSerialization Nothing InputFormatParquet
826−
827−-- | setInputCompressionType sets the compression type for the input
828−-- of the SelectRequest
829−setInputCompressionType ::
830− CompressionType ->
831− SelectRequest ->
832− SelectRequest
833−setInputCompressionType c i =
834− let is = srInputSerialization i
835− is' = is {isCompressionType = Just c}
836− in i {srInputSerialization = is'}
837−
838−-- | defaultCsvOutput returns OutputSerialization with default CSV
839−-- format.
840−defaultCsvOutput :: OutputSerialization
841−defaultCsvOutput = OutputSerializationCSV defaultCSVProp
842−
843−-- | defaultJsonInput returns OutputSerialization with default JSON
844−-- format.
845−defaultJsonOutput :: OutputSerialization
846−defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
847−
848−-- | selectRequest is used to build a `SelectRequest`
849−-- value. @selectRequest query inputSer outputSer@ represents a
850−-- SelectRequest with the SQL query text given by @query@, the input
851−-- serialization settings (compression format and format information)
852−-- @inputSer@ and the output serialization settings @outputSer@.
853−selectRequest ::
854− Text ->
855− InputSerialization ->
856− OutputSerialization ->
857− SelectRequest
858−selectRequest sqlQuery inputSer outputSer =
859− SelectRequest
860− { srExpression = sqlQuery,
861− srExpressionType = SQL,
862− srInputSerialization = inputSer,
863− srOutputSerialization = outputSer,
864− srRequestProgressEnabled = Nothing
865− }
866−
867−-- | setRequestProgressEnabled sets the flag for turning on progress
868−-- messages when the Select response is being streamed back to the
869−-- client.
870−setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
871−setRequestProgressEnabled enabled sr =
872− sr {srRequestProgressEnabled = Just enabled}
873−
874−type CSVInputProp = CSVProp
875−
876−-- | CSVProp represents CSV format properties. It is built up using
877−-- the Monoid instance.
878−newtype CSVProp = CSVProp (H.HashMap Text Text)
879− deriving stock (Show, Eq)
880−
881−instance Semigroup CSVProp where
882− (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
883−
884−instance Monoid CSVProp where
885− mempty = CSVProp mempty
886−
887−csvPropsList :: CSVProp -> [(Text, Text)]
888−csvPropsList (CSVProp h) = sort $ H.toList h
889−
890−defaultCSVProp :: CSVProp
891−defaultCSVProp = mempty
892−
893−-- | Specify the CSV record delimiter property.
894−recordDelimiter :: Text -> CSVProp
895−recordDelimiter = CSVProp . H.singleton "RecordDelimiter"
896−
897−-- | Specify the CSV field delimiter property.
898−fieldDelimiter :: Text -> CSVProp
899−fieldDelimiter = CSVProp . H.singleton "FieldDelimiter"
900−
901−-- | Specify the CSV quote character property.
902−quoteCharacter :: Text -> CSVProp
903−quoteCharacter = CSVProp . H.singleton "QuoteCharacter"
904−
905−-- | Specify the CSV quote-escape character property.
906−quoteEscapeCharacter :: Text -> CSVProp
907−quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
908−
909−-- | FileHeaderInfo specifies information about column headers for CSV
910−-- format.
911−data FileHeaderInfo
912− = -- | No column headers are present
913− FileHeaderNone
914− | -- | Headers are present and they should be used
915− FileHeaderUse
916− | -- | Header are present, but should be ignored
917− FileHeaderIgnore
918− deriving stock (Show, Eq)
919−
920−-- | Specify the CSV file header info property.
921−fileHeaderInfo :: FileHeaderInfo -> CSVProp
922−fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toStr
923− where
924− toStr FileHeaderNone = "NONE"
925− toStr FileHeaderUse = "USE"
926− toStr FileHeaderIgnore = "IGNORE"
927−
928−-- | Specify the CSV comment character property. Lines starting with
929−-- this character are ignored by the server.
930−commentCharacter :: Text -> CSVProp
931−commentCharacter = CSVProp . H.singleton "Comments"
377+ { nQueueConfigurations :: [NotificationConfig]
378+ , nTopicConfigurations :: [NotificationConfig]
379+ , nCloudFunctionConfigurations :: [NotificationConfig]
380+ } deriving (Eq, Show)
932381
933−-- | Allow quoted record delimiters inside a row using this property.
934−allowQuotedRecordDelimiter :: CSVProp
935−allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter" "TRUE"
936−
937−-- | Set the CSV format properties in the InputSerialization.
938−setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
939−setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
940−
941−-- | Set the CSV format properties in the OutputSerialization.
942−outputCSVFromProps :: CSVProp -> OutputSerialization
943−outputCSVFromProps = OutputSerializationCSV
944−
945−newtype JSONInputProp = JSONInputProp {jsonipType :: JSONType}
946− deriving stock (Show, Eq)
947−
948−data JSONType = JSONTypeDocument | JSONTypeLines
949− deriving stock (Show, Eq)
950−
951−-- | OutputSerialization represents output serialization settings for
952−-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
953−-- a starting point.
954−data OutputSerialization
955− = OutputSerializationJSON JSONOutputProp
956− | OutputSerializationCSV CSVOutputProp
957− deriving stock (Show, Eq)
958−
959−type CSVOutputProp = CSVProp
960−
961−-- | quoteFields is an output serialization parameter
962−quoteFields :: QuoteFields -> CSVProp
963−quoteFields q = CSVProp $
964− H.singleton "QuoteFields" $
965− case q of
966− QuoteFieldsAsNeeded -> "ASNEEDED"
967− QuoteFieldsAlways -> "ALWAYS"
968−
969−-- | Represent the QuoteField setting.
970−data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
971− deriving stock (Show, Eq)
972−
973−newtype JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
974− deriving stock (Show, Eq)
975−
976−-- | Set the output record delimiter for JSON format
977−outputJSONFromRecordDelimiter :: Text -> OutputSerialization
978−outputJSONFromRecordDelimiter t =
979− OutputSerializationJSON (JSONOutputProp $ Just t)
980−
981−-- Response related types
982−
983−-- | An EventMessage represents each kind of message received from the server.
984−data EventMessage
985− = ProgressEventMessage Progress
986− | StatsEventMessage Stats
987− | RequestLevelErrorMessage
988− Text
989− -- ^ Error code
990− Text
991− -- ^ Error message
992− | RecordPayloadEventMessage ByteString
993− deriving stock (Show, Eq)
994−
995−data MsgHeaderName
996− = MessageType
997− | EventType
998− | ContentType
999− | ErrorCode
1000− | ErrorMessage
1001− deriving stock (Show, Eq)
1002−
1003−msgHeaderValueType :: Word8
1004−msgHeaderValueType = 7
1005−
1006−type MessageHeader = (MsgHeaderName, Text)
1007−
1008−-- | Represent the progress event returned in the Select response.
1009−data Progress = Progress
1010− { pBytesScanned :: Int64,
1011− pBytesProcessed :: Int64,
1012− pBytesReturned :: Int64
1013− }
1014− deriving stock (Show, Eq)
1015−
1016−-- | Represent the stats event returned at the end of the Select
1017−-- response.
1018−type Stats = Progress
1019−
1020−--------------------------------------------------------------------------
1021−-- Select API Related Types End
1022−--------------------------------------------------------------------------
382+instance Default Notification where
383+ def = Notification [] [] []
1023384
1024385-- | Represents different kinds of payload that are used with S3 API
1025386-- requests.
1026−data Payload
1027− = PayloadBS ByteString
1028− | PayloadH Handle Int64 Int64 -- file handle, offset and length
1029− | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
1030−
1031−defaultPayload :: Payload
1032−defaultPayload = PayloadBS ""
1033−
1034−data AdminReqInfo = AdminReqInfo
1035− { ariMethod :: Method,
1036− ariPayloadHash :: Maybe ByteString,
1037− ariPayload :: Payload,
1038− ariPath :: ByteString,
1039− ariHeaders :: [Header],
1040− ariQueryParams :: Query
387+data Payload = PayloadBS ByteString
388+ | PayloadH Handle
389+ Int64 -- offset
390+ Int64 -- size
391+
392+instance Default Payload where
393+ def = PayloadBS ""
394+
395+data RequestInfo = RequestInfo {
396+ riMethod :: Method
397+ , riBucket :: Maybe Bucket
398+ , riObject :: Maybe Object
399+ , riQueryParams :: Query
400+ , riHeaders :: [Header]
401+ , riPayload :: Payload
402+ , riPayloadHash :: Maybe ByteString
403+ , riRegion :: Maybe Region
404+ , riNeedsLocation :: Bool
1041405 }
1042406
1043−data S3ReqInfo = S3ReqInfo
1044− { riMethod :: Method,
1045− riBucket :: Maybe Bucket,
1046− riObject :: Maybe Object,
1047− riQueryParams :: Query,
1048− riHeaders :: [Header],
1049− riPayload :: Payload,
1050− riPayloadHash :: Maybe ByteString,
1051− riRegion :: Maybe Region,
1052− riNeedsLocation :: Bool,
1053− riPresignExpirySecs :: Maybe UrlExpiry
1054− }
407+instance Default RequestInfo where
408+ def = RequestInfo HT.methodGet def def def def def Nothing def True
1055409
1056−defaultS3ReqInfo :: S3ReqInfo
1057−defaultS3ReqInfo =
1058− S3ReqInfo
1059− HT.methodGet
1060− Nothing
1061− Nothing
1062− []
1063− []
1064− defaultPayload
1065− Nothing
1066− Nothing
1067− True
1068− Nothing
410+getPathFromRI :: RequestInfo -> ByteString
411+getPathFromRI ri =
412+ let
413+ b = riBucket ri
414+ o = riObject ri
415+ segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
416+ in
417+ B.concat ["/", B.intercalate "/" segments]
1069418
1070−getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
1071−getS3Path b o =
1072− let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
1073− in B.concat ["/", B.intercalate "/" segments]
419+-- | Time to expire for a presigned URL. It interpreted as a number of
420+-- seconds. The maximum duration that can be specified is 7 days.
421+type UrlExpiry = Int
1074422
1075−type RegionMap = H.HashMap Bucket Region
423+type RegionMap = Map.Map Bucket Region
1076424
1077−-- | The Minio Monad - all computations accessing object storage
1078−-- happens in it.
1079−newtype Minio a = Minio
1080− { unMinio :: ReaderT MinioConn (ResourceT IO) a
425+newtype Minio a = Minio {
426+ unMinio :: ReaderT MinioConn (StateT RegionMap (ResourceT IO)) a
1081427 }
1082− deriving newtype
1083− ( Functor,
1084− Applicative,
1085− Monad,
1086− MonadIO,
1087− MonadReader MinioConn,
1088− MonadResource,
1089− MonadUnliftIO
428+ deriving (
429+ Functor
430+ , Applicative
431+ , Monad
432+ , MonadIO
433+ , MonadReader MinioConn
434+ , MonadState RegionMap
435+ , MonadThrow
436+ , MonadCatch
437+ , MonadBase IO
438+ , MonadResource
1090439 )
1091440
1092−-- | MinioConn holds connection info and a connection pool to allow
1093−-- for efficient resource re-use.
1094−data MinioConn = MinioConn
1095− { mcConnInfo :: ConnectInfo,
1096− mcConnManager :: NC.Manager,
1097− mcRegionMap :: MVar RegionMap
1098− }
1099−
1100−class HasSvcNamespace env where
1101− getSvcNamespace :: env -> Text
441+instance MonadBaseControl IO Minio where
442+ type StM Minio a = (a, RegionMap)
443+ liftBaseWith f = Minio $ liftBaseWith $ \q -> f (q . unMinio)
444+ restoreM = Minio . restoreM
1102445
1103−instance HasSvcNamespace MinioConn where
1104− getSvcNamespace env =
1105− let host = connectHost $ mcConnInfo env
1106− in ( if host == "storage.googleapis.com"
1107− then "http://doc.s3.amazonaws.com/2006-03-01"
1108− else "http://s3.amazonaws.com/doc/2006-03-01/"
1109− )
446+-- | MinioConn holds connection info and a connection pool
447+data MinioConn = MinioConn {
448+ mcConnInfo :: ConnectInfo
449+ , mcConnManager :: NC.Manager
450+ }
1110451
1111452-- | Takes connection information and returns a connection object to
1112−-- be passed to 'runMinio'. The returned value can be kept in the
1113−-- application environment and passed to `runMinioWith` whenever
1114−-- object storage is accessed.
453+-- be passed to 'runMinio'
1115454connect :: ConnectInfo -> IO MinioConn
1116455connect 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
456+ let settings = bool defaultManagerSettings NC.tlsManagerSettings $
457+ connectIsSecure ci
1127458 mgr <- NC.newManager settings
1128− mkMinioConn ci mgr
1129−
1130−-- | Run the computation accessing object storage using the given
1131−-- `MinioConn`. This reuses connections, but otherwise it is similar
1132−-- to `runMinio`.
1133−runMinioWith :: MinioConn -> Minio a -> IO (Either MinioErr a)
1134−runMinioWith conn m = runResourceT $ runMinioResWith conn m
1135−
1136−-- | Given `ConnectInfo` and a HTTP connection manager, create a
1137−-- `MinioConn`.
1138−mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
1139−mkMinioConn ci mgr = do
1140− rMapMVar <- M.newMVar H.empty
1141− return $ MinioConn ci mgr rMapMVar
459+ return $ MinioConn ci mgr
1142460
1143461-- | Run the Minio action and return the result or an error.
1144462runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
1145463runMinio ci m = do
1146− conn <- connect ci
1147− runResourceT $ runMinioResWith conn m
1148−
1149−-- | Similar to 'runMinioWith'. Allows applications to allocate/release
1150−-- its resources along side MinIO's internal resources.
1151−runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)
1152−runMinioResWith conn m =
1153− flip runReaderT conn . unMinio $
1154− fmap Right m
1155− `U.catches` [ U.Handler handlerServiceErr,
1156− U.Handler handlerHE,
1157− U.Handler handlerFE,
1158− U.Handler handlerValidation
1159− ]
464+ conn <- liftIO $ connect ci
465+ runResourceT . flip evalStateT Map.empty . flip runReaderT conn . unMinio $
466+ fmap Right m `MC.catches`
467+ [ MC.Handler handlerServiceErr
468+ , MC.Handler handlerHE
469+ , MC.Handler handlerFE
470+ , MC.Handler handlerValidation
471+ ]
1160472 where
1161473 handlerServiceErr = return . Left . MErrService
1162474 handlerHE = return . Left . MErrHTTP
1163475 handlerFE = return . Left . MErrIO
1164476 handlerValidation = return . Left . MErrValidation
1165477
1166−-- | Similar to 'runMinio'. Allows applications to allocate/release
1167−-- its resources along side MinIO's internal resources.
1168−runMinioRes :: ConnectInfo -> Minio a -> ResourceT IO (Either MinioErr a)
1169−runMinioRes ci m = do
1170− conn <- liftIO $ connect ci
1171− runMinioResWith conn m
478+s3Name :: Text -> Name
479+s3Name s = Name s (Just "http://s3.amazonaws.com/doc/2006-03-01/") Nothing
1172480
1173481-- | Format as per RFC 1123.
1174482formatRFC1123 :: UTCTime -> T.Text
1175483formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1176−
1177−lookupRegionCache :: Bucket -> Minio (Maybe Region)
1178−lookupRegionCache b = do
1179− rMVar <- asks mcRegionMap
1180− rMap <- UM.readMVar rMVar
1181− return $ H.lookup b rMap
1182−
1183−addToRegionCache :: Bucket -> Region -> Minio ()
1184−addToRegionCache b region = do
1185− rMVar <- asks mcRegionMap
1186− UM.modifyMVar_ rMVar $ return . H.insert b region
1187−
1188−deleteFromRegionCache :: Bucket -> Minio ()
1189−deleteFromRegionCache b = do
1190− rMVar <- asks mcRegionMap
1191− UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/Data/ByteString.hs+19−20
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,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+28−39
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,54 +15,50 @@
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
18+ (
19+ hashSHA256
20+ , hashSHA256FromSource
3021
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)
22+ , hashMD5
23+ , hashMD5FromSource
24+
25+ , hmacSHA256
26+ , hmacSHA256RawBS
27+ , digestToBS
28+ , digestToBase16
29+ ) where
30+
31+import Crypto.Hash (SHA256(..), MD5(..), hashWith, Digest)
32+import Crypto.Hash.Conduit (sinkHash)
33+import Crypto.MAC.HMAC (hmac, HMAC)
34+import Data.ByteArray (ByteArrayAccess, convert)
35+import Data.ByteArray.Encoding (convertToBase, Base(Base16))
4136import qualified Data.Conduit as C
4237
38+import Lib.Prelude
39+
4340hashSHA256 :: ByteString -> ByteString
4441hashSHA256 = digestToBase16 . hashWith SHA256
4542
46−hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
43+hashSHA256FromSource :: Monad m => C.Producer m ByteString -> m ByteString
4744hashSHA256FromSource src = do
48− digest <- C.connect src sinkSHA256Hash
45+ digest <- src C.$$ sinkSHA256Hash
4946 return $ digestToBase16 digest
5047 where
5148 -- To help with type inference
52− sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
49+ sinkSHA256Hash :: Monad m => C.Consumer ByteString m (Digest SHA256)
5350 sinkSHA256Hash = sinkHash
5451
55−-- Returns MD5 hash hex encoded.
5652hashMD5 :: ByteString -> ByteString
5753hashMD5 = digestToBase16 . hashWith MD5
5854
59−hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
55+hashMD5FromSource :: Monad m => C.Producer m ByteString -> m ByteString
6056hashMD5FromSource src = do
61− digest <- C.connect src sinkMD5Hash
57+ digest <- src C.$$ sinkMD5Hash
6258 return $ digestToBase16 digest
6359 where
6460 -- To help with type inference
65− sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
61+ sinkMD5Hash :: Monad m => C.Consumer ByteString m (Digest MD5)
6662 sinkMD5Hash = sinkHash
6763
6864hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -71,15 +67,8 @@ hmacSHA256 message key = hmac key message
7167hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
7268hmacSHA256RawBS message key = convert $ hmacSHA256 message key
7369
74−digestToBS :: (ByteArrayAccess a) => a -> ByteString
70+digestToBS :: ByteArrayAccess a => a -> ByteString
7571digestToBS = convert
7672
77−digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
73+digestToBase16 :: ByteArrayAccess a => a -> ByteString
7874digestToBase16 = convertToBase Base16
79−
80−-- Returns MD5 hash base 64 encoded.
81−hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
82−hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
83−
84−encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
85−encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+14−18
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,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+45−59
@@ -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.
@@ -14,83 +14,69 @@
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+ deriving (Show, Eq)
5442
5543instance Exception MErrV
5644
5745-- | 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)
46+data ServiceErr = BucketAlreadyExists
47+ | BucketAlreadyOwnedByYou
48+ | NoSuchBucket
49+ | InvalidBucketName
50+ | NoSuchKey
51+ | ServiceErr Text Text
52+ deriving (Show, Eq)
6753
6854instance Exception ServiceErr
6955
7056toServiceErr :: Text -> Text -> ServiceErr
71−toServiceErr "NoSuchKey" _ = NoSuchKey
72−toServiceErr "NoSuchBucket" _ = NoSuchBucket
73−toServiceErr "InvalidBucketName" _ = InvalidBucketName
57+toServiceErr "NoSuchKey" _ = NoSuchKey
58+toServiceErr "NoSuchBucket" _ = NoSuchBucket
59+toServiceErr "InvalidBucketName" _ = InvalidBucketName
7460toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
75−toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
76−toServiceErr code message = ServiceErr code message
61+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
62+toServiceErr code message = ServiceErr code message
63+
7764
7865-- | 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)
66+data MinioErr = MErrHTTP NC.HttpException
67+ | MErrIO IOException
68+ | MErrService ServiceErr
69+ | MErrValidation MErrV
70+ deriving (Show)
8571
8672instance 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
73+ MErrHTTP _ == MErrHTTP _ = True
74+ MErrHTTP _ == _ = False
75+ MErrIO _ == MErrIO _ = True
76+ MErrIO _ == _ = False
77+ MErrService a == MErrService b = a == b
78+ MErrService _ == _ = False
79+ MErrValidation a == MErrValidation b = a == b
80+ MErrValidation _ == _ = False
9581
9682instance Exception MinioErr
src/Network/Minio/JsonParser.hsdeleted−49
@@ -1,49 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−module Network.Minio.JsonParser
18− ( parseErrResponseJSON,
19− )
20−where
21−
22−import Data.Aeson
23− ( FromJSON,
24− eitherDecode,
25− parseJSON,
26− withObject,
27− (.:),
28− )
29−import qualified Data.Text as T
30−import Lib.Prelude
31−import Network.Minio.Errors
32−
33−data AdminErrJSON = AdminErrJSON
34− { aeCode :: Text,
35− aeMessage :: Text
36− }
37− deriving stock (Eq, Show)
38−
39−instance FromJSON AdminErrJSON where
40− parseJSON = withObject "AdminErrJSON" $ \v ->
41− AdminErrJSON
42− <$> v .: "Code"
43− <*> v .: "Message"
44−
45−parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
46−parseErrResponseJSON jsondata =
47− case eitherDecode jsondata of
48− Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
49− Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs+38−120
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 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.
@@ -19,162 +19,80 @@ module Network.Minio.ListOps where
1919import qualified Data.Conduit as C
2020import qualified Data.Conduit.Combinators as CC
2121import qualified Data.Conduit.List as CL
22−import Network.Minio.Data
23− ( Bucket,
24− ListObjectsResult
25− ( lorCPrefixes,
26− lorHasMore,
27− lorNextToken,
28− lorObjects
29− ),
30− ListObjectsV1Result
31− ( lorCPrefixes',
32− lorHasMore',
33− lorNextMarker,
34− lorObjects'
35− ),
36− ListPartsResult (lprHasMore, lprNextPart, lprParts),
37− ListUploadsResult
38− ( lurHasMore,
39− lurNextKey,
40− lurNextUpload,
41− lurUploads
42− ),
43− Minio,
44− Object,
45− ObjectInfo,
46− ObjectPartInfo (opiSize),
47− UploadId,
48− UploadInfo (UploadInfo),
49− )
50−import Network.Minio.S3API
51− ( listIncompleteParts',
52− listIncompleteUploads',
53− listObjects',
54− listObjectsV1',
55− )
5622
57−-- | Represents a list output item - either an object or an object
58−-- prefix (i.e. a directory).
59−data ListItem
60− = ListItemObject ObjectInfo
61− | ListItemPrefix Text
62− deriving stock (Show, Eq)
23+import Lib.Prelude
6324
64−-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
65−-- similar to a file system tree traversal.
66−--
67−-- If @prefix@ is not 'Nothing', only items with the given prefix are
68−-- listed, otherwise items under the bucket are returned.
69−--
70−-- If @recurse@ is set to @True@ all directories under the prefix are
71−-- recursively traversed and only objects are returned.
72−--
73−-- If @recurse@ is set to @False@, objects and directories immediately
74−-- under the given prefix are returned (no recursive traversal is
75−-- performed).
76−listObjects :: Bucket -> Maybe Text -> Bool -> C.ConduitM () ListItem Minio ()
25+import Network.Minio.Data
26+import Network.Minio.S3API
27+
28+-- | List objects in a bucket matching the given prefix. If recurse is
29+-- set to True objects matching prefix are recursively listed.
30+listObjects :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
7731listObjects bucket prefix recurse = loop Nothing
7832 where
79− loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
33+ loop :: Maybe Text -> C.Producer Minio ObjectInfo
8034 loop nextToken = do
81− let delimiter = bool (Just "/") Nothing recurse
35+ let
36+ delimiter = bool (Just "/") Nothing recurse
8237
8338 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
84− CL.sourceList $ map ListItemObject $ lorObjects res
85− unless recurse $
86− CL.sourceList $
87− map ListItemPrefix $
88− lorCPrefixes res
39+ CL.sourceList $ lorObjects res
8940 when (lorHasMore res) $
9041 loop (lorNextToken res)
9142
92−-- | Lists objects - similar to @listObjects@, however uses the older
93−-- V1 AWS S3 API. Prefer @listObjects@ to this.
94−listObjectsV1 ::
95− Bucket ->
96− Maybe Text ->
97− Bool ->
98− C.ConduitM () ListItem Minio ()
43+-- | List objects in a bucket matching the given prefix. If recurse is
44+-- set to True objects matching prefix are recursively listed.
45+listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
9946listObjectsV1 bucket prefix recurse = loop Nothing
10047 where
101− loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
48+ loop :: Maybe Text -> C.Producer Minio ObjectInfo
10249 loop nextMarker = do
103− let delimiter = bool (Just "/") Nothing recurse
50+ let
51+ delimiter = bool (Just "/") Nothing recurse
10452
10553 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
106− CL.sourceList $ map ListItemObject $ lorObjects' res
107− unless recurse $
108− CL.sourceList $
109− map ListItemPrefix $
110− lorCPrefixes' res
54+ CL.sourceList $ lorObjects' res
11155 when (lorHasMore' res) $
11256 loop (lorNextMarker res)
11357
11458-- | List incomplete uploads in a bucket matching the given prefix. If
11559-- recurse is set to True incomplete uploads for the given prefix are
11660-- recursively listed.
117−listIncompleteUploads ::
118− Bucket ->
119− Maybe Text ->
120− Bool ->
121− C.ConduitM () UploadInfo Minio ()
61+listIncompleteUploads :: Bucket -> Maybe Text -> Bool
62+ -> C.Producer Minio UploadInfo
12263listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
12364 where
124− loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
65+ loop :: Maybe Text -> Maybe Text -> C.Producer Minio UploadInfo
12566 loop nextKeyMarker nextUploadIdMarker = do
126− let delimiter = bool (Just "/") Nothing recurse
67+ let
68+ delimiter = bool (Just "/") Nothing recurse
12769
128− res <-
129− lift $
130− listIncompleteUploads'
131− bucket
132− prefix
133− delimiter
134− nextKeyMarker
135− nextUploadIdMarker
136− Nothing
70+ res <- lift $ listIncompleteUploads' bucket prefix delimiter
71+ nextKeyMarker nextUploadIdMarker Nothing
13772
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
73+ aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do
74+ partInfos <- listIncompleteParts bucket uKey uId C.$$ CC.sinkList
75+ return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
14576
14677 CL.sourceList $
147− zipWith
148− ( curry
149− ( \((uKey, uId, uInitTime), size) ->
150− UploadInfo uKey uId uInitTime size
151− )
152− )
153− (lurUploads res)
154− aggrSizes
78+ map (\((uKey, uId, uInitTime), size) ->
79+ UploadInfo uKey uId uInitTime size
80+ ) $ zip (lurUploads res) aggrSizes
15581
15682 when (lurHasMore res) $
15783 loop (lurNextKey res) (lurNextUpload res)
15884
85+
15986-- | List object parts of an ongoing multipart upload for given
16087-- bucket, object and uploadId.
161−listIncompleteParts ::
162− Bucket ->
163− Object ->
164− UploadId ->
165− C.ConduitM () ObjectPartInfo Minio ()
88+listIncompleteParts :: Bucket -> Object -> UploadId
89+ -> C.Producer Minio ObjectPartInfo
16690listIncompleteParts bucket object uploadId = loop Nothing
16791 where
168− loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
92+ loop :: Maybe Text -> C.Producer Minio ObjectPartInfo
16993 loop nextPartMarker = do
170− res <-
171− lift $
172− listIncompleteParts'
173− bucket
174− object
175− uploadId
176− Nothing
177− nextPartMarker
94+ res <- lift $ listIncompleteParts' bucket object uploadId Nothing
95+ nextPartMarker
17896 CL.sourceList $ lprParts res
17997 when (lprHasMore res) $
18098 loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs+147−221
@@ -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 Data.Default (def)
43+import qualified Data.Map.Strict as Map
44+import qualified Data.Text as T
45+import qualified Data.Time as Time
46+import qualified Network.HTTP.Types as HT
47+import Network.HTTP.Types.Header (hHost)
48+
49+import Lib.Prelude
50+
51+import Network.Minio.Data
52+import Network.Minio.Data.Time
53+import Network.Minio.Errors
54+import Network.Minio.Sign.V4
6155
6256-- | Generate a presigned URL. This function allows for advanced usage
6357-- - for simple cases prefer the `presigned*Url` functions.
@@ -67,36 +61,35 @@ 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+ throwM $ MErrVInvalidUrlExpiry expiry
70+
71+ ci <- asks mcConnInfo
72+
73+ let
74+ host = formatBS "{}:{}" (connectHost ci, connectPort ci)
75+ hostHeader = (hHost, host)
76+ ri = def { riMethod = method
77+ , riBucket = bucket
78+ , riObject = object
79+ , riQueryParams = extraQuery
80+ , riHeaders = hostHeader : extraHeaders
81+ , riRegion = Just $ maybe (connectRegion ci) identity region
82+ }
83+
84+ signPairs <- liftIO $ signV4 ci ri (Just expiry)
85+
86+ let
87+ qpToAdd = (fmap . fmap) Just signPairs
88+ queryStr = HT.renderQueryBuilder True (riQueryParams ri ++ qpToAdd)
89+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
90+
91+ return $ toS $ toLazyByteString $
92+ scheme <> byteString host <> byteString (getPathFromRI ri) <> queryStr
10093
10194-- | Generate a URL with authentication signature to PUT (upload) an
10295-- object. Any extra headers if passed, are signed, and so they are
@@ -105,22 +98,11 @@ makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
10598--
10699-- For a list of possible headers to pass, please refer to the PUT
107100-- object REST API AWS S3 documentation.
108−presignedPutObjectUrl ::
109− Bucket ->
110− Object ->
111− UrlExpiry ->
112− HT.RequestHeaders ->
113− Minio ByteString
114−presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
115− region <- asks (Just . connectRegion . mcConnInfo)
116− makePresignedUrl
117− expirySeconds
118− HT.methodPut
119− (Just bucket)
120− (Just object)
121− region
122− []
123− extraHeaders
101+presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders
102+ -> Minio ByteString
103+presignedPutObjectUrl bucket object expirySeconds extraHeaders =
104+ makePresignedUrl expirySeconds HT.methodPut
105+ (Just bucket) (Just object) Nothing [] extraHeaders
124106
125107-- | Generate a URL with authentication signature to GET (download) an
126108-- object. All extra query parameters and headers passed here will be
@@ -131,23 +113,11 @@ presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
131113--
132114-- For a list of possible request parameters and headers, please refer
133115-- 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
141−presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
142− region <- asks (Just . connectRegion . mcConnInfo)
143− makePresignedUrl
144− expirySeconds
145− HT.methodGet
146− (Just bucket)
147− (Just object)
148− region
149− extraQuery
150− extraHeaders
116+presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query
117+ -> HT.RequestHeaders -> Minio ByteString
118+presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders =
119+ makePresignedUrl expirySeconds HT.methodGet
120+ (Just bucket) (Just object) Nothing extraQuery extraHeaders
151121
152122-- | Generate a URL with authentication signature to make a HEAD
153123-- request on an object. This is used to fetch metadata about an
@@ -156,74 +126,49 @@ presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
156126--
157127-- For a list of possible headers to pass, please refer to the HEAD
158128-- object REST API AWS S3 documentation.
159−presignedHeadObjectUrl ::
160− Bucket ->
161− Object ->
162− UrlExpiry ->
163− HT.RequestHeaders ->
164− Minio ByteString
165−presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
166− region <- asks (Just . connectRegion . mcConnInfo)
167− makePresignedUrl
168− expirySeconds
169− HT.methodHead
170− (Just bucket)
171− (Just object)
172− region
173− []
174− extraHeaders
129+presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry
130+ -> HT.RequestHeaders -> Minio ByteString
131+presignedHeadObjectUrl bucket object expirySeconds extraHeaders =
132+ makePresignedUrl expirySeconds HT.methodHead
133+ (Just bucket) (Just object) Nothing [] extraHeaders
175134
176135-- | 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)
136+data PostPolicyCondition = PPCStartsWith Text Text
137+ | PPCEquals Text Text
138+ | PPCRange Text Int64 Int64
139+ deriving (Show, Eq)
182140
183−{- ORMOLU_DISABLE -}
184141instance Json.ToJSON PostPolicyCondition where
185142 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
189143 toJSON (PPCEquals k v) = Json.object [k .= v]
190−#endif
191144 toJSON (PPCRange k minVal maxVal) =
192145 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
193146
194147 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
198148 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
199−#endif
200149 toEncoding (PPCRange k minVal maxVal) =
201150 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202−{- ORMOLU_ENABLE -}
203151
204152-- | 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)
153+data PostPolicy = PostPolicy {
154+ expiration :: UTCTime
155+ , conditions :: [PostPolicyCondition]
156+ } deriving (Show, Eq)
210157
211158instance Json.ToJSON PostPolicy where
212159 toJSON (PostPolicy e c) =
213− Json.object
214− [ "expiration" .= iso8601TimeFormat e,
215− "conditions" .= c
216− ]
160+ Json.object $ [ "expiration" .= iso8601TimeFormat e
161+ , "conditions" .= c
162+ ]
217163 toEncoding (PostPolicy e c) =
218164 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
219165
220166-- | Possible validation errors when creating a PostPolicy.
221−data PostPolicyError
222− = PPEKeyNotSpecified
223− | PPEBucketNotSpecified
224− | PPEConditionKeyEmpty
225− | PPERangeInvalid
226− deriving stock (Show, Eq)
167+data PostPolicyError = PPEKeyNotSpecified
168+ | PPEBucketNotSpecified
169+ | PPEConditionKeyEmpty
170+ | PPERangeInvalid
171+ deriving (Eq, Show)
227172
228173-- | Set the bucket name that the upload should use.
229174ppCondBucket :: Bucket -> PostPolicyCondition
@@ -231,10 +176,8 @@ ppCondBucket = PPCEquals "bucket"
231176
232177-- | Set the content length range constraint with minimum and maximum
233178-- byte count values.
234−ppCondContentLengthRange ::
235− Int64 ->
236− Int64 ->
237− PostPolicyCondition
179+ppCondContentLengthRange :: Int64 -> Int64
180+ -> PostPolicyCondition
238181ppCondContentLengthRange = PPCRange "content-length-range"
239182
240183-- | Set the content-type header for the upload.
@@ -257,99 +200,82 @@ ppCondSuccessActionStatus n =
257200
258201-- | This function creates a PostPolicy after validating its
259202-- arguments.
260−newPostPolicy ::
261− UTCTime ->
262− [PostPolicyCondition] ->
263− Either PostPolicyError PostPolicy
203+newPostPolicy :: UTCTime -> [PostPolicyCondition]
204+ -> Either PostPolicyError PostPolicy
264205newPostPolicy expirationTime conds
265206 -- object name condition must be present
266207 | not $ any (keyEquals "key") conds =
267208 Left PPEKeyNotSpecified
209+
268210 -- bucket name condition must be present
269211 | not $ any (keyEquals "bucket") conds =
270212 Left PPEBucketNotSpecified
213+
271214 -- a condition with an empty key is invalid
272215 | any (keyEquals "") conds || any isEmptyRangeKey conds =
273216 Left PPEConditionKeyEmpty
217+
274218 -- invalid range check
275219 | any isInvalidRange conds =
276220 Left PPERangeInvalid
221+
277222 -- all good!
278223 | otherwise =
279224 return $ PostPolicy expirationTime conds
225+
280226 where
281227 keyEquals k' (PPCStartsWith k _) = k == k'
282− keyEquals k' (PPCEquals k _) = k == k'
283− keyEquals _ _ = False
228+ keyEquals k' (PPCEquals k _) = k == k'
229+ keyEquals _ _ = False
230+
284231 isEmptyRangeKey (PPCRange k _ _) = k == ""
285− isEmptyRangeKey _ = False
232+ isEmptyRangeKey _ = False
233+
286234 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
287− isInvalidRange _ = False
235+ isInvalidRange _ = False
288236
289237-- | Convert Post Policy to a string (e.g. for printing).
290238showPostPolicy :: PostPolicy -> ByteString
291−showPostPolicy = toStrictBS . Json.encode
239+showPostPolicy = toS . Json.encode
292240
293241-- | Generate a presigned URL and POST policy to upload files via a
294242-- browser. On success, this function returns a URL and POST
295243-- form-data.
296−presignedPostPolicy ::
297− PostPolicy ->
298− Minio (ByteString, H.HashMap Text ByteString)
244+presignedPostPolicy :: PostPolicy
245+ -> Minio (ByteString, Map.Map Text ByteString)
299246presignedPostPolicy p = do
300247 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 "/"
248+ signTime <- liftIO $ Time.getCurrentTime
249+
250+ let
251+ extraConditions =
252+ [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime)
253+ , PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256"
254+ , PPCEquals "x-amz-credential"
255+ (T.intercalate "/" [connectAccessKey ci,
256+ decodeUtf8 $ mkScope signTime region])
257+ ]
258+ ppWithCreds = p {
259+ conditions = conditions p ++ extraConditions
260+ }
261+ signData = signV4PostPolicy (showPostPolicy ppWithCreds)
262+ signTime ci
263+
264+ -- compute form-data
265+ mkPair (PPCStartsWith k v) = Just (k, v)
266+ mkPair (PPCEquals k v) = Just (k, v)
267+ mkPair _ = Nothing
268+ formFromPolicy = Map.map toS $ Map.fromList $ catMaybes $
269+ mkPair <$> conditions ppWithCreds
270+ formData = formFromPolicy `Map.union` signData
271+
272+ -- compute POST upload URL
273+ bucket = Map.findWithDefault "" "bucket" formData
274+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
275+ host = formatBS "{}:{}" (connectHost ci, connectPort ci)
276+ region = connectRegion ci
277+
278+ url = toS $ toLazyByteString $ scheme <> byteString host <>
279+ byteString "/" <> byteString (toS bucket) <> byteString "/"
354280
355281 return (url, formData)
src/Network/Minio/PutObject.hs+160−102
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017-2019 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,24 +15,43 @@
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
18+ (
19+ putObjectInternal
20+ , ObjectData(..)
21+ , selectPartSizes
22+ , copyObjectInternal
23+ , selectCopyRanges
24+ , minPartSize
25+ ) where
26+
27+
28+import qualified Data.Conduit as C
2729import qualified Data.Conduit.Binary as CB
2830import qualified Data.Conduit.Combinators as CC
2931import qualified Data.Conduit.List as CL
3032import 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
33+
34+import Lib.Prelude
35+
36+import Network.Minio.Data
37+import Network.Minio.Errors
38+import Network.Minio.S3API
39+import Network.Minio.Utils
40+
41+
42+-- | max obj size is 5TiB
43+maxObjectSize :: Int64
44+maxObjectSize = 5 * 1024 * 1024 * oneMiB
45+
46+-- | minimum size of parts used in multipart operations.
47+minPartSize :: Int64
48+minPartSize = 64 * oneMiB
49+
50+oneMiB :: Int64
51+oneMiB = 1024 * 1024
52+
53+maxMultipartParts :: Int64
54+maxMultipartParts = 10000
3655
3756-- | A data-type to represent the source data for an object. A
3857-- file-path or a producer-conduit may be provided.
@@ -43,90 +62,70 @@ import Network.Minio.Utils
4362--
4463-- For streams also, a size may be provided. This is useful to limit
4564-- the input - if it is not provided, upload will continue until the
46−-- stream ends or the object reaches `maxObjectSize` size.
47−data ObjectData m
48− = -- | Takes filepath and optional
49− -- size.
50− ODFile FilePath (Maybe Int64)
51− | -- | Pass
52− -- size
53− -- (bytes)
54− -- if
55− -- known.
56− ODStream (C.ConduitM () ByteString m ()) (Maybe Int64)
65+-- stream ends or the object reaches `maxObjectsize` size.
66+data ObjectData m =
67+ ODFile FilePath (Maybe Int64) -- ^ Takes filepath and optional size.
68+ | ODStream (C.Producer m ByteString) (Maybe Int64) -- ^ Pass size in bytes as maybe if known.
5769
5870-- | Put an object from ObjectData. This high-level API handles
5971-- 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
66−putObjectInternal b o opts (ODStream src sizeMay) = do
67− case sizeMay of
68− -- unable to get size, so assume non-seekable file
69− Nothing -> sequentialMultipartUpload b o opts Nothing src
70− -- got file size, so check for single/multipart upload
71− Just size ->
72− if
73− | size <= 64 * oneMiB -> do
74− bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
75− putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
76− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
77− | otherwise -> sequentialMultipartUpload b o opts (Just size) src
78−putObjectInternal b o opts (ODFile fp sizeMay) = do
72+putObjectInternal :: Bucket -> Object -> ObjectData Minio -> Minio ETag
73+putObjectInternal b o (ODStream src sizeMay) = sequentialMultipartUpload b o sizeMay src
74+putObjectInternal b o (ODFile fp sizeMay) = do
7975 hResE <- withNewHandle fp $ \h ->
80− liftA2 (,) (isHandleSeekable h) (getFileSize h)
76+ liftM2 (,) (isHandleSeekable h) (getFileSize h)
8177
82− (isSeekable, handleSizeMay) <-
83− either
84− (const $ return (False, Nothing))
85− return
86− hResE
78+ (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return
79+ hResE
8780
8881 -- prefer given size to queried size.
8982 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
9083
9184 case finalSizeMay of
92− -- unable to get size, so assume non-seekable file
93− Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
85+ -- unable to get size, so assume non-seekable file and max-object size
86+ Nothing -> sequentialMultipartUpload b o (Just maxObjectSize) $
87+ CB.sourceFile fp
88+
9489 -- got file size, so check for single/multipart upload
9590 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
113−parallelMultipartUpload b o opts filePath size = do
91+ if | size <= 64 * oneMiB -> either throwM return =<<
92+ withNewHandle fp (\h -> putObjectSingle b o [] h 0 size)
93+ | size > maxObjectSize -> throwM $ MErrVPutSizeExceeded size
94+ | isSeekable -> parallelMultipartUpload b o fp size
95+ | otherwise -> sequentialMultipartUpload b o (Just size) $
96+ CB.sourceFile fp
97+
98+-- | Select part sizes - the logic is that the minimum part-size will
99+-- be 64MiB.
100+selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
101+selectPartSizes size = uncurry (List.zip3 [1..]) $
102+ List.unzip $ loop 0 size
103+ where
104+ ceil :: Double -> Int64
105+ ceil = ceiling
106+ partSize = max minPartSize (ceil $ fromIntegral size /
107+ fromIntegral maxMultipartParts)
108+
109+ m = fromIntegral partSize
110+ loop st sz
111+ | st > sz = []
112+ | st + m >= sz = [(st, sz - st)]
113+ | otherwise = (st, m) : loop (st + m) sz
114+
115+parallelMultipartUpload :: Bucket -> Object -> FilePath -> Int64
116+ -> Minio ETag
117+parallelMultipartUpload b o filePath size = do
114118 -- get a new upload id.
115− uploadId <- newMultipartUpload b o (pooToHeaders opts)
119+ uploadId <- newMultipartUpload b o []
116120
117121 let partSizeInfo = selectPartSizes size
118122
119− let threads = fromMaybe 10 $ pooNumThreads opts
120−
121− -- perform upload with 'threads' threads
122− uploadedPartsE <-
123− limitedMapConcurrently
124− (fromIntegral threads)
125− (uploadPart uploadId)
126− partSizeInfo
123+ -- perform upload with 10 threads
124+ uploadedPartsE <- limitedMapConcurrently 10
125+ (uploadPart uploadId) partSizeInfo
127126
128127 -- if there were any errors, rethrow exception.
129− mapM_ throwIO $ lefts uploadedPartsE
128+ mapM_ throwM $ lefts uploadedPartsE
130129
131130 -- if we get here, all parts were successfully uploaded.
132131 completeMultipartUpload b o uploadId $ rights uploadedPartsE
@@ -137,37 +136,96 @@ parallelMultipartUpload b o opts filePath size = do
137136 putObjectPart b o uploadId partNum [] payload
138137
139138-- | 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
147−sequentialMultipartUpload b o opts sizeMay src = do
139+sequentialMultipartUpload :: Bucket -> Object -> Maybe Int64
140+ -> C.Producer Minio ByteString -> Minio ETag
141+sequentialMultipartUpload b o sizeMay src = do
148142 -- get a new upload id.
149− uploadId <- newMultipartUpload b o (pooToHeaders opts)
143+ uploadId <- newMultipartUpload b o []
150144
151145 -- upload parts in loop
152146 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
153147 (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
148+ uploadedParts <- src
149+ C..| chunkBSConduit sizes
150+ C..| CL.map PayloadBS
151+ C..| uploadPart' uploadId pnums
152+ C.$$ CC.sinkList
161153
162154 -- complete multipart upload
163155 completeMultipartUpload b o uploadId uploadedParts
156+
164157 where
165158 uploadPart' _ [] = return ()
166− uploadPart' uid (pn : pns) = do
159+ uploadPart' uid (pn:pns) = do
167160 payloadMay <- C.await
168161 case payloadMay of
169162 Nothing -> return ()
170− Just payload -> do
171− pinfo <- lift $ putObjectPart b o uid pn [] payload
172− C.yield pinfo
173− uploadPart' uid pns
163+ Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload
164+ C.yield pinfo
165+ uploadPart' uid pns
166+
167+-- | Copy an object using single or multipart copy strategy.
168+copyObjectInternal :: Bucket -> Object -> CopyPartSource
169+ -> Minio ETag
170+copyObjectInternal b' o cps = do
171+ -- validate and extract the src bucket and object
172+ (srcBucket, srcObject) <- maybe
173+ (throwM $ MErrVInvalidSrcObjSpec $ cpSource cps)
174+ return $ cpsToObject cps
175+
176+ -- get source object size with a head request
177+ (ObjectInfo _ _ _ srcSize) <- headObject srcBucket srcObject
178+
179+ -- check that byte offsets are valid if specified in cps
180+ when (isJust (cpSourceRange cps) &&
181+ or [fst range < 0, snd range < fst range,
182+ snd range >= fromIntegral srcSize]) $
183+ throwM $ MErrVInvalidSrcObjByteRange range
184+
185+ -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
186+ -- 2. If startOffset /= 0 use multipart copy
187+ let destSize = (\(a, b) -> b - a + 1 ) $
188+ maybe (0, srcSize - 1) identity $ cpSourceRange cps
189+ startOffset = maybe 0 fst $ cpSourceRange cps
190+ endOffset = maybe (srcSize - 1) snd $ cpSourceRange cps
191+
192+ if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
193+ then multiPartCopyObject b' o cps srcSize
194+
195+ else fst <$> copyObjectSingle b' o cps{cpSourceRange = Nothing} []
196+
197+ where
198+ range = maybe (0, 0) identity $ cpSourceRange cps
199+
200+-- | Given the input byte range of the source object, compute the
201+-- splits for a multipart copy object procedure. Minimum part size
202+-- used is minPartSize.
203+selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
204+selectCopyRanges (st, end) = zip pns $
205+ map (\(x, y) -> (st + x, st + x + y - 1)) $ zip startOffsets partSizes
206+ where
207+ size = end - st + 1
208+ (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
209+
210+-- | Perform a multipart copy object action. Since we cannot verify
211+-- existing parts based on the source object, there is no resuming
212+-- copy action support.
213+multiPartCopyObject :: Bucket -> Object -> CopyPartSource -> Int64
214+ -> Minio ETag
215+multiPartCopyObject b o cps srcSize = do
216+ uid <- newMultipartUpload b o []
217+
218+ let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $
219+ cpSourceRange cps
220+ partRanges = selectCopyRanges byteRange
221+ partSources = map (\(x, y) -> (x, cps {cpSourceRange = Just y}))
222+ partRanges
223+
224+ copiedParts <- limitedMapConcurrently 10
225+ (\(pn, cps') -> do
226+ (etag, _) <- copyObjectPart b o cps' uid pn []
227+ return (pn, etag)
228+ )
229+ partSources
230+
231+ completeMultipartUpload b o uid copiedParts
src/Network/Minio/S3API.hs+281−544
@@ -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.
@@ -14,672 +14,409 @@
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
18+ (
19+ Region
20+ , getLocation
21+
22+ -- * Listing buckets
23+ --------------------
24+ , getService
25+
26+ -- * Listing objects
27+ --------------------
28+ , ListObjectsResult(..)
29+ , ListObjectsV1Result(..)
30+ , listObjects'
31+ , listObjectsV1'
32+
33+ -- * Retrieving buckets
34+ , headBucket
35+
36+ -- * Retrieving objects
37+ -----------------------
38+ , getObject'
39+ , headObject
40+
41+ -- * Creating buckets and objects
42+ ---------------------------------
43+ , putBucket
44+ , ETag
45+ , putObjectSingle
46+ , copyObjectSingle
47+
48+ -- * Multipart Upload APIs
49+ --------------------------
50+ , UploadId
51+ , PartTuple
52+ , Payload(..)
53+ , PartNumber
54+ , CopyPartSource(..)
55+ , newMultipartUpload
56+ , putObjectPart
57+ , copyObjectPart
58+ , completeMultipartUpload
59+ , abortMultipartUpload
60+ , ListUploadsResult(..)
61+ , listIncompleteUploads'
62+ , ListPartsResult(..)
63+ , listIncompleteParts'
64+
65+ -- * Deletion APIs
66+ --------------------------
67+ , deleteBucket
68+ , deleteObject
69+
70+ -- * Presigned Operations
71+ -----------------------------
72+ , module Network.Minio.PresignedOperations
73+
74+ -- * Bucket Notifications
75+ -------------------------
76+ , Notification(..)
77+ , NotificationConfig(..)
78+ , Arn
79+ , Event(..)
80+ , Filter(..)
81+ , FilterKey(..)
82+ , FilterRules(..)
83+ , FilterRule(..)
84+ , getBucketNotification
85+ , putBucketNotification
86+ , removeAllBucketNotification
87+ ) where
88+
89+import Control.Monad.Catch (catches, Handler(..))
90+import qualified Data.Conduit as C
91+import Data.Default (def)
11392import qualified Network.HTTP.Conduit as NC
11493import 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))
94+import Network.HTTP.Types.Status (status404)
95+
96+import Lib.Prelude hiding (catches)
97+
98+import Network.Minio.API
99+import Network.Minio.Data
100+import Network.Minio.Errors
101+import Network.Minio.Utils
102+import Network.Minio.XmlGenerator
103+import Network.Minio.XmlParser
104+import Network.Minio.PresignedOperations
105+
125106
126107-- | Fetch all buckets from the service.
127108getService :: Minio [BucketInfo]
128109getService = do
129− resp <-
130− executeRequest $
131− defaultS3ReqInfo
132− { riNeedsLocation = False
133− }
110+ resp <- executeRequest $ def {
111+ riNeedsLocation = False
112+ }
134113 parseListBuckets $ NC.responseBody resp
135114
136−-- Parse headers from getObject and headObject calls.
137−parseGetObjectHeaders :: Object -> [HT.Header] -> Maybe ObjectInfo
138−parseGetObjectHeaders object headers =
139− let metadataPairs = getMetadata headers
140− userMetadata = getUserMetadataMap metadataPairs
141− metadata = getNonUserMetadataMap metadataPairs
142− in ObjectInfo
143− <$> Just object
144− <*> getLastModifiedHeader headers
145− <*> getETagHeader headers
146− <*> getContentLength headers
147− <*> Just userMetadata
148− <*> Just metadata
149−
150−-- | GET an object from the service and return parsed ObjectInfo and a
151−-- conduit source for the object content
152−getObject' ::
153− Bucket ->
154− Object ->
155− HT.Query ->
156− [HT.Header] ->
157− Minio GetObjectResponse
115+-- | GET an object from the service and return the response headers
116+-- and a conduit source for the object content
117+getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
118+ -> Minio ([HT.Header], C.ResumableSource Minio ByteString)
158119getObject' bucket object queryParams headers = do
159120 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− }
121+ return (NC.responseHeaders resp, NC.responseBody resp)
171122 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− }
123+ reqInfo = def { riBucket = Just bucket
124+ , riObject = Just object
125+ , riQueryParams = queryParams
126+ , riHeaders = headers
127+ }
184128
185129-- | Creates a bucket via a PUT bucket call.
186130putBucket :: Bucket -> Region -> Minio ()
187−putBucket bucket location = do
188− ns <- asks getSvcNamespace
189− void $
190− executeRequest $
191− defaultS3ReqInfo
192− { riMethod = HT.methodPut,
193− riBucket = Just bucket,
194− riPayload = PayloadBS $ mkCreateBucketConfig ns location,
195− riNeedsLocation = False
131+putBucket bucket location = void $
132+ executeRequest $
133+ def { riMethod = HT.methodPut
134+ , riBucket = Just bucket
135+ , riPayload = PayloadBS $ mkCreateBucketConfig location
136+ , riNeedsLocation = False
196137 }
197138
198139-- | Single PUT object size.
199140maxSinglePutObjectSizeBytes :: Int64
200141maxSinglePutObjectSizeBytes = 5 * 1024 * 1024 * 1024
201142
202−-- | PUT an object into the service. This function performs a single
203−-- PUT object call and uses a strict ByteString as the object
204−-- data. `putObjectSingle` is preferable as the object data will not
205−-- be resident in memory.
206−putObjectSingle' :: Bucket -> Object -> [HT.Header] -> ByteString -> Minio ETag
207−putObjectSingle' bucket object headers bs = do
208− let size = fromIntegral (BS.length bs)
209− -- check length is within single PUT object size.
210− when (size > maxSinglePutObjectSizeBytes) $
211− throwIO $
212− MErrVSinglePUTSizeExceeded size
213−
214− let payload = mkStreamingPayload $ PayloadBS bs
215− resp <-
216− executeRequest $
217− defaultS3ReqInfo
218− { riMethod = HT.methodPut,
219− riBucket = Just bucket,
220− riObject = Just object,
221− riHeaders = headers,
222− riPayload = payload
223− }
224−
225− let rheaders = NC.responseHeaders resp
226− etag = getETagHeader rheaders
227− maybe
228− (throwIO MErrVETagHeaderNotFound)
229− return
230− etag
231−
232143-- | PUT an object into the service. This function performs a single
233144-- 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
145+putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
146+ -> Int64 -> Minio ETag
242147putObjectSingle bucket object headers h offset size = do
243148 -- check length is within single PUT object size.
244149 when (size > maxSinglePutObjectSizeBytes) $
245− throwIO $
246− MErrVSinglePUTSizeExceeded size
150+ throwM $ MErrVSinglePUTSizeExceeded size
247151
248152 -- content-length header is automatically set by library.
249− let payload = mkStreamingPayload $ PayloadH h offset size
250− resp <-
251− executeRequest $
252− defaultS3ReqInfo
253− { riMethod = HT.methodPut,
254− riBucket = Just bucket,
255− riObject = Just object,
256− riHeaders = headers,
257− riPayload = payload
258− }
153+ resp <- executeRequest $
154+ def { riMethod = HT.methodPut
155+ , riBucket = Just bucket
156+ , riObject = Just object
157+ , riHeaders = headers
158+ , riPayload = PayloadH h offset size
159+ }
259160
260161 let rheaders = NC.responseHeaders resp
261162 etag = getETagHeader rheaders
262163 maybe
263− (throwIO MErrVETagHeaderNotFound)
264− return
265− etag
164+ (throwM MErrVETagHeaderNotFound)
165+ return etag
266166
267167-- | List objects in a bucket matching prefix up to delimiter,
268168-- starting from nextMarker.
269−listObjectsV1' ::
270− Bucket ->
271− Maybe Text ->
272− Maybe Text ->
273− Maybe Text ->
274− Maybe Int ->
275− Minio ListObjectsV1Result
169+listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
170+ -> Minio ListObjectsV1Result
276171listObjectsV1' 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− }
172+ resp <- executeRequest $ def { riMethod = HT.methodGet
173+ , riBucket = Just bucket
174+ , riQueryParams = mkOptionalParams params
175+ }
284176 parseListObjectsV1Response $ NC.responseBody resp
285177 where
286− params =
287− [ ("marker", nextMarker),
288− ("prefix", prefix),
289− ("delimiter", delimiter),
290− ("max-keys", show <$> maxKeys)
178+ params = [
179+ ("marker", nextMarker)
180+ , ("prefix", prefix)
181+ , ("delimiter", delimiter)
182+ , ("max-keys", show <$> maxKeys)
291183 ]
292184
293185-- | List objects in a bucket matching prefix up to delimiter,
294186-- starting from nextToken.
295−listObjects' ::
296− Bucket ->
297− Maybe Text ->
298− Maybe Text ->
299− Maybe Text ->
300− Maybe Int ->
301− Minio ListObjectsResult
187+listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
188+ -> Minio ListObjectsResult
302189listObjects' 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− }
190+ resp <- executeRequest $ def { riMethod = HT.methodGet
191+ , riBucket = Just bucket
192+ , riQueryParams = mkOptionalParams params
193+ }
310194 parseListObjectsResponse $ NC.responseBody resp
311195 where
312− params =
313− [ ("list-type", Just "2"),
314− ("continuation_token", nextToken),
315− ("prefix", prefix),
316− ("delimiter", delimiter),
317− ("max-keys", show <$> maxKeys)
196+ params = [
197+ ("list-type", Just "2")
198+ , ("continuation_token", nextToken)
199+ , ("prefix", prefix)
200+ , ("delimiter", delimiter)
201+ , ("max-keys", show <$> maxKeys)
318202 ]
319203
320204-- | DELETE a bucket from the service.
321205deleteBucket :: Bucket -> Minio ()
322−deleteBucket bucket =
323− void $
324− executeRequest $
325− defaultS3ReqInfo
326− { riMethod = HT.methodDelete,
327− riBucket = Just bucket
206+deleteBucket bucket = void $
207+ executeRequest $
208+ def { riMethod = HT.methodDelete
209+ , riBucket = Just bucket
328210 }
329211
330212-- | DELETE an object from the service.
331213deleteObject :: 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
214+deleteObject bucket object = void $
215+ executeRequest $
216+ def { riMethod = HT.methodDelete
217+ , riBucket = Just bucket
218+ , riObject = Just object
339219 }
340220
341221-- | Create a new multipart upload.
342222newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
343223newMultipartUpload 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− }
224+ resp <- executeRequest $ def { riMethod = HT.methodPost
225+ , riBucket = Just bucket
226+ , riObject = Just object
227+ , riQueryParams = [("uploads", Nothing)]
228+ , riHeaders = headers
229+ }
353230 parseNewMultipartUpload $ NC.responseBody resp
354231
355232-- | 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
233+putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
234+ -> Payload -> Minio PartTuple
364235putObjectPart bucket object uploadId partNumber headers payload = do
365− -- transform payload to conduit to enable streaming signature
366− let payload' = mkStreamingPayload payload
367− resp <-
368− executeRequest $
369− defaultS3ReqInfo
370− { riMethod = HT.methodPut,
371− riBucket = Just bucket,
372− riObject = Just object,
373− riQueryParams = mkOptionalParams params,
374− riHeaders = headers,
375− riPayload = payload'
376− }
236+ resp <- executeRequest $
237+ def { riMethod = HT.methodPut
238+ , riBucket = Just bucket
239+ , riObject = Just object
240+ , riQueryParams = mkOptionalParams params
241+ , riHeaders = headers
242+ , riPayload = payload
243+ }
377244 let rheaders = NC.responseHeaders resp
378245 etag = getETagHeader rheaders
379246 maybe
380− (throwIO MErrVETagHeaderNotFound)
381− (return . (partNumber,))
382− etag
383− where
384− params =
385− [ ("uploadId", Just uploadId),
386− ("partNumber", Just $ show partNumber)
387− ]
388−
389−srcInfoToHeaders :: SourceInfo -> [HT.Header]
390−srcInfoToHeaders srcInfo =
391− ( "x-amz-copy-source",
392− encodeUtf8 $
393− T.concat
394− [ "/",
395− srcBucket srcInfo,
396− "/",
397− srcObject srcInfo
398− ]
399− )
400− : rangeHdr
401− ++ zip names values
247+ (throwM MErrVETagHeaderNotFound)
248+ (return . (partNumber, )) etag
402249 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"
250+ params = [
251+ ("uploadId", Just uploadId)
252+ , ("partNumber", Just $ show partNumber)
408253 ]
409− values =
410− mapMaybe
411− (fmap encodeUtf8 . (srcInfo &))
412− [ srcIfMatch,
413− srcIfNoneMatch,
414− fmap formatRFC1123 . srcIfUnmodifiedSince,
415− fmap formatRFC1123 . srcIfModifiedSince
416− ]
417− rangeHdr =
418− maybe [] ((\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) . toByteRange) (srcRange srcInfo)
419− toByteRange :: (Int64, Int64) -> HT.ByteRange
420− toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
421254
422255-- | Performs server-side copy of an object or part of an object as an
423256-- 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)
431−copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
432− resp <-
433− executeRequest $
434− defaultS3ReqInfo
435− { riMethod = HT.methodPut,
436− riBucket = Just $ dstBucket dstInfo,
437− riObject = Just $ dstObject dstInfo,
438− riQueryParams = mkOptionalParams params,
439− riHeaders = headers ++ srcInfoToHeaders srcInfo
440− }
257+copyObjectPart :: Bucket -> Object -> CopyPartSource -> UploadId
258+ -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
259+copyObjectPart bucket object cps uploadId partNumber headers = do
260+ resp <- executeRequest $
261+ def { riMethod = HT.methodPut
262+ , riBucket = Just bucket
263+ , riObject = Just object
264+ , riQueryParams = mkOptionalParams params
265+ , riHeaders = headers ++ cpsToHeaders cps
266+ }
441267
442268 parseCopyObjectResponse $ NC.responseBody resp
443269 where
444− params =
445− [ ("uploadId", Just uploadId),
446− ("partNumber", Just $ show partNumber)
270+ params = [
271+ ("uploadId", Just uploadId)
272+ , ("partNumber", Just $ show partNumber)
447273 ]
448274
449275-- | Performs server-side copy of an object that is upto 5GiB in
450276-- size. If the object is greater than 5GiB, this function throws the
451277-- error returned by the server.
452−copyObjectSingle ::
453− Bucket ->
454− Object ->
455− SourceInfo ->
456− [HT.Header] ->
457− Minio (ETag, UTCTime)
458−copyObjectSingle bucket object srcInfo headers = do
459− -- validate that srcRange is Nothing for this API.
460− when (isJust $ srcRange srcInfo) $
461− throwIO MErrVCopyObjSingleNoRangeAccepted
462− resp <-
463− executeRequest $
464− defaultS3ReqInfo
465− { riMethod = HT.methodPut,
466− riBucket = Just bucket,
467− riObject = Just object,
468− riHeaders = headers ++ srcInfoToHeaders srcInfo
469− }
278+copyObjectSingle :: Bucket -> Object -> CopyPartSource -> [HT.Header]
279+ -> Minio (ETag, UTCTime)
280+copyObjectSingle bucket object cps headers = do
281+ -- validate that cpSourceRange is Nothing for this API.
282+ when (isJust $ cpSourceRange cps) $
283+ throwM MErrVCopyObjSingleNoRangeAccepted
284+ resp <- executeRequest $
285+ def { riMethod = HT.methodPut
286+ , riBucket = Just bucket
287+ , riObject = Just object
288+ , riHeaders = headers ++ cpsToHeaders cps
289+ }
470290 parseCopyObjectResponse $ NC.responseBody resp
471291
472292-- | Complete a multipart upload.
473−completeMultipartUpload ::
474− Bucket ->
475− Object ->
476− UploadId ->
477− [PartTuple] ->
478− Minio ETag
293+completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
294+ -> Minio ETag
479295completeMultipartUpload 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− }
296+ resp <- executeRequest $
297+ def { riMethod = HT.methodPost
298+ , riBucket = Just bucket
299+ , riObject = Just object
300+ , riQueryParams = mkOptionalParams params
301+ , riPayload = PayloadBS $
302+ mkCompleteMultipartUploadRequest partTuple
303+ }
491304 parseCompleteMultipartUploadResponse $ NC.responseBody resp
492305 where
493306 params = [("uploadId", Just uploadId)]
494307
495308-- | Abort a multipart upload.
496309abortMultipartUpload :: 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− }
310+abortMultipartUpload bucket object uploadId = void $
311+ executeRequest $ def { riMethod = HT.methodDelete
312+ , riBucket = Just bucket
313+ , riObject = Just object
314+ , riQueryParams = mkOptionalParams params
315+ }
506316 where
507317 params = [("uploadId", Just uploadId)]
508318
509319-- | 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
320+listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
321+ -> Maybe Text -> Maybe Int -> Minio ListUploadsResult
518322listIncompleteUploads' 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− }
323+ resp <- executeRequest $ def { riMethod = HT.methodGet
324+ , riBucket = Just bucket
325+ , riQueryParams = params
326+ }
526327 parseListUploadsResponse $ NC.responseBody resp
527328 where
528329 -- 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− ]
330+ params = ("uploads", Nothing) : mkOptionalParams
331+ [ ("prefix", prefix)
332+ , ("delimiter", delimiter)
333+ , ("key-marker", keyMarker)
334+ , ("upload-id-marker", uploadIdMarker)
335+ , ("max-uploads", show <$> maxKeys)
336+ ]
337+
538338
539339-- | List parts of an ongoing multipart upload.
540−listIncompleteParts' ::
541− Bucket ->
542− Object ->
543− UploadId ->
544− Maybe Text ->
545− Maybe Text ->
546− Minio ListPartsResult
340+listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
341+ -> Maybe Text -> Minio ListPartsResult
547342listIncompleteParts' 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− }
343+ resp <- executeRequest $ def { riMethod = HT.methodGet
344+ , riBucket = Just bucket
345+ , riObject = Just object
346+ , riQueryParams = mkOptionalParams params
347+ }
556348 parseListPartsResponse $ NC.responseBody resp
557349 where
558350 -- build optional query params
559− params =
560− [ ("uploadId", Just uploadId),
561− ("part-number-marker", partNumMarker),
562− ("max-parts", maxParts)
351+ params = [
352+ ("uploadId", Just uploadId)
353+ , ("part-number-marker", partNumMarker)
354+ , ("max-parts", maxParts)
563355 ]
564356
565357-- | Get metadata of an object.
566−headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
567−headObject bucket object reqHeaders = do
568− resp <-
569− executeRequest $
570− defaultS3ReqInfo
571− { riMethod = HT.methodHead,
572− riBucket = Just bucket,
573− riObject = Just object,
574− riHeaders =
575− reqHeaders
576− -- This header is required for safety as otherwise http-client,
577− -- sends Accept-Encoding: gzip, and the server may actually gzip
578− -- body. In that case Content-Length header will be missing.
579− <> [("Accept-Encoding", "identity")]
580− }
581− maybe (throwIO MErrVInvalidObjectInfoResponse) return $
582− parseGetObjectHeaders object $
583− NC.responseHeaders resp
358+headObject :: Bucket -> Object -> Minio ObjectInfo
359+headObject bucket object = do
360+ resp <- executeRequest $ def { riMethod = HT.methodHead
361+ , riBucket = Just bucket
362+ , riObject = Just object
363+ }
364+
365+ let
366+ headers = NC.responseHeaders resp
367+ modTime = getLastModifiedHeader headers
368+ etag = getETagHeader headers
369+ size = getContentLength headers
370+
371+ maybe (throwM MErrVInvalidObjectInfoResponse) return $
372+ ObjectInfo <$> Just object <*> modTime <*> etag <*> size
373+
374+
584375
585376-- | Query the object store if a given bucket exists.
586377headBucket :: Bucket -> Minio Bool
587−headBucket bucket =
588− headBucketEx
589− `catches` [ Handler handleNoSuchBucket,
590− Handler handleStatus404
591− ]
378+headBucket bucket = headBucketEx `catches`
379+ [ Handler handleNoSuchBucket
380+ , Handler handleStatus404
381+ ]
382+
592383 where
593384 handleNoSuchBucket :: ServiceErr -> Minio Bool
594− handleNoSuchBucket e
595− | e == NoSuchBucket = return False
596− | otherwise = throwIO e
385+ handleNoSuchBucket e | e == NoSuchBucket = return False
386+ | otherwise = throwM e
387+
597388 handleStatus404 :: NC.HttpException -> Minio Bool
598389 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
599390 if NC.responseStatus res == status404
600− then return False
601− else throwIO e
602− handleStatus404 e = throwIO e
391+ then return False
392+ else throwM e
393+ handleStatus404 e = throwM e
394+
603395 headBucketEx = do
604− resp <-
605− executeRequest $
606− defaultS3ReqInfo
607− { riMethod = HT.methodHead,
608− riBucket = Just bucket
609− }
396+ resp <- executeRequest $ def { riMethod = HT.methodHead
397+ , riBucket = Just bucket
398+ }
610399 return $ NC.responseStatus resp == HT.ok200
611400
612401-- | Set the notification configuration on a bucket.
613402putBucketNotification :: Bucket -> Notification -> Minio ()
614−putBucketNotification bucket ncfg = do
615− ns <- asks getSvcNamespace
616− void $
617− executeRequest $
618− defaultS3ReqInfo
619− { riMethod = HT.methodPut,
620− riBucket = Just bucket,
621− riQueryParams = [("notification", Nothing)],
622− riPayload =
623− PayloadBS $
624− mkPutNotificationRequest ns ncfg
625− }
403+putBucketNotification bucket ncfg =
404+ void $ executeRequest $ def { riMethod = HT.methodPut
405+ , riBucket = Just bucket
406+ , riQueryParams = [("notification", Nothing)]
407+ , riPayload = PayloadBS $
408+ mkPutNotificationRequest ncfg
409+ }
626410
627411-- | Retrieve the notification configuration on a bucket.
628412getBucketNotification :: Bucket -> Minio Notification
629413getBucketNotification bucket = do
630− resp <-
631− executeRequest $
632− defaultS3ReqInfo
633− { riMethod = HT.methodGet,
634− riBucket = Just bucket,
635− riQueryParams = [("notification", Nothing)]
636− }
414+ resp <- executeRequest $ def { riMethod = HT.methodGet
415+ , riBucket = Just bucket
416+ , riQueryParams = [("notification", Nothing)]
417+ }
637418 parseNotification $ NC.responseBody resp
638419
639420-- | Remove all notifications configured on a bucket.
640421removeAllBucketNotification :: Bucket -> Minio ()
641−removeAllBucketNotification = flip putBucketNotification defaultNotification
642−
643−-- | Fetch the policy if any on a bucket.
644−getBucketPolicy :: Bucket -> Minio Text
645−getBucketPolicy bucket = do
646− resp <-
647− executeRequest $
648− defaultS3ReqInfo
649− { riMethod = HT.methodGet,
650− riBucket = Just bucket,
651− riQueryParams = [("policy", Nothing)]
652− }
653− return $ decodeUtf8Lenient $ toStrictBS $ NC.responseBody resp
654−
655−-- | Set a new policy on a bucket.
656−-- As a special condition if the policy is empty
657−-- then we treat it as policy DELETE operation.
658−setBucketPolicy :: Bucket -> Text -> Minio ()
659−setBucketPolicy bucket policy = do
660− if T.null policy
661− then deleteBucketPolicy bucket
662− else putBucketPolicy bucket policy
663−
664−-- | Save a new policy on a bucket.
665−putBucketPolicy :: Bucket -> Text -> Minio ()
666−putBucketPolicy bucket policy = do
667− void $
668− executeRequest $
669− defaultS3ReqInfo
670− { riMethod = HT.methodPut,
671− riBucket = Just bucket,
672− riQueryParams = [("policy", Nothing)],
673− riPayload = PayloadBS $ encodeUtf8 policy
674− }
675−
676−-- | Delete any policy set on a bucket.
677−deleteBucketPolicy :: Bucket -> Minio ()
678−deleteBucketPolicy bucket = do
679− void $
680− executeRequest $
681− defaultS3ReqInfo
682− { riMethod = HT.methodDelete,
683− riBucket = Just bucket,
684− riQueryParams = [("policy", Nothing)]
685− }
422+removeAllBucketNotification = flip putBucketNotification def
src/Network/Minio/SelectAPI.hsdeleted−294
@@ -1,294 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−module Network.Minio.SelectAPI
18− ( -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
19− -- format objects in AWS S3 and in MinIO using SQL Select
20− -- statements. This allows significant reduction of data transfer
21− -- from object storage for computation-intensive tasks, as relevant
22− -- data is filtered close to the storage.
23− selectObjectContent,
24− SelectRequest,
25− selectRequest,
26−
27− -- *** Input Serialization
28− InputSerialization,
29− defaultCsvInput,
30− linesJsonInput,
31− documentJsonInput,
32− defaultParquetInput,
33− setInputCSVProps,
34− CompressionType (..),
35− setInputCompressionType,
36−
37− -- *** CSV Format details
38−
39− -- | CSV format options such as delimiters and quote characters are
40− -- specified using using the functions below. Options are combined
41− -- monoidally.
42− CSVProp,
43− recordDelimiter,
44− fieldDelimiter,
45− quoteCharacter,
46− quoteEscapeCharacter,
47− commentCharacter,
48− allowQuotedRecordDelimiter,
49− FileHeaderInfo (..),
50− fileHeaderInfo,
51− QuoteFields (..),
52− quoteFields,
53−
54− -- *** Output Serialization
55− OutputSerialization,
56− defaultCsvOutput,
57− defaultJsonOutput,
58− outputCSVFromProps,
59− outputJSONFromRecordDelimiter,
60−
61− -- *** Progress messages
62− setRequestProgressEnabled,
63−
64− -- *** Interpreting Select output
65−
66− -- | The conduit returned by `selectObjectContent` returns values of
67− -- the `EventMessage` data type. This returns the query output
68− -- messages formatted according to the chosen output serialization,
69− -- interleaved with progress messages (if enabled by
70− -- `setRequestProgressEnabled`), and at the end a statistics
71− -- message.
72− --
73− -- If the application is interested in only the payload, then
74− -- `getPayloadBytes` can be used. For example to simply print the
75− -- payload to stdout:
76− --
77− -- > resultConduit <- selectObjectContent bucket object mySelectRequest
78− -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
79− --
80− -- Note that runConduit, the connect operator (.|) and stdoutC are
81− -- all from the "conduit" package.
82− getPayloadBytes,
83− EventMessage (..),
84− Progress (..),
85− Stats,
86− )
87−where
88−
89−import Conduit ((.|))
90−import qualified Conduit as C
91−import qualified Data.Binary as Bin
92−import qualified Data.ByteString as B
93−import qualified Data.ByteString.Lazy as LB
94−import Data.Digest.CRC32 (crc32, crc32Update)
95−import Lib.Prelude
96−import qualified Network.HTTP.Conduit as NC
97−import qualified Network.HTTP.Types as HT
98−import Network.Minio.API
99−import Network.Minio.Data
100−import Network.Minio.Errors
101−import Network.Minio.Utils
102−import Network.Minio.XmlGenerator
103−import Network.Minio.XmlParser
104−import UnliftIO (MonadUnliftIO)
105−
106−data EventStreamException
107− = ESEPreludeCRCFailed
108− | ESEMessageCRCFailed
109− | ESEUnexpectedEndOfStream
110− | ESEDecodeFail [Char]
111− | ESEInvalidHeaderType
112− | ESEInvalidHeaderValueType
113− | ESEInvalidMessageType
114− deriving stock (Eq, Show)
115−
116−instance Exception EventStreamException
117−
118−-- chunkSize in bytes is 32KiB
119−chunkSize :: Int
120−chunkSize = 32 * 1024
121−
122−parseBinary :: (Bin.Binary a) => ByteString -> IO a
123−parseBinary b = do
124− case Bin.decodeOrFail $ LB.fromStrict b of
125− Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
126− Right (_, _, r) -> return r
127−
128−bytesToHeaderName :: Text -> IO MsgHeaderName
129−bytesToHeaderName t = case t of
130− ":message-type" -> return MessageType
131− ":event-type" -> return EventType
132− ":content-type" -> return ContentType
133− ":error-code" -> return ErrorCode
134− ":error-message" -> return ErrorMessage
135− _ -> throwIO ESEInvalidHeaderType
136−
137−parseHeaders ::
138− (MonadUnliftIO m) =>
139− Word32 ->
140− C.ConduitM ByteString a m [MessageHeader]
141−parseHeaders 0 = return []
142−parseHeaders hdrLen = do
143− bs1 <- readNBytes 1
144− n :: Word8 <- liftIO $ parseBinary bs1
145−
146− headerKeyBytes <- readNBytes $ fromIntegral n
147− let headerKey = decodeUtf8Lenient headerKeyBytes
148− headerName <- liftIO $ bytesToHeaderName headerKey
149−
150− bs2 <- readNBytes 1
151− headerValueType :: Word8 <- liftIO $ parseBinary bs2
152− when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
153−
154− bs3 <- readNBytes 2
155− vLen :: Word16 <- liftIO $ parseBinary bs3
156− headerValueBytes <- readNBytes $ fromIntegral vLen
157− let headerValue = decodeUtf8Lenient headerValueBytes
158− m = (headerName, headerValue)
159− k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
160−
161− ms <- parseHeaders (hdrLen - k)
162− return (m : ms)
163−
164−-- readNBytes returns N bytes read from the string and throws an
165−-- exception if N bytes are not present on the stream.
166−readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
167−readNBytes n = do
168− b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
169− if B.length b /= n
170− then throwIO ESEUnexpectedEndOfStream
171− else return b
172−
173−crcCheck ::
174− (MonadUnliftIO m) =>
175− C.ConduitM ByteString ByteString m ()
176−crcCheck = do
177− b <- readNBytes 12
178− n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
179− preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
180− when (crc32 (B.take 8 b) /= preludeCRC) $
181− throwIO ESEPreludeCRCFailed
182−
183− -- we do not yield the checksum
184− C.yield $ B.take 8 b
185−
186− -- 12 bytes have been read off the current message. Now read the
187− -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
188− let startCrc = crc32 b
189− finalCrc <- accumulateYield (fromIntegral n - 16) startCrc
190−
191− bs <- readNBytes 4
192− expectedCrc :: Word32 <- liftIO $ parseBinary bs
193−
194− when (finalCrc /= expectedCrc) $
195− throwIO ESEMessageCRCFailed
196−
197− -- we unconditionally recurse - downstream figures out when to
198− -- quit reading the stream
199− crcCheck
200− where
201− accumulateYield n checkSum = do
202− let toRead = min n chunkSize
203− b <- readNBytes toRead
204− let c' = crc32Update checkSum b
205− n' = n - B.length b
206− C.yield b
207− if n' > 0
208− then accumulateYield n' c'
209− else return c'
210−
211−handleMessage :: (MonadUnliftIO m) => C.ConduitT ByteString EventMessage m ()
212−handleMessage = do
213− b1 <- readNBytes 4
214− msgLen :: Word32 <- liftIO $ parseBinary b1
215−
216− b2 <- readNBytes 4
217− hdrLen :: Word32 <- liftIO $ parseBinary b2
218−
219− hs <- parseHeaders hdrLen
220−
221− let payloadLen = msgLen - hdrLen - 16
222− getHdrVal h = fmap snd . find ((h ==) . fst)
223− eventHdrValue = getHdrVal EventType hs
224− msgHdrValue = getHdrVal MessageType hs
225− errCode = getHdrVal ErrorCode hs
226− errMsg = getHdrVal ErrorMessage hs
227−
228− case msgHdrValue of
229− Just "event" -> do
230− case eventHdrValue of
231− Just "Records" -> passThrough $ fromIntegral payloadLen
232− Just "Cont" -> return ()
233− Just "Progress" -> do
234− bs <- readNBytes $ fromIntegral payloadLen
235− progress <- parseSelectProgress bs
236− C.yield $ ProgressEventMessage progress
237− Just "Stats" -> do
238− bs <- readNBytes $ fromIntegral payloadLen
239− stats <- parseSelectProgress bs
240− C.yield $ StatsEventMessage stats
241− Just "End" -> return ()
242− _ -> throwIO ESEInvalidMessageType
243− when (eventHdrValue /= Just "End") handleMessage
244− Just "error" -> do
245− let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
246− maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
247− _ -> throwIO ESEInvalidMessageType
248− where
249− passThrough 0 = return ()
250− passThrough n = do
251− let c = min n chunkSize
252− b <- readNBytes c
253− C.yield $ RecordPayloadEventMessage b
254− passThrough $ n - B.length b
255−
256−selectProtoConduit ::
257− (MonadUnliftIO m) =>
258− C.ConduitT ByteString EventMessage m ()
259−selectProtoConduit = crcCheck .| handleMessage
260−
261−-- | selectObjectContent calls the SelectRequest on the given
262−-- object. It returns a Conduit of event messages that can be consumed
263−-- by the client.
264−selectObjectContent ::
265− Bucket ->
266− Object ->
267− SelectRequest ->
268− Minio (C.ConduitT () EventMessage Minio ())
269−selectObjectContent b o r = do
270− let reqInfo =
271− defaultS3ReqInfo
272− { riMethod = HT.methodPost,
273− riBucket = Just b,
274− riObject = Just o,
275− riPayload = PayloadBS $ mkSelectRequest r,
276− riNeedsLocation = False,
277− riQueryParams = [("select", Nothing), ("select-type", Just "2")]
278− }
279− -- print $ mkSelectRequest r
280− resp <- mkStreamRequest reqInfo
281− return $ NC.responseBody resp .| selectProtoConduit
282−
283−-- | A helper conduit that returns only the record payload bytes.
284−getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
285−getPayloadBytes = do
286− evM <- C.await
287− case evM of
288− Just v -> do
289− case v of
290− RecordPayloadEventMessage b -> C.yield b
291− RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
292− _ -> return ()
293− getPayloadBytes
294− Nothing -> return ()
src/Network/Minio/Sign/V4.hs+199−446
@@ -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,313 +13,219 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE BangPatterns #-}
1716
1817module Network.Minio.Sign.V4
19− ( SignParams (..),
20− signV4QueryParams,
21− signV4,
22− signV4PostPolicy,
23− signV4Stream,
24− Service (..),
25− credentialScope,
26− )
27−where
18+ (
19+ signV4
20+ , signV4AtTime
21+ , signV4PostPolicy
22+ , mkScope
23+ , getHeadersToSign
24+ , mkCanonicalRequest
25+ , mkStringToSign
26+ , mkSigningKey
27+ , computeSignature
28+ , SignV4Data(..)
29+ , debugPrintSignV4Data
30+ ) where
2831
29−import qualified Conduit as C
30−import qualified Data.ByteArray as BA
3132import qualified Data.ByteString as B
32−import qualified Data.ByteString.Base64 as Base64
3333import qualified Data.ByteString.Char8 as B8
34−import qualified Data.ByteString.Lazy as LB
35−import Data.CaseInsensitive (mk)
34+import Data.CaseInsensitive (mk)
3635import 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
36+import qualified Data.Set as Set
4137import 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)
38+import qualified Data.ByteString.Base64 as Base64
39+import qualified Data.Map.Strict as Map
40+import Network.HTTP.Types (Header)
41+import qualified Network.HTTP.Types.Header as H
42+
43+import Lib.Prelude
44+import Network.Minio.Data
45+import Network.Minio.Data.ByteString
46+import Network.Minio.Data.Crypto
47+import Network.Minio.Data.Time
5248
5349-- these headers are not included in the string to sign when signing a
5450-- request
55−ignoredHeaders :: Set.HashSet ByteString
56−ignoredHeaders =
57− Set.fromList $
58− map
59− CI.foldedCase
60− [ H.hAuthorization,
61− H.hContentType,
62− H.hUserAgent
63− ]
64−
65−data Service = ServiceS3 | ServiceSTS
66− deriving stock (Eq, Show)
67−
68−toByteString :: Service -> ByteString
69−toByteString ServiceS3 = "s3"
70−toByteString ServiceSTS = "sts"
71−
72−data SignParams = SignParams
73− { spAccessKey :: Text,
74− spSecretKey :: BA.ScrubbedBytes,
75− spSessionToken :: Maybe BA.ScrubbedBytes,
76− spService :: Service,
77− spTimeStamp :: UTCTime,
78− spRegion :: Maybe Text,
79− spExpirySecs :: Maybe UrlExpiry,
80− spPayloadHash :: Maybe ByteString
81− }
82− deriving stock (Show)
83−
84−mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
85−mkAuthHeader accessKey scope signedHeaderKeys sign =
86− let authValue =
87− B.concat
88− [ "AWS4-HMAC-SHA256 Credential=",
89− encodeUtf8 accessKey,
90− "/",
91− scope,
92− ", SignedHeaders=",
93− signedHeaderKeys,
94− ", Signature=",
95− sign
96− ]
97− in (H.hAuthorization, authValue)
98−
99−data IsStreaming = IsStreamingLength Int64 | NotStreaming
100− deriving stock (Eq, Show)
101−
102−amzSecurityToken :: ByteString
103−amzSecurityToken = "X-Amz-Security-Token"
104−
105−-- | Given SignParams and request details, including request method,
51+ignoredHeaders :: Set ByteString
52+ignoredHeaders = Set.fromList $ map CI.foldedCase
53+ [ H.hAuthorization
54+ , H.hContentType
55+ , H.hContentLength
56+ , H.hUserAgent
57+ ]
58+
59+data SignV4Data = SignV4Data {
60+ sv4SignTime :: UTCTime
61+ , sv4Scope :: ByteString
62+ , sv4CanonicalRequest :: ByteString
63+ , sv4HeadersToSign :: [(ByteString, ByteString)]
64+ , sv4Output :: [(ByteString, ByteString)]
65+ , sv4StringToSign :: ByteString
66+ , sv4SigningKey :: ByteString
67+ } deriving (Show)
68+
69+debugPrintSignV4Data :: SignV4Data -> IO ()
70+debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
71+ B8.putStrLn "SignV4Data:"
72+ B8.putStr "Timestamp: " >> print t
73+ B8.putStr "Scope: " >> B8.putStrLn s
74+ B8.putStrLn "Canonical Request:"
75+ B8.putStrLn cr
76+ B8.putStr "Headers to Sign: " >> print h2s
77+ B8.putStr "Output: " >> print o
78+ B8.putStr "StringToSign: " >> B8.putStrLn sts
79+ B8.putStr "SigningKey: " >> printBytes sk
80+ B8.putStrLn "END of SignV4Data ========="
81+ where
82+ printBytes b = do
83+ mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b
84+ B8.putStrLn ""
85+
86+-- | Given MinioClient and request details, including request method,
10687-- request path, headers, query params and payload hash, generates an
10788-- updated set of headers, including the x-amz-date header and the
10889-- Authorization header, which includes the signature.
109−--
110−-- For normal requests (i.e. without an expiry time), the output is
111−-- the list of headers to add to authenticate the request.
90+signV4 :: ConnectInfo -> RequestInfo -> Maybe Int
91+ -> IO [(ByteString, ByteString)]
92+signV4 !ci !ri !expiry = do
93+ timestamp <- Time.getCurrentTime
94+ let signData = signV4AtTime timestamp ci ri expiry
95+ -- debugPrintSignV4Data signData
96+ return $ sv4Output signData
97+
98+-- | Takes a timestamp, server params and request params and generates
99+-- AWS Sign V4 data. For normal requests (i.e. without an expiry
100+-- time), the output is the list of headers to add to authenticate the
101+-- request.
112102--
113103-- If `expiry` is not Nothing, it is assumed that a presigned request
114104-- is being created. The expiry is interpreted as an integer number of
115105-- seconds. The output will be the list of query-parameters to add to
116106-- 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]
162−signV4 !sp !req =
163− let scope = credentialScope sp
107+signV4AtTime :: UTCTime -> ConnectInfo -> RequestInfo -> Maybe Int
108+ -> SignV4Data
109+signV4AtTime ts ci ri expiry =
110+ let
111+ region = maybe (connectRegion ci) identity $ riRegion ri
112+ scope = mkScope ts region
113+ accessKey = toS $ connectAccessKey ci
114+ secretKey = toS $ connectSecretKey ci
115+
116+ -- headers to be added to the request
117+ datePair = ("X-Amz-Date", awsTimeFormatBS ts)
118+ computedHeaders = riHeaders ri ++
119+ if isJust expiry
120+ then []
121+ else [(\(x, y) -> (mk x, y)) datePair]
122+ headersToSign = getHeadersToSign computedHeaders
123+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
124+
125+ -- query-parameters to be added before signing for presigned URLs
126+ -- (i.e. when `isJust expiry`)
127+ authQP = [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
128+ , ("X-Amz-Credential", B.concat [accessKey, "/", scope])
129+ , datePair
130+ , ("X-Amz-Expires", maybe "" show expiry)
131+ , ("X-Amz-SignedHeaders", signedHeaderKeys)
132+ ]
133+ finalQP = riQueryParams ri ++
134+ if isJust expiry
135+ then (fmap . fmap) Just authQP
136+ else []
137+
138+ -- 1. compute canonical request
139+ canonicalRequest = mkCanonicalRequest (ri {riQueryParams = finalQP})
140+ headersToSign
141+
142+ -- 2. compute string to sign
143+ stringToSign = mkStringToSign ts scope canonicalRequest
144+
145+ -- 3.1 compute signing key
146+ signingKey = mkSigningKey ts region secretKey
147+
148+ -- 3.2 compute signature
149+ signature = computeSignature stringToSign signingKey
150+
151+ -- 4. compute auth header
152+ authValue = B.concat
153+ [ "AWS4-HMAC-SHA256 Credential="
154+ , accessKey
155+ , "/"
156+ , scope
157+ , ", SignedHeaders="
158+ , signedHeaderKeys
159+ , ", Signature="
160+ , signature
161+ ]
162+ authHeader = (H.hAuthorization, authValue)
164163
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)
164+ -- finally compute output pairs
165+ output = if isJust expiry
166+ then ("X-Amz-Signature", signature) : authQP
167+ else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
168+ datePair]
176169
177− -- 1. compute canonical request
178− reqHeaders = NC.requestHeaders req ++ extraHeaders
179− (canonicalRequest, signedHeaderKeys) =
180− getCanonicalRequestAndSignedHeaders
181− NotStreaming
182− sp
183− req
184− reqHeaders
170+ in
171+ SignV4Data ts scope canonicalRequest headersToSign output
172+ stringToSign signingKey
185173
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
195174
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− ]
175+mkScope :: UTCTime -> Region -> ByteString
176+mkScope ts region = B.intercalate "/"
177+ [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts
178+ , toS region
179+ , "s3"
180+ , "aws4_request"
181+ ]
206182
207−-- Folds header name, trims whitespace in header values, skips ignored headers
208−-- and sorts headers.
209183getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
210184getHeadersToSign !h =
211− filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
212− map (bimap CI.foldedCase stripBS) h
213−
214−-- | Given the list of headers in the request, computes the canonical headers
215−-- and the signed headers strings.
216−getCanonicalHeaders :: NonEmpty Header -> (ByteString, ByteString)
217−getCanonicalHeaders h =
218− let -- Folds header name, trims spaces in header values, skips ignored
219− -- headers and sorts headers by name (we must not re-order multi-valued
220− -- headers).
221− headersToSign =
222− NE.toList $
223− NE.sortBy (\a b -> compare (fst a) (fst b)) $
224− NE.fromList $
225− NE.filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
226− NE.map (bimap CI.foldedCase stripBS) h
227−
228− canonicalHeaders = mconcat $ map (\(a, b) -> a <> ":" <> b <> "\n") headersToSign
229− signedHeaderKeys = B.intercalate ";" $ map fst headersToSign
230− in (canonicalHeaders, signedHeaderKeys)
231−
232−getCanonicalRequestAndSignedHeaders ::
233− IsStreaming ->
234− SignParams ->
235− NC.Request ->
236− [Header] ->
237− (ByteString, ByteString)
238−getCanonicalRequestAndSignedHeaders isStreaming sp req requestHeaders =
239− let httpMethod = NC.method req
240−
241− canonicalUri = uriEncode False $ NC.path req
242−
243− canonicalQueryString =
244− B.intercalate "&" $
245− map (\(x, y) -> B.concat [x, "=", y]) $
246− sort $
247− map
248− ( bimap (uriEncode True) (maybe "" (uriEncode True))
249− )
250− (parseQuery $ NC.queryString req)
251−
252− (canonicalHeaders, signedHeaderKeys) = getCanonicalHeaders $ NE.fromList requestHeaders
253− payloadHashStr =
254− case isStreaming of
255− IsStreamingLength _ -> "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
256− NotStreaming -> fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
257−
258− canonicalRequest =
259− B.intercalate
260− "\n"
261− [ httpMethod,
262− canonicalUri,
263− canonicalQueryString,
264− canonicalHeaders,
265− signedHeaderKeys,
266− payloadHashStr
267− ]
268− in (canonicalRequest, signedHeaderKeys)
269−
270−mkCanonicalRequest ::
271− Bool ->
272− SignParams ->
273− NC.Request ->
274− [(ByteString, ByteString)] ->
275− ByteString
276−mkCanonicalRequest !isStreaming !sp !req !headersForSign =
277− let httpMethod = NC.method req
278− canonicalUri = uriEncode False $ NC.path req
279− canonicalQueryString =
280− B.intercalate "&" $
281− map (\(x, y) -> B.concat [x, "=", y]) $
282− sortBy (\a b -> compare (fst a) (fst b)) $
283− map
284− ( bimap (uriEncode True) (maybe "" (uriEncode True))
285− )
286− (parseQuery $ NC.queryString req)
287− sortedHeaders = sort headersForSign
288− canonicalHeaders =
289− B.concat $
290− map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
291− signedHeaders = B.intercalate ";" $ map fst sortedHeaders
292− payloadHashStr =
293− if isStreaming
294− then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
295− else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
296− in B.intercalate
297− "\n"
298− [ httpMethod,
299− canonicalUri,
300− canonicalQueryString,
301− canonicalHeaders,
302− signedHeaders,
303− payloadHashStr
304− ]
305−
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
185+ filter (flip Set.notMember ignoredHeaders . fst) $
186+ map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
187+
188+mkCanonicalRequest :: RequestInfo -> [(ByteString, ByteString)]
189+ -> ByteString
190+mkCanonicalRequest !ri !headersForSign =
191+ let
192+ canonicalQueryString = B.intercalate "&" $
193+ map (\(x, y) -> B.concat [x, "=", y]) $
194+ sort $ map (\(x, y) ->
195+ (uriEncode True x, maybe "" (uriEncode True) y)) $
196+ riQueryParams ri
197+
198+ sortedHeaders = sort headersForSign
199+
200+ canonicalHeaders = B.concat $
201+ map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
202+
203+ signedHeaders = B.intercalate ";" $ map fst sortedHeaders
204+
205+ in
206+ B.intercalate "\n"
207+ [ riMethod ri
208+ , uriEncode False $ getPathFromRI ri
209+ , canonicalQueryString
210+ , canonicalHeaders
211+ , signedHeaders
212+ , maybe "UNSIGNED-PAYLOAD" identity $ riPayloadHash ri
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 -> Region -> ByteString -> ByteString
224+mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"
225+ . hmacSHA256RawBS "s3"
226+ . hmacSHA256RawBS (toS region)
227+ . hmacSHA256RawBS (awsDateFormatBS ts)
228+ $ B.concat ["AWS4", secretKey]
323229
324230computeSignature :: ByteString -> ByteString -> ByteString
325231computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -327,168 +233,15 @@ 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
334−signV4PostPolicy !postPolicyJSON !sp =
335− let stringToSign = Base64.encode postPolicyJSON
336− signingKey = getSigningKey sp
337− signature = computeSignature stringToSign signingKey
338− in Map.fromList $
339− [ ("x-amz-signature", signature),
340− ("policy", stringToSign)
341− ]
342− ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
343−
344−chunkSizeConstant :: Int
345−chunkSizeConstant = 64 * 1024
346−
347−-- base16Len computes the number of bytes required to represent @n (> 0)@ in
348−-- hexadecimal.
349−base16Len :: (Integral a) => a -> Int
350−base16Len n
351− | n == 0 = 0
352− | otherwise = 1 + base16Len (n `div` 16)
353−
354−signedStreamLength :: Int64 -> Int64
355−signedStreamLength dataLen =
356− let chunkSzInt = fromIntegral chunkSizeConstant
357− (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
358− -- Structure of a chunk:
359− -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
360− encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
361− fullChunkSize = encodedChunkLen chunkSzInt
362− lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
363− finalChunkSize = 1 + 17 + 64 + 2 + 2
364− in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
365−
366−-- For streaming S3, we need to update the content-encoding header.
367−addContentEncoding :: [Header] -> [Header]
368−addContentEncoding hs =
369− -- assume there is at most one content-encoding header.
370− let (ceHdrs, others) = partition ((== hContentEncoding) . fst) hs
371− in maybe
372− (hContentEncoding, "aws-chunked")
373− (\(k, v) -> (k, v <> ",aws-chunked"))
374− (listToMaybe ceHdrs)
375− : others
376−
377−signV4Stream ::
378− Int64 ->
379− SignParams ->
380− NC.Request ->
381− (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
382−signV4Stream !payloadLength !sp !req =
383− let ts = spTimeStamp sp
384−
385− -- compute the updated list of headers to be added for signing purposes.
386− signedContentLength = signedStreamLength payloadLength
387− extraHeaders =
388− [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
389− ("x-amz-decoded-content-length", showBS payloadLength),
390− ("content-length", showBS signedContentLength),
391− ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
392− ]
393− ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
394− requestHeaders =
395− addContentEncoding $
396− foldr setHeader (NC.requestHeaders req) extraHeaders
397−
398− -- 1. Compute Seed Signature
399− -- 1.1 Canonical Request
400− (canonicalReq, signedHeaderKeys) =
401− getCanonicalRequestAndSignedHeaders
402− (IsStreamingLength payloadLength)
403− sp
404− req
405− requestHeaders
406−
407− scope = credentialScope sp
408− accessKey = spAccessKey sp
409− -- 1.2 String toSign
410− stringToSign = mkStringToSign ts scope canonicalReq
411− -- 1.3 Compute signature
412− -- 1.3.1 compute signing key
413− signingKey = getSigningKey sp
414− -- 1.3.2 Compute signature
415− seedSignature = computeSignature stringToSign signingKey
416− -- 1.3.3 Compute Auth Header
417− authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
418− -- 1.4 Updated headers for the request
419− finalReqHeaders = authHeader : requestHeaders
420− -- headersToAdd = authHeader : datePair : streamingHeaders
421−
422− toHexStr n = B8.pack $ printf "%x" n
423− (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
424− -- Function to compute string to sign for each chunk.
425− chunkStrToSign prevSign currChunkHash =
426− B.intercalate
427− "\n"
428− [ "AWS4-HMAC-SHA256-PAYLOAD",
429− awsTimeFormatBS ts,
430− scope,
431− prevSign,
432− hashSHA256 "",
433− currChunkHash
434− ]
435− -- Read n byte from upstream and return a strict bytestring.
436− mustTakeN n = do
437− bs <- LB.toStrict <$> (C.takeCE n C..| C.sinkLazy)
438− when (B.length bs /= n) $
439− throwIO MErrVStreamingBodyUnexpectedEOF
440− return bs
441− signerConduit n lps prevSign =
442− -- First case encodes a full chunk of length
443− -- 'chunkSizeConstant'.
444− if
445− | n > 0 -> do
446− bs <- mustTakeN chunkSizeConstant
447− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
448− nextSign = computeSignature strToSign signingKey
449− chunkBS =
450− toHexStr chunkSizeConstant
451− <> ";chunk-signature="
452− <> nextSign
453− <> "\r\n"
454− <> bs
455− <> "\r\n"
456− C.yield chunkBS
457− signerConduit (n - 1) lps nextSign
458−
459− -- Second case encodes the last chunk which is smaller than
460− -- 'chunkSizeConstant'
461− | lps > 0 -> do
462− bs <- mustTakeN $ fromIntegral lps
463− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
464− nextSign = computeSignature strToSign signingKey
465− chunkBS =
466− toHexStr lps
467− <> ";chunk-signature="
468− <> nextSign
469− <> "\r\n"
470− <> bs
471− <> "\r\n"
472− C.yield chunkBS
473− signerConduit 0 0 nextSign
474−
475− -- Last case encodes the final signature chunk that has no
476− -- data.
477− | otherwise -> do
478− let strToSign = chunkStrToSign prevSign (hashSHA256 "")
479− nextSign = computeSignature strToSign signingKey
480− lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
481− C.yield lastChunkBS
482− in \src ->
483− req
484− { NC.requestHeaders = finalReqHeaders,
485− NC.requestBody =
486− NC.requestBodySource signedContentLength $
487− src C..| signerConduit numParts lastPSize seedSignature
488− }
489−
490−-- "setHeader r hdr" adds the hdr to r, replacing it in r if it already exists.
491−setHeader :: Header -> RequestHeaders -> RequestHeaders
492−setHeader hdr r =
493− let r' = filter (\(name, _) -> name /= fst hdr) r
494− in hdr : r'
236+signV4PostPolicy :: ByteString -> UTCTime -> ConnectInfo
237+ -> Map.Map Text ByteString
238+signV4PostPolicy !postPolicyJSON !signTime !ci =
239+ let
240+ stringToSign = Base64.encode postPolicyJSON
241+ region = connectRegion ci
242+ signingKey = mkSigningKey signTime region $ toS $ connectSecretKey ci
243+ signature = computeSignature stringToSign signingKey
244+ in
245+ Map.fromList [ ("x-amz-signature", signature)
246+ , ("policy", stringToSign)
247+ ]
src/Network/Minio/Utils.hs+87−167
@@ -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.
@@ -16,146 +16,88 @@
1616
1717module Network.Minio.Utils where
1818
19−import qualified Conduit as C
20−import Control.Monad.IO.Unlift (MonadUnliftIO)
19+import qualified Control.Concurrent.Async.Lifted as A
20+import qualified Control.Concurrent.QSem.Lifted as Q
21+import qualified Control.Exception.Lifted as ExL
22+import qualified Control.Monad.Catch as MC
2123import qualified Control.Monad.Trans.Resource as R
24+
2225import qualified Data.ByteString as B
2326import qualified Data.ByteString.Lazy as LB
24−import Data.CaseInsensitive (mk, original)
27+import qualified Data.Conduit as C
2528import qualified Data.Conduit.Binary as CB
26−import qualified Data.HashMap.Strict as H
2729import 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)
30+import Data.Text.Encoding.Error (lenientDecode)
31+import Data.Text.Read (decimal)
32+import Data.Time
33+import Network.HTTP.Conduit (Response)
3634import qualified Network.HTTP.Conduit as NC
3735import qualified Network.HTTP.Types as HT
3836import 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)
4237import qualified System.IO as IO
43−import qualified UnliftIO as U
44−import qualified UnliftIO.Async as A
38+import Data.CaseInsensitive (mk)
39+
40+
41+import Lib.Prelude
4542
46−allocateReadFile ::
47− (MonadUnliftIO m, R.MonadResource m) =>
48− FilePath ->
49− m (R.ReleaseKey, Handle)
43+import Network.Minio.XmlParser (parseErrResponse)
44+
45+allocateReadFile :: (R.MonadResource m, R.MonadResourceBase m, MonadCatch m)
46+ => FilePath -> m (R.ReleaseKey, Handle)
5047allocateReadFile fp = do
5148 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
52− either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
49+ either (\(e :: IOException) -> throwM e) (return . (rk,)) hdlE
5350 where
54− openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
51+ openReadFile f = ExL.try $ IO.openBinaryFile f IO.ReadMode
5552 cleanup = either (const $ return ()) IO.hClose
5653
5754-- | Queries the file size from the handle. Catches any file operation
5855-- exceptions and returns Nothing instead.
59−getFileSize ::
60− (MonadUnliftIO m) =>
61− Handle ->
62− m (Maybe Int64)
56+getFileSize :: (R.MonadResourceBase m, R.MonadResource m)
57+ => Handle -> m (Maybe Int64)
6358getFileSize h = do
6459 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
6560 case resE of
66− Left (_ :: U.IOException) -> return Nothing
61+ Left (_ :: IOException) -> return Nothing
6762 Right s -> return $ Just s
6863
6964-- | Queries if handle is seekable. Catches any file operation
7065-- exceptions and return False instead.
71−isHandleSeekable ::
72− (R.MonadResource m) =>
73− Handle ->
74− m Bool
66+isHandleSeekable :: (R.MonadResource m, R.MonadResourceBase m)
67+ => Handle -> m Bool
7568isHandleSeekable h = do
7669 resE <- liftIO $ try $ IO.hIsSeekable h
7770 case resE of
78− Left (_ :: U.IOException) -> return False
71+ Left (_ :: IOException) -> return False
7972 Right v -> return v
8073
8174-- | Helper function that opens a handle to the filepath and performs
8275-- the given action on it. Exceptions of type MError are caught and
8376-- returned - both during file handle allocation and when the action
8477-- is run.
85−withNewHandle ::
86− (MonadUnliftIO m, R.MonadResource m) =>
87− FilePath ->
88− (Handle -> m a) ->
89− m (Either U.IOException a)
78+withNewHandle :: (R.MonadResourceBase m, R.MonadResource m, MonadCatch m)
79+ => FilePath -> (Handle -> m a) -> m (Either IOException a)
9080withNewHandle fp fileAction = do
9181 -- opening a handle can throw MError exception.
92− handleE <- try $ allocateReadFile fp
82+ handleE <- MC.try $ allocateReadFile fp
9383 either (return . Left) doAction handleE
9484 where
9585 doAction (rkey, h) = do
9686 -- fileAction may also throw MError exception, so we catch and
9787 -- return it.
98− resE <- try $ fileAction h
88+ resE <- MC.try $ fileAction h
9989 R.release rkey
10090 return resE
10191
10292mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
103−mkHeaderFromPairs = map (first mk)
93+mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
10494
10595lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
106−lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
96+lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
10797
10898getETagHeader :: [HT.Header] -> Maybe Text
10999getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
110100
111−getMetadata :: [HT.Header] -> [(Text, Text)]
112−getMetadata =
113− map (\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y))
114−
115−-- | If the given header name has the @X-Amz-Meta-@ prefix, it is
116−-- stripped and a Just is returned.
117−userMetadataHeaderNameMaybe :: Text -> Maybe Text
118−userMetadataHeaderNameMaybe k =
119− let prefix = T.toCaseFold "X-Amz-Meta-"
120− n = T.length prefix
121− in if T.toCaseFold (T.take n k) == prefix
122− then Just (T.drop n k)
123− else Nothing
124−
125−toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
126−toMaybeMetadataHeader (k, v) =
127− (,v) <$> userMetadataHeaderNameMaybe k
128−
129−getNonUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
130−getNonUserMetadataMap =
131− H.fromList
132− . filter
133− ( isNothing
134− . userMetadataHeaderNameMaybe
135− . fst
136− )
137−
138−addXAmzMetaPrefix :: Text -> Text
139−addXAmzMetaPrefix s
140− | isJust (userMetadataHeaderNameMaybe s) = s
141− | otherwise = "X-Amz-Meta-" <> s
142−
143−mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
144−mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix x, encodeUtf8 y))
145−
146−-- | This function collects all headers starting with `x-amz-meta-`
147−-- and strips off this prefix, and returns a map.
148−getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
149−getUserMetadataMap =
150− H.fromList
151− . mapMaybe toMaybeMetadataHeader
152−
153−getHostHeader :: (ByteString, Int) -> ByteString
154−getHostHeader (host_, port_) =
155− if port_ == 80 || port_ == 443
156− then host_
157− else host_ <> ":" <> show port_
158−
159101getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
160102getLastModifiedHeader hs = do
161103 modTimebs <- decodeUtf8Lenient <$> lookupHeader Hdr.hLastModified hs
@@ -164,103 +106,77 @@ getLastModifiedHeader hs = do
164106getContentLength :: [HT.Header] -> Maybe Int64
165107getContentLength hs = do
166108 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
167− fst <$> either (const Nothing) Just (decimal nbs)
109+ fst <$> hush (decimal nbs)
110+
168111
169112decodeUtf8Lenient :: ByteString -> Text
170113decodeUtf8Lenient = decodeUtf8With lenientDecode
171114
172115isSuccessStatus :: HT.Status -> Bool
173−isSuccessStatus sts =
174− let s = HT.statusCode sts
175− in (s >= 200 && s < 300)
116+isSuccessStatus sts = let s = HT.statusCode sts
117+ in (s >= 200 && s < 300)
176118
177−httpLbs ::
178− (MonadIO m) =>
179− NC.Request ->
180− NC.Manager ->
181− m (NC.Response LByteString)
119+httpLbs :: (R.MonadThrow m, MonadIO m)
120+ => NC.Request -> NC.Manager
121+ -> m (NC.Response LByteString)
182122httpLbs req mgr = do
183123 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
184− resp <- either throwIO return respE
124+ resp <- either throwM return respE
185125 unless (isSuccessStatus $ NC.responseStatus resp) $
186126 case contentTypeMay resp of
187127 Just "application/xml" -> do
188128 sErr <- parseErrResponse $ NC.responseBody resp
189− throwIO sErr
190− Just "application/json" -> do
191− sErr <- parseErrResponseJSON $ NC.responseBody resp
192− throwIO sErr
193− _ ->
194− throwIO $
195− NC.HttpExceptionRequest req $
196− NC.StatusCodeException (void resp) (showBS resp)
129+ throwM sErr
130+
131+ _ -> throwM $ NC.HttpExceptionRequest req $
132+ NC.StatusCodeException (void resp) (show resp)
197133
198134 return resp
199135 where
200− tryHttpEx ::
201− IO (NC.Response LByteString) ->
202− IO (Either NC.HttpException (NC.Response LByteString))
136+ tryHttpEx :: IO (NC.Response LByteString)
137+ -> IO (Either NC.HttpException (NC.Response LByteString))
203138 tryHttpEx = try
204− contentTypeMay resp =
205− lookupHeader Hdr.hContentType $
206− NC.responseHeaders resp
139+ contentTypeMay resp = lookupHeader Hdr.hContentType $
140+ NC.responseHeaders resp
207141
208−http ::
209− (MonadUnliftIO m, R.MonadResource m) =>
210− NC.Request ->
211− NC.Manager ->
212− m (Response (C.ConduitT () ByteString m ()))
142+http :: (R.MonadResourceBase m, R.MonadResource m)
143+ => NC.Request -> NC.Manager
144+ -> m (Response (C.ResumableSource m ByteString))
213145http req mgr = do
214146 respE <- tryHttpEx $ NC.http req mgr
215− resp <- either throwIO return respE
147+ resp <- either throwM return respE
216148 unless (isSuccessStatus $ NC.responseStatus resp) $
217149 case contentTypeMay resp of
218150 Just "application/xml" -> do
219− respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
151+ respBody <- NC.responseBody resp C.$$+- CB.sinkLbs
220152 sErr <- parseErrResponse respBody
221− throwIO sErr
153+ throwM sErr
154+
222155 _ -> do
223156 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
224− throwIO $
225− NC.HttpExceptionRequest req $
226− NC.StatusCodeException (void resp) content
157+ throwM $ NC.HttpExceptionRequest req $
158+ NC.StatusCodeException (void resp) content
159+
227160
228161 return resp
229162 where
230− tryHttpEx ::
231− (MonadUnliftIO m) =>
232− m a ->
233− m (Either NC.HttpException a)
234− tryHttpEx = try
235− contentTypeMay resp =
236− lookupHeader Hdr.hContentType $
237− NC.responseHeaders resp
163+ tryHttpEx :: (R.MonadResourceBase m) => m a
164+ -> m (Either NC.HttpException a)
165+ tryHttpEx = ExL.try
166+ contentTypeMay resp = lookupHeader Hdr.hContentType $ NC.responseHeaders resp
238167
239168-- Similar to mapConcurrently but limits the number of threads that
240169-- can run using a quantity semaphore.
241−limitedMapConcurrently ::
242− (MonadUnliftIO m) =>
243− Int ->
244− (t -> m a) ->
245− [t] ->
246− m [a]
247−limitedMapConcurrently 0 _ _ = return []
170+limitedMapConcurrently :: (MonadIO m, R.MonadBaseControl IO m)
171+ => Int -> (t -> m a) -> [t] -> m [a]
248172limitedMapConcurrently count act args = do
249− t' <- U.newTVarIO count
250− threads <- mapM (A.async . wThread t') args
173+ qSem <- liftIO $ Q.newQSem count
174+ threads <- mapM (A.async . wThread qSem) args
251175 mapM A.wait threads
252176 where
253− wThread t arg =
254− U.bracket_ (waitSem t) (signalSem t) $ act arg
255− -- quantity semaphore implementation using TVar
256− waitSem t = U.atomically $ do
257− v <- U.readTVar t
258− if v > 0
259− then U.writeTVar t (v - 1)
260− else U.retrySTM
261− signalSem t = U.atomically $ do
262− v <- U.readTVar t
263− U.writeTVar t (v + 1)
177+ -- grab 1 unit from semaphore, run action and release it
178+ wThread qs arg =
179+ ExL.bracket_ (Q.waitQSem qs) (Q.signalQSem qs) $ act arg
264180
265181-- helper function to 'drop' empty optional parameter.
266182mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -269,17 +185,21 @@ mkQuery k mv = (k,) <$> mv
269185-- helper function to build query parameters that are optional.
270186-- don't use it with mandatory query params with empty value.
271187mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
272−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
188+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
273189
274−-- | Conduit that rechunks bytestrings into the given chunk
275−-- lengths. Stops after given chunk lengths are yielded. Stops if
276−-- there are no more chunks to yield or if a shorter chunk is
277−-- received. Does not throw any errors.
278−chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
279−chunkBSConduit [] = return ()
280−chunkBSConduit (s : ss) = do
281− bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
282− if
283− | B.length bs == s -> C.yield bs >> chunkBSConduit ss
284− | B.length bs > 0 -> C.yield bs
285− | otherwise -> return ()
190+chunkBSConduit :: (Monad m, Integral a)
191+ => [a] -> C.Conduit ByteString m ByteString
192+chunkBSConduit s = loop 0 [] s
193+ where
194+ loop _ _ [] = return ()
195+ loop n readChunks (size:sizes) = do
196+ bsMay <- C.await
197+ case bsMay of
198+ Nothing -> when (n > 0) $ C.yield $ B.concat readChunks
199+ Just bs -> if n + fromIntegral (B.length bs) >= size
200+ then do let (a, b) = B.splitAt (fromIntegral $ size - n) bs
201+ chunkBS = B.concat $ readChunks ++ [a]
202+ C.yield chunkBS
203+ loop (fromIntegral $ B.length b) [b] sizes
204+ else loop (n + fromIntegral (B.length bs))
205+ (readChunks ++ [bs]) (size:sizes)
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+54−183
@@ -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,217 +15,88 @@
1515--
1616
1717module Network.Minio.XmlGenerator
18− ( mkCreateBucketConfig,
19− mkCompleteMultipartUploadRequest,
20− mkPutNotificationRequest,
21− mkSelectRequest,
22− )
23−where
18+ ( mkCreateBucketConfig
19+ , mkCompleteMultipartUploadRequest
20+ , mkPutNotificationRequest
21+ ) where
22+
2423
2524import 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
25+import qualified Data.Map as M
26+import qualified Data.Text as T
27+import Text.XML
28+
29+import Lib.Prelude
30+
31+import Network.Minio.Data
32+
3033
3134-- | Create a bucketConfig request body XML
32−mkCreateBucketConfig :: Text -> Region -> ByteString
33−mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
35+mkCreateBucketConfig :: Region -> ByteString
36+mkCreateBucketConfig location = LBS.toStrict $ renderLBS def bucketConfig
3437 where
35− s3Element n = Element (s3Name ns n) mempty
36− root =
37− s3Element
38− "CreateBucketConfiguration"
39− [ NodeElement $
40− s3Element
41− "LocationConstraint"
42− [NodeContent location]
38+ s3Element n = Element (s3Name n) M.empty
39+ root = s3Element "CreateBucketConfiguration"
40+ [ NodeElement $ s3Element "LocationConstraint"
41+ [ NodeContent location]
4342 ]
44− bucketConfig = Document (Prologue [] Nothing []) root []
43+ bucketConfig = Document (Prologue [] Nothing []) root []
4544
4645-- | Create a completeMultipartUpload request body XML
4746mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
4847mkCompleteMultipartUploadRequest partInfo =
4948 LBS.toStrict $ renderLBS def cmur
5049 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− ]
50+ root = Element "CompleteMultipartUpload" M.empty $
51+ map (NodeElement . mkPart) partInfo
52+ mkPart (n, etag) = Element "Part" M.empty
53+ [ NodeElement $ Element "PartNumber" M.empty
54+ [NodeContent $ T.pack $ show n]
55+ , NodeElement $ Element "ETag" M.empty
56+ [NodeContent etag]
57+ ]
6958 cmur = Document (Prologue [] Nothing []) root []
7059
7160-- Simplified XML representation without element attributes.
72−data XNode
73− = XNode Text [XNode]
74− | XLeaf Text Text
75− deriving stock (Eq, Show)
76−
77−toXML :: Text -> XNode -> ByteString
78−toXML ns node =
79− LBS.toStrict $
80− renderLBS def $
81− Document (Prologue [] Nothing []) (xmlNode node) []
61+data XNode = XNode Text [XNode]
62+ | XLeaf Text Text
63+ deriving (Eq, Show)
64+
65+toXML :: XNode -> ByteString
66+toXML node = LBS.toStrict $ renderLBS def $
67+ Document (Prologue [] Nothing []) (xmlNode node) []
8268 where
8369 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]
70+ xmlNode (XNode name nodes) = Element (s3Name name) M.empty $
71+ map (NodeElement . xmlNode) nodes
72+ xmlNode (XLeaf name content) = Element (s3Name name) M.empty
73+ [NodeContent content]
9274
9375class ToXNode a where
9476 toXNode :: a -> XNode
9577
9678instance ToXNode Event where
97− toXNode = XLeaf "Event" . toText
79+ toXNode = XLeaf "Event" . show
9880
9981instance 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
82+ toXNode (Notification qc tc lc) = XNode "NotificationConfiguration" $
83+ map (toXNodesWithArnName "QueueConfiguration" "Queue") qc ++
84+ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc ++
85+ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
10586
10687toXNodesWithArnName :: 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]
88+toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
89+ XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events ++
90+ [toXNode fRule]
11291
11392instance ToXNode Filter where
11493 toXNode (Filter (FilterKey (FilterRules rules))) =
11594 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
11695
11796getFRXNode :: FilterRule -> XNode
118−getFRXNode (FilterRule n v) =
119− XNode
120− "FilterRule"
121− [ XLeaf "Name" n,
122− XLeaf "Value" v
123− ]
124−
125−mkPutNotificationRequest :: Text -> Notification -> ByteString
126−mkPutNotificationRequest ns = toXML ns . toXNode
127−
128−mkSelectRequest :: SelectRequest -> ByteString
129−mkSelectRequest r = LBS.toStrict $ renderLBS def sr
130− where
131− sr = Document (Prologue [] Nothing []) root []
132− root =
133− Element "SelectRequest" mempty $
134− [ NodeElement
135− ( Element
136− "Expression"
137− mempty
138− [NodeContent $ srExpression r]
139− ),
140− NodeElement
141− ( Element
142− "ExpressionType"
143− mempty
144− [NodeContent $ show $ srExpressionType r]
145− ),
146− NodeElement
147− ( Element "InputSerialization" mempty $
148− inputSerializationNodes $
149− srInputSerialization r
150− ),
151− NodeElement
152− ( Element "OutputSerialization" mempty $
153− outputSerializationNodes $
154− srOutputSerialization r
155− )
156− ]
157− ++ maybe [] reqProgElem (srRequestProgressEnabled r)
158− reqProgElem enabled =
159− [ NodeElement
160− ( Element
161− "RequestProgress"
162− mempty
163− [ NodeElement
164− ( Element
165− "Enabled"
166− mempty
167− [ NodeContent
168− (if enabled then "TRUE" else "FALSE")
169− ]
170− )
171− ]
172− )
173− ]
174− inputSerializationNodes is =
175− comprTypeNode (isCompressionType is)
176− ++ [NodeElement $ formatNode (isFormatInfo is)]
177− comprTypeNode (Just c) =
178− [ NodeElement $
179− Element
180− "CompressionType"
181− mempty
182− [ NodeContent $ case c of
183− CompressionTypeNone -> "NONE"
184− CompressionTypeGzip -> "GZIP"
185− CompressionTypeBzip2 -> "BZIP2"
186− ]
187− ]
188− comprTypeNode Nothing = []
189− kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
190− formatNode (InputFormatCSV c) =
191− Element
192− "CSV"
193− mempty
194− (map (NodeElement . kvElement) (csvPropsList c))
195− formatNode (InputFormatJSON p) =
196− Element
197− "JSON"
198− mempty
199− [ NodeElement
200− ( Element
201− "Type"
202− mempty
203− [ NodeContent $ case jsonipType p of
204− JSONTypeDocument -> "DOCUMENT"
205− JSONTypeLines -> "LINES"
206− ]
207− )
208− ]
209− formatNode InputFormatParquet = Element "Parquet" mempty []
210− outputSerializationNodes (OutputSerializationJSON j) =
211− [ NodeElement
212− ( Element "JSON" mempty $
213− rdElem $
214− jsonopRecordDelimiter j
215− )
216− ]
217− outputSerializationNodes (OutputSerializationCSV c) =
218− [ NodeElement $
219− Element
220− "CSV"
221− mempty
222− (map (NodeElement . kvElement) (csvPropsList c))
223− ]
224− rdElem Nothing = []
225− rdElem (Just t) =
226− [ NodeElement $
227− Element
228− "RecordDelimiter"
229− mempty
230− [NodeContent t]
231− ]
97+getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
98+ , XLeaf "Value" v
99+ ]
100+
101+mkPutNotificationRequest :: Notification -> ByteString
102+mkPutNotificationRequest = toXML . toXNode
src/Network/Minio/XmlParser.hs+154−142
@@ -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,216 +15,228 @@
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
32−
33−import qualified Data.ByteString.Lazy as LB
34−import qualified Data.HashMap.Strict as H
35−import Data.List (zip4, zip6)
36−import qualified Data.Text as T
37−import Data.Time
38−import Network.Minio.Data
39−import Network.Minio.XmlCommon
40−import Text.XML.Cursor hiding (bool)
18+ ( parseListBuckets
19+ , parseLocation
20+ , parseNewMultipartUpload
21+ , parseCompleteMultipartUploadResponse
22+ , parseCopyObjectResponse
23+ , parseListObjectsResponse
24+ , parseListObjectsV1Response
25+ , parseListUploadsResponse
26+ , parseListPartsResponse
27+ , parseErrResponse
28+ , parseNotification
29+ ) where
30+
31+import Control.Monad.Trans.Resource
32+import Data.List (zip3, zip4)
33+import qualified Data.Text as T
34+import Data.Text.Read (decimal)
35+import Data.Time
36+import Text.XML
37+import Text.XML.Cursor hiding (bool)
38+
39+import Lib.Prelude
40+
41+import Network.Minio.Data
42+import Network.Minio.Errors
43+
44+
45+-- | Represent the time format string returned by S3 API calls.
46+s3TimeFormat :: [Char]
47+s3TimeFormat = iso8601DateFormat $ Just "%T%QZ"
48+
49+-- | Helper functions.
50+uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
51+uncurry4 f (a, b, c, d) = f a b c d
52+
53+-- | Parse time strings from XML
54+parseS3XMLTime :: (MonadThrow m) => Text -> m UTCTime
55+parseS3XMLTime = either (throwM . MErrVXmlParse) return
56+ . parseTimeM True defaultTimeLocale s3TimeFormat
57+ . T.unpack
58+
59+parseDecimal :: (MonadThrow m, Integral a) => Text -> m a
60+parseDecimal numStr = either (throwM . MErrVXmlParse . show) return $
61+ fst <$> decimal numStr
62+
63+parseDecimals :: (MonadThrow m, Integral a) => [Text] -> m [a]
64+parseDecimals numStr = forM numStr parseDecimal
65+
66+s3Elem :: Text -> Axis
67+s3Elem = element . s3Name
68+
69+parseRoot :: (MonadThrow m) => LByteString -> m Cursor
70+parseRoot = either (throwM . MErrVXmlParse . show) (return . fromDocument)
71+ . parseLBS def
4172
4273-- | Parse the response XML of a list buckets call.
43−parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
74+parseListBuckets :: (MonadThrow m) => LByteString -> m [BucketInfo]
4475parseListBuckets xmldata = do
4576 r <- parseRoot xmldata
46− ns <- asks getSvcNamespace
47− let s3Elem' = s3Elem ns
48− names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
49− timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
77+ let
78+ names = r $// s3Elem "Bucket" &// s3Elem "Name" &/ content
79+ timeStrings = r $// s3Elem "Bucket" &// s3Elem "CreationDate" &/ content
5080
5181 times <- mapM parseS3XMLTime timeStrings
5282 return $ zipWith BucketInfo names times
5383
5484-- | Parse the response XML of a location request.
55−parseLocation :: (MonadIO m) => LByteString -> m Region
85+parseLocation :: (MonadThrow m) => LByteString -> m Region
5686parseLocation xmldata = do
5787 r <- parseRoot xmldata
5888 let region = T.concat $ r $/ content
5989 return $ bool "us-east-1" region $ region /= ""
6090
6191-- | Parse the response XML of an newMultipartUpload call.
62−parseNewMultipartUpload :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m UploadId
92+parseNewMultipartUpload :: (MonadThrow m) => LByteString -> m UploadId
6393parseNewMultipartUpload xmldata = do
6494 r <- parseRoot xmldata
65− ns <- asks getSvcNamespace
66− let s3Elem' = s3Elem ns
67− return $ T.concat $ r $// s3Elem' "UploadId" &/ content
95+ return $ T.concat $ r $// s3Elem "UploadId" &/ content
6896
6997-- | Parse the response XML of completeMultipartUpload call.
70−parseCompleteMultipartUploadResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ETag
98+parseCompleteMultipartUploadResponse :: (MonadThrow m) => LByteString -> m ETag
7199parseCompleteMultipartUploadResponse xmldata = do
72100 r <- parseRoot xmldata
73− ns <- asks getSvcNamespace
74− let s3Elem' = s3Elem ns
75− return $ T.concat $ r $// s3Elem' "ETag" &/ content
101+ return $ T.concat $ r $// s3Elem "ETag" &/ content
76102
77103-- | Parse the response XML of copyObject and copyObjectPart
78−parseCopyObjectResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m (ETag, UTCTime)
104+parseCopyObjectResponse :: (MonadThrow m) => LByteString -> m (ETag, UTCTime)
79105parseCopyObjectResponse xmldata = do
80106 r <- parseRoot xmldata
81− ns <- asks getSvcNamespace
82− let s3Elem' = s3Elem ns
83− mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
107+ let
108+ mtimeStr = T.concat $ r $// s3Elem "LastModified" &/ content
84109
85110 mtime <- parseS3XMLTime mtimeStr
86− return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
111+ return (T.concat $ r $// s3Elem "ETag" &/ content, mtime)
87112
88113-- | 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
114+parseListObjectsV1Response :: (MonadThrow m)
115+ => LByteString -> m ListObjectsV1Result
93116parseListObjectsV1Response xmldata = do
94117 r <- parseRoot xmldata
95− ns <- asks getSvcNamespace
96− let s3Elem' = s3Elem ns
97− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
98− nextMarker = listToMaybe $ r $/ s3Elem' "NextMarker" &/ content
99− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
100− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
101− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
102− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
103− -- if response xml contains empty etag response fill them with as
104− -- many empty Text for the zip4 below to work as intended.
105− etags = etagsList ++ repeat ""
106− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
118+ let
119+ hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
120+
121+ nextMarker = headMay $ r $/ s3Elem "NextMarker" &/ content
122+
123+ prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content
124+
125+ keys = r $/ s3Elem "Contents" &/ s3Elem "Key" &/ content
126+ modTimeStr = r $/ s3Elem "Contents" &/ s3Elem "LastModified" &/ content
127+ etagsList = r $/ s3Elem "Contents" &/ s3Elem "ETag" &/ content
128+ -- if response xml contains empty etag response fill them with as
129+ -- many empty Text for the zip4 below to work as intended.
130+ etags = etagsList ++ repeat ""
131+ sizeStr = r $/ s3Elem "Contents" &/ s3Elem "Size" &/ content
107132
108133 modTimes <- mapM parseS3XMLTime modTimeStr
109134 sizes <- parseDecimals sizeStr
110135
111− let objects =
112− map (uncurry6 ObjectInfo) $
113− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
136+ let
137+ objects = map (uncurry4 ObjectInfo) $ zip4 keys modTimes etags sizes
114138
115139 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
116140
117141-- | Parse the response XML of a list objects call.
118−parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ListObjectsResult
142+parseListObjectsResponse :: (MonadThrow m) => LByteString -> m ListObjectsResult
119143parseListObjectsResponse xmldata = do
120144 r <- parseRoot xmldata
121− ns <- asks getSvcNamespace
122− let s3Elem' = s3Elem ns
123− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
124− nextToken = listToMaybe $ r $/ s3Elem' "NextContinuationToken" &/ content
125− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
126− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
127− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
128− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
129− -- if response xml contains empty etag response fill them with as
130− -- many empty Text for the zip4 below to work as intended.
131− etags = etagsList ++ repeat ""
132− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
145+ let
146+ hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
147+
148+ nextToken = headMay $ r $/ s3Elem "NextContinuationToken" &/ content
149+
150+ prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content
151+
152+ keys = r $/ s3Elem "Contents" &/ s3Elem "Key" &/ content
153+ modTimeStr = r $/ s3Elem "Contents" &/ s3Elem "LastModified" &/ content
154+ etagsList = r $/ s3Elem "Contents" &/ s3Elem "ETag" &/ content
155+ -- if response xml contains empty etag response fill them with as
156+ -- many empty Text for the zip4 below to work as intended.
157+ etags = etagsList ++ repeat ""
158+ sizeStr = r $/ s3Elem "Contents" &/ s3Elem "Size" &/ content
133159
134160 modTimes <- mapM parseS3XMLTime modTimeStr
135161 sizes <- parseDecimals sizeStr
136162
137− let objects =
138− map (uncurry6 ObjectInfo) $
139− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
163+ let
164+ objects = map (uncurry4 ObjectInfo) $ zip4 keys modTimes etags sizes
140165
141166 return $ ListObjectsResult hasMore nextToken objects prefixes
142167
143168-- | Parse the response XML of a list incomplete multipart upload call.
144−parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ListUploadsResult
169+parseListUploadsResponse :: (MonadThrow m) => LByteString -> m ListUploadsResult
145170parseListUploadsResponse xmldata = do
146171 r <- parseRoot xmldata
147− ns <- asks getSvcNamespace
148− let s3Elem' = s3Elem ns
149− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
150− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
151− nextKey = listToMaybe $ r $/ s3Elem' "NextKeyMarker" &/ content
152− nextUpload = listToMaybe $ r $/ s3Elem' "NextUploadIdMarker" &/ content
153− uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
154− uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
155− uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
172+ let
173+ hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
174+ prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content
175+ nextKey = headMay $ r $/ s3Elem "NextKeyMarker" &/ content
176+ nextUpload = headMay $ r $/ s3Elem "NextUploadIdMarker" &/ content
177+ uploadKeys = r $/ s3Elem "Upload" &/ s3Elem "Key" &/ content
178+ uploadIds = r $/ s3Elem "Upload" &/ s3Elem "UploadId" &/ content
179+ uploadInitTimeStr = r $/ s3Elem "Upload" &/ s3Elem "Initiated" &/ content
156180
157181 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
158182
159− let uploads = zip3 uploadKeys uploadIds uploadInitTimes
183+ let
184+ uploads = zip3 uploadKeys uploadIds uploadInitTimes
160185
161186 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
162187
163−parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ListPartsResult
188+parseListPartsResponse :: (MonadThrow m) => LByteString -> m ListPartsResult
164189parseListPartsResponse xmldata = do
165190 r <- parseRoot xmldata
166− ns <- asks getSvcNamespace
167− let s3Elem' = s3Elem ns
168− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
169− nextPartNumStr = listToMaybe $ r $/ s3Elem' "NextPartNumberMarker" &/ content
170− partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
171− partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
172− partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
173− partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
191+ let
192+ hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
193+ nextPartNumStr = headMay $ r $/ s3Elem "NextPartNumberMarker" &/ content
194+ partNumberStr = r $/ s3Elem "Part" &/ s3Elem "PartNumber" &/ content
195+ partModTimeStr = r $/ s3Elem "Part" &/ s3Elem "LastModified" &/ content
196+ partETags = r $/ s3Elem "Part" &/ s3Elem "ETag" &/ content
197+ partSizeStr = r $/ s3Elem "Part" &/ s3Elem "Size" &/ content
174198
175199 partModTimes <- mapM parseS3XMLTime partModTimeStr
176200 partSizes <- parseDecimals partSizeStr
177201 partNumbers <- parseDecimals partNumberStr
178202 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
179203
180− let partInfos =
181− map (uncurry4 ObjectPartInfo) $
182− zip4 partNumbers partETags partSizes partModTimes
204+ let
205+ partInfos = map (uncurry4 ObjectPartInfo) $
206+ zip4 partNumbers partETags partSizes partModTimes
183207
184208 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
185209
186−parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
210+
211+parseErrResponse :: (MonadThrow m) => LByteString -> m ServiceErr
212+parseErrResponse xmldata = do
213+ r <- parseRoot xmldata
214+ let code = T.concat $ r $/ element "Code" &/ content
215+ message = T.concat $ r $/ element "Message" &/ content
216+ return $ toServiceErr code message
217+
218+parseNotification :: (MonadThrow m) => LByteString -> m Notification
187219parseNotification xmldata = do
188220 r <- parseRoot xmldata
189− ns <- asks getSvcNamespace
190− let s3Elem' = s3Elem ns
191− qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
192− tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
193− lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
194− Notification
195− <$> mapM (parseNode ns "Queue") qcfg
196− <*> mapM (parseNode ns "Topic") tcfg
197− <*> mapM (parseNode ns "CloudFunction") lcfg
221+ let qcfg = map node $ r $/ s3Elem "QueueConfiguration"
222+ tcfg = map node $ r $/ s3Elem "TopicConfiguration"
223+ lcfg = map node $ r $/ s3Elem "CloudFunctionConfiguration"
224+ Notification <$> (mapM (parseNode "Queue") qcfg)
225+ <*> (mapM (parseNode "Topic") tcfg)
226+ <*> (mapM (parseNode "CloudFunction") lcfg)
198227 where
199− getFilterRule ns c =
200− let name = T.concat $ c $/ s3Elem ns "Name" &/ content
201− value = T.concat $ c $/ s3Elem ns "Value" &/ content
202− in FilterRule name value
203− parseNode ns arnName nodeData = do
228+
229+ getFilterRule c =
230+ let name = T.concat $ c $/ s3Elem "Name" &/ content
231+ value = T.concat $ c $/ s3Elem "Value" &/ content
232+ in FilterRule name value
233+
234+ parseNode arnName nodeData = do
204235 let c = fromNode nodeData
205− itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
206− arn = T.concat $ c $/ s3Elem ns arnName &/ content
207− events = mapMaybe textToEvent (c $/ s3Elem ns "Event" &/ content)
208− rules =
209− c
210− $/ s3Elem ns "Filter"
211− &/ s3Elem ns "S3Key"
212− &/ s3Elem ns "FilterRule"
213− &| getFilterRule ns
214− return $
215− NotificationConfig
216− itemId
217− arn
218− events
219− (Filter $ FilterKey $ FilterRules rules)
220−
221−parseSelectProgress :: (MonadIO m) => ByteString -> m Progress
222−parseSelectProgress xmldata = do
223− r <- parseRoot $ LB.fromStrict xmldata
224− let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
225− bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content
226− bReturned = T.concat $ r $/ element "BytesReturned" &/ content
227− Progress
228− <$> parseDecimal bScanned
229− <*> parseDecimal bProcessed
230− <*> parseDecimal bReturned
236+ id = T.concat $ c $/ s3Elem "Id" &/ content
237+ arn = T.concat $ c $/ s3Elem arnName &/ content
238+ events = catMaybes $ map textToEvent $ c $/ s3Elem "Event" &/ content
239+ rules = c $/ s3Elem "Filter" &/ s3Elem "S3Key" &/
240+ s3Elem "FilterRule" &| getFilterRule
241+ return $ NotificationConfig id arn events
242+ (Filter $ FilterKey $ FilterRules rules)
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-9.1
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+478−1135
@@ -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.
@@ -14,34 +14,38 @@
1414-- limitations under the License.
1515--
1616
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
17+import qualified Test.QuickCheck as Q
18+import Test.Tasty
19+import Test.Tasty.HUnit
20+import Test.Tasty.QuickCheck as QC
21+
22+import Lib.Prelude
23+
24+import System.Directory (getTemporaryDirectory)
25+import qualified System.IO as SIO
26+
27+import qualified Control.Monad.Catch as MC
28+import qualified Control.Monad.Trans.Resource as R
29+import qualified Data.ByteString as BS
30+import Data.Conduit (yield, ($$))
31+import qualified Data.Conduit as C
32+import qualified Data.Conduit.Binary as CB
33+import Data.Conduit.Combinators (sinkList)
34+import Data.Default (Default (..))
35+import qualified Data.Map.Strict as Map
36+import qualified Data.Text as T
37+import qualified Data.Time as Time
2938import 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
39+import qualified Network.HTTP.Conduit as NC
40+import qualified Network.HTTP.Types as HT
41+import System.Environment (lookupEnv)
42+
43+import Network.Minio
44+import Network.Minio.Data
45+import Network.Minio.ListOps
46+import Network.Minio.PutObject
47+import Network.Minio.S3API
48+import Network.Minio.Utils
4549
4650main :: IO ()
4751main = defaultMain tests
@@ -49,574 +53,451 @@ 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.Producer m ByteString
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+ oneMiB = 1024*1024
61+
62+ concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++
63+ [BS.take (fromIntegral r) bs]
64+ where (q, r) = n `divMod` fromIntegral (BS.length bs)
65+
10766 genBS s = do
10867 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
10968 let byteArr64 = BS.pack w8s
11069 if s < oneMiB
11170 then yield $ concatIt byteArr64 s
112− else do
113− yield $ concatIt byteArr64 oneMiB
114− genBS (s - oneMiB)
71+ else do yield $ concatIt byteArr64 oneMiB
72+ genBS (s - oneMiB)
11573
116−mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
74+mkRandFile :: R.MonadResource m => Int64 -> m FilePath
11775mkRandFile size = do
118− dir <- liftIO getTemporaryDirectory
119− C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
76+ dir <- liftIO $ getTemporaryDirectory
77+ randomDataSrc size C.$$ CB.sinkTempFile dir "miniohstest.random"
12078
12179funTestBucketPrefix :: Text
12280funTestBucketPrefix = "miniohstest-"
12381
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
82+funTestWithBucket :: TestName
83+ -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree
14784funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
14885 -- generate a random name for the bucket
14986 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
15087 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
15188 liftStep = liftIO . step
152− connInfo <- loadTestServerConnInfo
89+ connInfo <- maybe minioPlayCI (const def) <$> lookupEnv "MINIO_LOCAL"
15390 ret <- runMinio connInfo $ do
15491 liftStep $ "Creating bucket for test - " ++ t
15592 foundBucket <- bucketExists b
15693 liftIO $ foundBucket @?= False
157− makeBucket b Nothing
94+ makeBucket b def
15895 minioTest liftStep b
15996 deleteBucket b
16097 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
16198
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')
99+lowLevelMultipartTest :: TestTree
100+lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
101+ \step bucket -> do
102+ -- low-level multipart operation tests.
103+ let object = "newmpupload"
104+ mb15 = 15 * 1024 * 1024
172105
173−liveServerUnitTests :: TestTree
174−liveServerUnitTests =
175− testGroup
176− "Unit tests against a live server"
177− [ basicTests,
178− listingTest,
179− highLevelListingTest,
180− lowLevelMultipartTest,
181− putObjectSizeTest,
182− putObjectNoSizeTest,
183− multipartTest,
184− putObjectContentTypeTest,
185− putObjectContentLanguageTest,
186− putObjectStorageClassTest,
187− putObjectUserMetadataTest,
188− getObjectTest,
189− copyObjectTests,
190− presignedUrlFunTest,
191− presignedPostPolicyFunTest,
192− bucketPolicyFunTest,
193− getNPutSSECTest,
194− assumeRoleRequestTest
195− ]
106+ step "Prepare for low-level multipart tests."
107+ step "create new multipart upload"
108+ uid <- newMultipartUpload bucket object []
109+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
196110
197−basicTests :: TestTree
198−basicTests = funTestWithBucket "Basic tests" $
199− \step bucket -> do
200− step "getService works and contains the test bucket."
201− buckets <- getService
202− unless (length (filter (== bucket) $ map biName buckets) == 1) $
203− liftIO $
204− assertFailure
205− ( "The bucket "
206− ++ show bucket
207− ++ " was expected to exist."
208− )
209−
210− step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
211− mbE <- try $ makeBucket bucket Nothing
212− case mbE of
213− Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
214− _ -> return ()
215−
216− step "makeBucket with an invalid bucket name and check for appropriate exception."
217− invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
218− case invalidMBE of
219− Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
220− _ -> return ()
221−
222− step "getLocation works"
223− region <- getLocation bucket
224− liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
225−
226− step "singlepart putObject works"
227− testFilepath <- mkRandFile 200
228− fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
229−
230− step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
231− fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
232− case fpE of
233− Left exn -> liftIO $ exn @?= NoSuchBucket
234− _ -> return ()
235−
236− outFile <- mkRandFile 0
237− step "simple fGetObject works"
238− fGetObject bucket "test-file" outFile defaultGetObjectOptions
239−
240− let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
241− step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
242− resE <-
243− try $
244− fGetObject
245− bucket
246− "test-file"
247− outFile
248− defaultGetObjectOptions
249− { gooIfUnmodifiedSince = Just unmodifiedTime
250− }
251− case resE of
252− Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
253− _ -> return ()
254−
255− step "fGetObject an object with no matching etag, check for exception"
256− resE1 <-
257− try $
258− fGetObject
259− bucket
260− "test-file"
261− outFile
262− defaultGetObjectOptions
263− { gooIfMatch = Just "invalid-etag"
264− }
265− case resE1 of
266− Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
267− _ -> return ()
268−
269− step "fGetObject an object with no valid range, check for exception"
270− resE2 <-
271− try $
272− fGetObject
273− bucket
274− "test-file"
275− outFile
276− defaultGetObjectOptions
277− { gooRange = Just $ HT.ByteRangeFromTo 100 300
278− }
279− case resE2 of
280− Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
281− _ -> return ()
282−
283− step "fGetObject on object with a valid range"
284− fGetObject
285− bucket
286− "test-file"
287− outFile
288− defaultGetObjectOptions
289− { gooRange = Just $ HT.ByteRangeFrom 1
290− }
111+ randFile <- mkRandFile mb15
291112
292− step "fGetObject a non-existent object and check for NoSuchKey exception"
293− resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
294− case resE3 of
295− Left exn -> liftIO $ exn @?= NoSuchKey
296− _ -> return ()
113+ step "put object parts 1 of 1"
114+ h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
115+ partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
297116
298− step "create new multipart upload works"
299− uid <- newMultipartUpload bucket "newmpupload" []
300− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
117+ step "complete multipart"
118+ void $ completeMultipartUpload bucket object uid [partInfo]
301119
302− step "abort a new multipart upload works"
303− abortMultipartUpload bucket "newmpupload" uid
120+ destFile <- mkRandFile 0
121+ step "Retrieve the created object and check size"
122+ fGetObject bucket object destFile
123+ gotSize <- withNewHandle destFile getFileSize
124+ liftIO $ gotSize == Right (Just mb15) @?
125+ "Wrong file size of put file after getting"
304126
305− step "delete object works"
306− deleteObject bucket "test-file"
127+ step "Cleanup actions"
128+ removeObject bucket object
307129
308− step "statObject test"
309− let object = "sample"
310− step "create an object"
311− inputFile <- mkRandFile 0
312− fPutObject bucket object inputFile defaultPutObjectOptions
130+putObjectNoSizeTest :: TestTree
131+putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
132+ \step bucket -> do
133+ -- putObject test (conduit source, no size specified)
134+ let obj = "mpart"
135+ mb70 = 70 * 1024 * 1024
313136
314− step "get metadata of the object"
315− res <- statObject bucket object defaultGetObjectOptions
316− liftIO $ oiSize res @?= 0
137+ step "Prepare for putObject with from source without providing size."
138+ rFile <- mkRandFile mb70
317139
318− step "delete object"
319− deleteObject bucket object
140+ step "Upload multipart file."
141+ putObject bucket obj (CB.sourceFile rFile) Nothing
320142
321−lowLevelMultipartTest :: TestTree
322−lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
323− \step bucket -> do
324− -- low-level multipart operation tests.
325− let object = "newmpupload"
326− mb15 = 15 * 1024 * 1024
327−
328− step "Prepare for low-level multipart tests."
329− step "create new multipart upload"
330− uid <- newMultipartUpload bucket object []
331− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
332−
333− randFile <- mkRandFile mb15
334−
335− step "put object parts 1 of 1"
336− partInfo <-
337− UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
338− putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
339−
340− step "complete multipart"
341− void $ completeMultipartUpload bucket object uid [partInfo]
342−
343− destFile <- mkRandFile 0
344− step "Retrieve the created object and check size"
345− fGetObject bucket object destFile defaultGetObjectOptions
346− gotSize <- withNewHandle destFile getFileSize
347− liftIO $
348− gotSize
349− == Right (Just mb15)
350− @? "Wrong file size of put file after getting"
351−
352− step "Cleanup actions"
353− removeObject bucket object
354−
355−putObjectSizeTest :: TestTree
356−putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
357− \step bucket -> do
358− -- putObject test (conduit source, size specified)
359− let obj = "msingle"
360− mb1 = 1 * 1024 * 1024
143+ step "Retrieve and verify file size"
144+ destFile <- mkRandFile 0
145+ fGetObject bucket obj destFile
146+ gotSize <- withNewHandle destFile getFileSize
147+ liftIO $ gotSize == Right (Just mb70) @?
148+ "Wrong file size of put file after getting"
361149
362− step "Prepare for putObject with from source with size."
363− rFile <- mkRandFile mb1
150+ step "Cleanup actions"
151+ deleteObject bucket obj
364152
365− step "Upload single file."
366− putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
153+highLevelListingTest :: TestTree
154+highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
155+ \step bucket -> do
156+ step "High-level listObjects Test"
157+ step "put 3 objects"
158+ let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3"]
159+ forM_ expectedObjects $
160+ \obj -> fPutObject bucket obj "/etc/lsb-release"
161+
162+ step "High-level listing of objects"
163+ objects <- listObjects bucket Nothing True $$ sinkList
164+
165+ liftIO $ assertEqual "Objects match failed!" (sort expectedObjects)
166+ (map oiObject objects)
167+
168+ step "High-level listing of objects (version 1)"
169+ objects <- listObjectsV1 bucket Nothing True $$ sinkList
170+
171+ step "Cleanup actions"
172+ forM_ expectedObjects $
173+ \obj -> removeObject bucket obj
174+
175+ step "High-level listIncompleteUploads Test"
176+ let object = "newmpupload"
177+ step "create 10 multipart uploads"
178+ forM_ [1..10::Int] $ \_ -> do
179+ uid <- newMultipartUpload bucket object []
180+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
181+
182+ step "High-level listing of incomplete multipart uploads"
183+ uploads <- listIncompleteUploads bucket Nothing True $$ sinkList
184+ -- Minio server behaviour changed to list no incomplete uploads,
185+ -- so the check below reflects this; this test is expected to
186+ -- fail on AWS S3.
187+ liftIO $ length uploads @?= 0
188+
189+ step "cleanup"
190+ forM_ uploads $ \(UploadInfo _ uid _ _) ->
191+ abortMultipartUpload bucket object uid
192+
193+ step "High-level listIncompleteParts Test"
194+ let mb5 = 5 * 1024 * 1024
195+
196+ step "create a multipart upload"
197+ uid <- newMultipartUpload bucket object []
198+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
367199
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"
200+ step "put object parts 1..10"
201+ inputFile <- mkRandFile mb5
202+ h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
203+ forM_ [1..10] $ \pnum ->
204+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
376205
377− step "Cleanup actions"
378− deleteObject bucket obj
206+ step "fetch list parts"
207+ incompleteParts <- listIncompleteParts bucket object uid $$ sinkList
208+ liftIO $ length incompleteParts @?= 10
379209
380−putObjectNoSizeTest :: TestTree
381−putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
382− \step bucket -> do
383− -- putObject test (conduit source, no size specified)
384− let obj = "mpart"
385− mb70 = 70 * 1024 * 1024
210+ step "cleanup"
211+ abortMultipartUpload bucket object uid
386212
387− step "Prepare for putObject with from source without providing size."
388− rFile <- mkRandFile mb70
213+listingTest :: TestTree
214+listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
215+ step "listObjects' test"
216+ step "put 10 objects"
217+ let objects = (\s ->T.concat ["lsb-release", T.pack (show s)]) <$> [1..10::Int]
218+
219+ forM_ [1..10::Int] $ \s ->
220+ fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release"
221+
222+ step "Simple list"
223+ res <- listObjects' bucket Nothing Nothing Nothing Nothing
224+ let expectedObjects = sort objects
225+ liftIO $ assertEqual "Objects match failed!" expectedObjects
226+ (map oiObject $ lorObjects res)
227+
228+ step "Simple list version 1"
229+ res <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
230+ let expected = sort $ map (T.concat .
231+ ("lsb-release":) .
232+ (\x -> [x]) .
233+ T.pack .
234+ show) [1..10::Int]
235+ liftIO $ assertEqual "Objects match failed!" expected
236+ (map oiObject $ lorObjects' res)
237+
238+ step "Cleanup actions"
239+ forM_ objects $ \obj -> deleteObject bucket obj
240+
241+ step "listIncompleteUploads' test"
242+ step "create 10 multipart uploads"
243+ let object = "newmpupload"
244+ forM_ [1..10::Int] $ \_ -> do
245+ uid <- newMultipartUpload bucket object []
246+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
247+
248+ step "list incomplete multipart uploads"
249+ incompleteUploads <- listIncompleteUploads' bucket Nothing Nothing
250+ Nothing Nothing Nothing
251+ -- Minio server behaviour changed to list no incomplete uploads,
252+ -- so the check below reflects this; this test is expected to
253+ -- fail on AWS S3.
254+ liftIO $ (length $ lurUploads incompleteUploads) @?= 0
255+
256+ step "cleanup"
257+ forM_ (lurUploads incompleteUploads) $
258+ \(_, uid, _) -> abortMultipartUpload bucket object uid
259+
260+ step "Basic listIncompleteParts Test"
261+ let mb5 = 5 * 1024 * 1024
262+
263+ step "create a multipart upload"
264+ uid <- newMultipartUpload bucket object []
265+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
389266
390− step "Upload multipart file."
391− putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
267+ step "put object parts 1..10"
268+ inputFile <- mkRandFile mb5
269+ h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
270+ forM_ [1..10] $ \pnum ->
271+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
392272
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"
273+ step "fetch list parts"
274+ listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
275+ liftIO $ (length $ lprParts listPartsResult) @?= 10
276+ abortMultipartUpload bucket object uid
401277
402− step "Cleanup actions"
403− deleteObject bucket obj
404278
405−highLevelListingTest :: TestTree
406−highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
407− \step bucket -> do
408− step "High-level listObjects Test"
409− step "put 3 objects"
410− let extractObjectsFromList =
411− mapM
412− ( \case
413− ListItemObject o -> Just $ oiObject o
414− _ -> Nothing
415− )
416− extractObjectsAndDirsFromList =
417− map
418− ( \case
419− ListItemObject o -> oiObject o
420− ListItemPrefix d -> d
421− )
422− expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
423− expectedNonRecList = ["o4", "dir/"]
424−
425− testFilepath <- mkRandFile 200
426− forM_ expectedObjects $
427− \obj -> fPutObject bucket obj testFilepath defaultPutObjectOptions
428−
429− step "High-level listing of objects"
430− items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
431− liftIO $
432− assertEqual "Objects/Dirs match failed!" expectedNonRecList $
433− extractObjectsAndDirsFromList items
434−
435− step "High-level recursive listing of objects"
436− objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
437−
438− liftIO
439− $ assertEqual
440− "Objects match failed!"
441− (Just $ sort expectedObjects)
442− $ extractObjectsFromList objects
443−
444− step "High-level listing of objects (version 1)"
445− itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
446− liftIO $
447− assertEqual "Objects/Dirs match failed!" expectedNonRecList $
448− extractObjectsAndDirsFromList itemsV1
449−
450− step "High-level recursive listing of objects (version 1)"
451− objectsV1 <-
452− C.runConduit $
453− listObjectsV1 bucket Nothing True
454− C..| sinkList
455−
456− liftIO
457− $ assertEqual
458− "Objects match failed!"
459− (Just $ sort expectedObjects)
460− $ extractObjectsFromList objectsV1
461−
462− let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
463− expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
464− step "High-level listing with prefix"
465− prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
466− liftIO
467− $ assertEqual
468− "Objects/Dirs under prefix match failed!"
469− expectedPrefListing
470− $ extractObjectsAndDirsFromList prefItems
471−
472− step "High-level listing with prefix recursive"
473− prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
474− liftIO
475− $ assertEqual
476− "Objects/Dirs under prefix match recursive failed!"
477− expectedPrefListingRec
478− $ extractObjectsFromList prefItemsRec
479−
480− step "High-level listing with prefix (version 1)"
481− prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
482− liftIO
483− $ assertEqual
484− "Objects/Dirs under prefix match failed!"
485− expectedPrefListing
486− $ extractObjectsAndDirsFromList prefItemsV1
487−
488− step "High-level listing with prefix recursive (version 1)"
489− prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
490− liftIO
491− $ assertEqual
492− "Objects/Dirs under prefix match recursive failed!"
493− expectedPrefListingRec
494− $ extractObjectsFromList prefItemsRecV1
495−
496− step "Cleanup actions"
497− forM_ expectedObjects $
498− \obj -> removeObject bucket obj
499−
500− step "High-level listIncompleteUploads Test"
501− let object = "newmpupload"
502− step "create 10 multipart uploads"
503− forM_ [1 .. 10 :: Int] $ \_ -> do
279+liveServerUnitTests :: TestTree
280+liveServerUnitTests = testGroup "Unit tests against a live server"
281+ [ basicTests
282+ , listingTest
283+ , highLevelListingTest
284+ , lowLevelMultipartTest
285+ , putObjectNoSizeTest
286+ , funTestWithBucket "Multipart Tests" $
287+ \step bucket -> do
288+ step "Prepare for putObjectInternal with non-seekable file, with size."
289+ step "Upload multipart file."
290+ let mb80 = 80 * 1024 * 1024
291+ obj = "mpart"
292+
293+ void $ putObjectInternal bucket obj $ ODFile "/dev/zero" (Just mb80)
294+
295+ step "Retrieve and verify file size"
296+ destFile <- mkRandFile 0
297+ fGetObject bucket obj destFile
298+ gotSize <- withNewHandle destFile getFileSize
299+ liftIO $ gotSize == Right (Just mb80) @?
300+ "Wrong file size of put file after getting"
301+
302+ step "Cleanup actions"
303+ removeObject bucket obj
304+
305+ step "cleanup"
306+ removeObject bucket "big"
307+
308+ step "Prepare for removeIncompleteUpload"
309+ -- low-level multipart operation tests.
310+ let object = "newmpupload"
311+ kb5 = 5 * 1024
312+
313+ step "create new multipart upload"
504314 uid <- newMultipartUpload bucket object []
505315 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
506316
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
317+ randFile <- mkRandFile kb5
513318
514− step "cleanup"
515− forM_ uploads $ \(UploadInfo _ uid _ _) ->
516− abortMultipartUpload bucket object uid
319+ step "upload 2 parts"
320+ forM_ [1,2] $ \partNum -> do
321+ h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
322+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
517323
518− step "High-level listIncompleteParts Test"
519− let mb5 = 5 * 1024 * 1024
324+ step "remove ongoing upload"
325+ removeIncompleteUpload bucket object
326+ uploads <- listIncompleteUploads bucket (Just object) False C.$$ sinkList
327+ liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
520328
521− step "create a multipart upload"
522− uid <- newMultipartUpload bucket object []
523− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
524329
525− step "put object parts 1..10"
526− inputFile <- mkRandFile mb5
527− UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
528− forM_ [1 .. 10] $ \pnum ->
529− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
330+ , funTestWithBucket "copyObject related tests" $ \step bucket -> do
331+ step "copyObjectSingle basic tests"
332+ let object = "xxx"
333+ objCopy = "xxxCopy"
334+ size1 = 100 :: Int64
530335
531− step "fetch list parts"
532− incompleteParts <-
533− C.runConduit $
534− listIncompleteParts bucket object uid
535− C..| sinkList
536− liftIO $ length incompleteParts @?= 10
336+ step "create server object to copy"
337+ inputFile <- mkRandFile size1
338+ fPutObject bucket object inputFile
537339
538− step "cleanup"
539− abortMultipartUpload bucket object uid
340+ step "copy object"
341+ let cps = def { cpSource = format "/{}/{}" [bucket, object] }
342+ (etag, modTime) <- copyObjectSingle bucket objCopy cps []
540343
541−listingTest :: TestTree
542−listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
543− step "listObjects' test"
544− step "put 10 objects"
545− let objects = (\s -> T.concat ["test-file-", T.pack (show s)]) <$> [1 .. 10 :: Int]
546−
547− testFilepath <- mkRandFile 200
548− forM_ [1 .. 10 :: Int] $ \s ->
549− fPutObject bucket (T.concat ["test-file-", T.pack (show s)]) testFilepath defaultPutObjectOptions
550−
551− step "Simple list"
552− res <- listObjects' bucket Nothing Nothing Nothing Nothing
553− let expectedObjects = sort objects
554− liftIO $
555− assertEqual
556− "Objects match failed!"
557− expectedObjects
558− (map oiObject $ lorObjects res)
559−
560− step "Simple list version 1"
561− resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
562− let expected =
563− sort $
564− map
565− ( T.concat
566− . ("test-file-" :)
567− . (: [])
568− . T.pack
569− . show
570− )
571− [1 .. 10 :: Int]
572− liftIO $
573− assertEqual
574− "Objects match failed!"
575− expected
576− (map oiObject $ lorObjects' resV1)
577−
578− step "Cleanup actions"
579− forM_ objects $ \obj -> deleteObject bucket obj
580−
581− step "listIncompleteUploads' test"
582− step "create 10 multipart uploads"
583− let object = "newmpupload"
584− forM_ [1 .. 10 :: Int] $ \_ -> do
585− uid <- newMultipartUpload bucket object []
586− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
587−
588− step "list incomplete multipart uploads"
589− incompleteUploads <-
590− listIncompleteUploads'
591− bucket
592− (Just "newmpupload")
593− Nothing
594− Nothing
595− Nothing
596− Nothing
597− liftIO $ length (lurUploads incompleteUploads) @?= 10
598−
599− step "cleanup"
600− forM_ (lurUploads incompleteUploads) $
601− \(_, uid, _) -> abortMultipartUpload bucket object uid
602−
603− step "Basic listIncompleteParts Test"
604− let mb5 = 5 * 1024 * 1024
605−
606− step "create a multipart upload"
607− uid <- newMultipartUpload bucket object []
608− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
609−
610− step "put object parts 1..10"
611− inputFile <- mkRandFile mb5
612− UIO.withBinaryFile inputFile UIO.ReadMode $ \h ->
613− forM_ [1 .. 10] $ \pnum ->
614− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
615−
616− step "fetch list parts"
617− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
618− liftIO $ length (lprParts listPartsResult) @?= 10
619− abortMultipartUpload bucket object uid
344+ -- retrieve obj info to check
345+ ObjectInfo _ t e s <- headObject bucket objCopy
346+
347+ let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
348+
349+ liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
350+ "Copied object did not match expected."
351+
352+ step "cleanup actions"
353+ removeObject bucket object
354+ removeObject bucket objCopy
355+
356+ step "copyObjectPart basic tests"
357+ let srcObj = "XXX"
358+ copyObj = "XXXCopy"
359+
360+ step "Prepare"
361+ let mb15 = 15 * 1024 * 1024
362+ mb5 = 5 * 1024 * 1024
363+ randFile <- mkRandFile mb15
364+ fPutObject bucket srcObj randFile
365+
366+ step "create new multipart upload"
367+ uid <- newMultipartUpload bucket copyObj []
368+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
369+
370+ step "put object parts 1-3"
371+ let cps' = def {cpSource = format "/{}/{}" [bucket, srcObj]}
372+ parts <- forM [1..3] $ \p -> do
373+ (etag', _) <- copyObjectPart bucket copyObj cps'{
374+ cpSourceRange = Just ((p-1)*mb5, (p-1)*mb5 + (mb5 - 1))
375+ } uid (fromIntegral p) []
376+ return (fromIntegral p, etag')
377+
378+ step "complete multipart"
379+ void $ completeMultipartUpload bucket copyObj uid parts
380+
381+ step "verify copied object size"
382+ (ObjectInfo _ _ _ s') <- headObject bucket copyObj
383+
384+ liftIO $ (s' == mb15) @? "Size failed to match"
385+
386+ step "Cleanup actions"
387+ removeObject bucket srcObj
388+ removeObject bucket copyObj
389+
390+ step "copyObject basic tests"
391+ let srcs = ["XXX", "XXXL"]
392+ copyObjs = ["XXXCopy", "XXXLCopy"]
393+ sizes = map (* (1024 * 1024)) [15, 65]
394+
395+ step "Prepare"
396+ forM_ (zip srcs sizes) $ \(src, size) ->
397+ fPutObject bucket src =<< mkRandFile size
398+
399+ step "make small and large object copy"
400+ forM_ (zip copyObjs srcs) $ \(cp, src) ->
401+ copyObject bucket cp def{cpSource = format "/{}/{}" [bucket, src]}
402+
403+ step "verify uploaded objects"
404+ uploadedSizes <- fmap oiSize <$> forM copyObjs (headObject bucket)
405+
406+ liftIO $ (sizes == uploadedSizes) @? "Uploaded obj sizes failed to match"
407+
408+ forM_ (srcs ++ copyObjs) (removeObject bucket)
409+
410+ step "copyObject with offset test "
411+ let src = "XXX"
412+ size = 15 * 1024 * 1024
413+
414+ step "Prepare"
415+ fPutObject bucket src =<< mkRandFile size
416+
417+ step "copy last 10MiB of object"
418+ copyObject bucket copyObj def{
419+ cpSource = format "/{}/{}" [bucket, src]
420+ , cpSourceRange = Just (5 * 1024 * 1024, size - 1)
421+ }
422+
423+ step "verify uploaded object"
424+ cSize <- oiSize <$> headObject bucket copyObj
425+
426+ liftIO $ (cSize == 10 * 1024 * 1024) @? "Uploaded obj size mismatched!"
427+
428+ forM_ [src, copyObj] (removeObject bucket)
429+
430+ , presignedUrlFunTest
431+ , presignedPostPolicyFunTest
432+ ]
433+
434+basicTests :: TestTree
435+basicTests = funTestWithBucket "Basic tests" $ \step bucket -> do
436+ step "getService works and contains the test bucket."
437+ buckets <- getService
438+ unless (length (filter (== bucket) $ map biName buckets) == 1) $
439+ liftIO $
440+ assertFailure ("The bucket " ++ show bucket ++
441+ " was expected to exist.")
442+
443+ step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
444+ mbE <- MC.try $ makeBucket bucket Nothing
445+ case mbE of
446+ Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
447+ _ -> return ()
448+
449+ step "makeBucket with an invalid bucket name and check for appropriate exception."
450+ invalidMBE <- MC.try $ makeBucket "invalidBucketName" Nothing
451+ case invalidMBE of
452+ Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
453+ _ -> return ()
454+
455+ step "getLocation works"
456+ region <- getLocation bucket
457+ liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
458+
459+ step "singlepart putObject works"
460+ fPutObject bucket "lsb-release" "/etc/lsb-release"
461+
462+ step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
463+ fpE <- MC.try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release"
464+ case fpE of
465+ Left exn -> liftIO $ exn @?= NoSuchBucket
466+ _ -> return ()
467+
468+ outFile <- mkRandFile 0
469+ step "simple fGetObject works"
470+ fGetObject bucket "lsb-release" outFile
471+
472+ step "fGetObject a non-existent object and check for NoSuchKey exception"
473+ resE <- MC.try $ fGetObject bucket "noSuchKey" outFile
474+ case resE of
475+ Left exn -> liftIO $ exn @?= NoSuchKey
476+ _ -> return ()
477+
478+
479+ step "create new multipart upload works"
480+ uid <- newMultipartUpload bucket "newmpupload" []
481+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
482+
483+ step "abort a new multipart upload works"
484+ abortMultipartUpload bucket "newmpupload" uid
485+
486+ step "delete object works"
487+ deleteObject bucket "lsb-release"
488+
489+ step "statObject test"
490+ let object = "sample"
491+ step "create an object"
492+ inputFile <- mkRandFile 0
493+ fPutObject bucket object inputFile
494+
495+ step "get metadata of the object"
496+ res <- statObject bucket object
497+ liftIO $ (oiSize res) @?= 0
498+
499+ step "delete object"
500+ deleteObject bucket object
620501
621502presignedUrlFunTest :: TestTree
622503presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -628,120 +509,97 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
628509 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
629510
630511 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
512+ putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
513+ (Just obj) (Just "us-east-1") [] []
514+
642515 let size1 = 1000 :: Int64
643516 inputFile <- mkRandFile size1
644517
645518 -- attempt to upload using the presigned URL
646519 putResp <- putR size1 inputFile mgr putUrl
647− liftIO $
648− (NC.responseStatus putResp == HT.status200)
649− @? "presigned PUT failed"
520+ liftIO $ (NC.responseStatus putResp == HT.status200) @?
521+ "presigned PUT failed"
650522
651523 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− []
524+ getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
525+ (Just obj) (Just "us-east-1") [] []
661526
662527 getResp <- getR mgr getUrl
663− liftIO $
664− (NC.responseStatus getResp == HT.status200)
665− @? "presigned GET failed"
528+ liftIO $ (NC.responseStatus getResp == HT.status200) @?
529+ "presigned GET failed"
666530
667531 -- read content from file to compare with response above
668− bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
669− liftIO $
670− (bs == NC.responseBody getResp)
671− @? "presigned put and get got mismatched data"
532+ bs <- CB.sourceFile inputFile $$ CB.sinkLbs
533+ liftIO $ (bs == NC.responseBody getResp) @?
534+ "presigned put and get got mismatched data"
672535
673536 step "PUT object presigned - presignedPutObjectURL"
674− putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
537+ putUrl2 <- presignedPutObjectUrl bucket obj2 3600 []
675538
676539 let size2 = 1200
677540 testFile <- mkRandFile size2
678541
679542 putResp2 <- putR size2 testFile mgr putUrl2
680− liftIO $
681− (NC.responseStatus putResp2 == HT.status200)
682− @? "presigned PUT failed (presignedPutObjectUrl)"
543+ liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
544+ "presigned PUT failed (presignedPutObjectUrl)"
683545
684546 step "HEAD object presigned URL - presignedHeadObjectUrl"
685547 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
686548
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)"
549+ headResp <- do req <- NC.parseRequest $ toS headUrl
550+ NC.httpLbs (req {NC.method = HT.methodHead}) mgr
551+ liftIO $ (NC.responseStatus headResp == HT.status200) @?
552+ "presigned HEAD failed (presignedHeadObjectUrl)"
693553
694554 -- check that header info is accurate
695− let h = H.fromList $ NC.responseHeaders headResp
696− cLen = H.lookupDefault "0" HT.hContentLength h
697− liftIO $ (cLen == showBS size2) @? "Head req returned bad content length"
555+ let h = Map.fromList $ NC.responseHeaders headResp
556+ cLen = Map.findWithDefault "0" HT.hContentLength h
557+ liftIO $ (cLen == show size2) @? "Head req returned bad content length"
698558
699559 step "GET object presigned URL - presignedGetObjectUrl"
700560 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
701561
702562 getResp2 <- getR mgr getUrl2
703− liftIO $
704− (NC.responseStatus getResp2 == HT.status200)
705− @? "presigned GET failed (presignedGetObjectUrl)"
563+ liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
564+ "presigned GET failed (presignedGetObjectUrl)"
706565
707566 -- read content from file to compare with response above
708− bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
709− liftIO $
710− (bs2 == NC.responseBody getResp2)
711− @? "presigned put and get got mismatched data (presigned*Url)"
567+ bs2 <- CB.sourceFile testFile $$ CB.sinkLbs
568+ liftIO $ (bs2 == NC.responseBody getResp2) @?
569+ "presigned put and get got mismatched data (presigned*Url)"
570+
712571
713572 mapM_ (removeObject bucket) [obj, obj2]
714573 where
715574 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− }
575+ req <- NC.parseRequest $ toS url
576+ let req' = req { NC.method = HT.methodPut
577+ , NC.requestBody = NC.requestBodySource size $
578+ CB.sourceFile filePath}
724579 NC.httpLbs req' mgr
580+
725581 getR mgr url = do
726− let req = NC.parseRequest_ $ decodeUtf8 url
582+ req <- NC.parseRequest $ toS url
727583 NC.httpLbs req mgr
728584
729585presignedPostPolicyFunTest :: TestTree
730−presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
586+presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
731587 \step bucket -> do
588+
732589 step "presignedPostPolicy basic test"
733− now <- liftIO Time.getCurrentTime
590+ now <- liftIO $ Time.getCurrentTime
734591
735592 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− ]
593+ policyConds = [ ppCondBucket bucket
594+ , ppCondKey key
595+ , ppCondContentLengthRange 1 1000
596+ , ppCondContentType "application/octet-stream"
597+ , ppCondSuccessActionStatus 200
598+ ]
599+
743600 expirationTime = Time.addUTCTime 3600 now
744601 postPolicyE = newPostPolicy expirationTime policyConds
602+
745603 size = 1000 :: Int64
746604
747605 inputFile <- mkRandFile size
@@ -752,532 +610,17 @@ presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests
752610 (url, formData) <- presignedPostPolicy postPolicy
753611 -- liftIO (print url) >> liftIO (print formData)
754612 result <- liftIO $ postForm url formData inputFile
755− liftIO $
756− (NC.responseStatus result == HT.status200)
757− @? "presigned POST failed"
613+ liftIO $ (NC.responseStatus result == HT.status200) @?
614+ "presigned POST failed"
758615
759616 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
770−
771−bucketPolicyFunTest :: TestTree
772−bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
773− \step bucket -> do
774− step "bucketPolicy basic test - no policy exception"
775− resE <- try $ getBucketPolicy bucket
776− case resE of
777− Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
778− _ -> return ()
779−
780− resE' <- try $ setBucketPolicy bucket T.empty
781− case resE' of
782− Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
783− _ -> return ()
784−
785− let expectedPolicyJSON = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket/*\"]}]}"
786−
787− step "try a malformed policy, expect error"
788− resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
789− case resE'' of
790− Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
791− _ -> return ()
792−
793− let expectedPolicyJSON' = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "/*\"]}]}"
794−
795− step "set bucket policy"
796− setBucketPolicy bucket expectedPolicyJSON'
797−
798− let obj = "myobject"
799−
800− step "verify bucket policy: (1) create `myobject`"
801− putObject bucket obj (replicateC 100 "c") Nothing defaultPutObjectOptions
802−
803− step "verify bucket policy: (2) get `myobject` anonymously"
804− connInfo <- asks mcConnInfo
805− let proto = bool "http://" "https://" $ connectIsSecure connInfo
806− url =
807− BS.concat
808− [ proto,
809− getHostAddr connInfo,
810− "/",
811− encodeUtf8 bucket,
812− "/",
813− encodeUtf8 obj
814− ]
815− respE <-
816− liftIO $
817− fmap (Right . toStrictBS) (NC.simpleHttp $ decodeUtf8 url)
818− `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
819− case respE of
820− Left err -> liftIO $ assertFailure $ show err
821− Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
822−
823− deleteObject bucket obj
824−
825− step "delete bucket policy"
826− setBucketPolicy bucket T.empty
827−
828−multipartTest :: TestTree
829−multipartTest = funTestWithBucket "Multipart Tests" $
830− \step bucket -> do
831− -- Commenting out test since it's platform specific.
832− -- FIXME: Need to find a platform agnostic way to test this.
833− -- step "Prepare for putObjectInternal with non-seekable file, with size."
834− -- step "Upload multipart file."
835− -- let mb80 = 80 * 1024 * 1024
836− -- obj = "mpart"
837−
838− -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
839−
840− -- step "Retrieve and verify file size"
841− -- destFile <- mkRandFile 0
842− -- fGetObject bucket obj destFile defaultGetObjectOptions
843− -- gotSize <- withNewHandle destFile getFileSize
844− -- liftIO $
845− -- gotSize == Right (Just mb80)
846− -- @? "Wrong file size of put file after getting"
847−
848− -- step "Cleanup actions"
849− -- removeObject bucket obj
850−
851− -- step "cleanup"
852− -- removeObject bucket "big"
853−
854− step "Prepare for removeIncompleteUpload"
855− -- low-level multipart operation tests.
856− let object = "newmpupload"
857− kb5 = 5 * 1024
858−
859− step "create new multipart upload"
860− uid <- newMultipartUpload bucket object []
861− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
862−
863− randFile <- mkRandFile kb5
864−
865− step "upload 2 parts"
866− forM_ [1, 2] $ \partNum -> do
867− UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
868− void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
869−
870− step "remove ongoing upload"
871− removeIncompleteUpload bucket object
872− uploads <-
873− C.runConduit $
874− listIncompleteUploads bucket (Just object) False
875− C..| sinkList
876− liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
877−
878−putObjectContentTypeTest :: TestTree
879−putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
880− \step bucket -> do
881− step "fPutObject content type test"
882− let object = "xxx-content-type"
883− size1 = 100 :: Int64
884−
885− step "create server object with content-type"
886− inputFile <- mkRandFile size1
887− fPutObject
888− bucket
889− object
890− inputFile
891− defaultPutObjectOptions
892− { pooContentType = Just "application/javascript"
893− }
894−
895− -- retrieve obj info to check
896− oi <- headObject bucket object []
897− let m = oiMetadata oi
898−
899− step "Validate content-type"
900− liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
901−
902− step "upload object with content-encoding set to identity"
903− fPutObject
904− bucket
905− object
906− inputFile
907− defaultPutObjectOptions
908− { pooContentEncoding = Just "identity"
909− }
910−
911− oiCE <- headObject bucket object []
912− let m' = oiMetadata oiCE
913−
914− step "Validate content-encoding"
915− liftIO $
916− assertEqual
917− "Content-Encoding did not match"
918− (Just "identity")
919− (H.lookup "Content-Encoding" m')
920−
921− step "Cleanup actions"
922−
923− removeObject bucket object
924−
925−putObjectContentLanguageTest :: TestTree
926−putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage tests" $
927− \step bucket -> do
928− step "fPutObject content language test"
929− let object = "xxx-content-language"
930− size1 = 100 :: Int64
931−
932− step "create server object with content-language"
933− inputFile <- mkRandFile size1
934− fPutObject
935− bucket
936− object
937− inputFile
938− defaultPutObjectOptions
939− { pooContentLanguage = Just "en-US"
940− }
941−
942− -- retrieve obj info to check
943− oi <- headObject bucket object []
944− let m = oiMetadata oi
945−
946− step "Validate content-language"
947− liftIO $
948− assertEqual
949− "content-language did not match"
950− (Just "en-US")
951− (H.lookup "Content-Language" m)
952− step "Cleanup actions"
953−
954− removeObject bucket object
955−
956−putObjectUserMetadataTest :: TestTree
957−putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
958− \step bucket -> do
959− step "putObject user-metadata test"
960− let object = "object-with-metadata"
961− size1 = 100 :: Int64
962−
963− step "create server object with usermetdata"
964− inputFile <- mkRandFile size1
965−
966− fPutObject
967− bucket
968− object
969− inputFile
970− defaultPutObjectOptions
971− { pooUserMetadata =
972− [ ("x-Amz-meta-mykey1", "myval1"),
973− ("mykey2", "myval2")
974− ]
975− }
976−
977− step "Validate user-metadata"
978− -- retrieve obj info to check
979− oi <- headObject bucket object []
980− let m = oiUserMetadata oi
981− -- need to do a case-insensitive comparison
982− sortedMeta =
983− sort $
984− map (bimap T.toLower T.toLower) $
985− H.toList m
986− ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
987−
988− liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
989−
990− step "Cleanup actions"
991− removeObject bucket object
992−
993−getObjectTest :: TestTree
994−getObjectTest = funTestWithBucket "getObject test" $
995− \step bucket -> do
996− step "putObject with some metadata"
997− let object = "object-with-metadata"
998− size1 = 100 :: Int64
999−
1000− inputFile <- mkRandFile size1
1001− fPutObject
1002− bucket
1003− object
1004− inputFile
1005− defaultPutObjectOptions
1006− { pooUserMetadata =
1007− [ ("x-Amz-meta-mykey1", "myval1"),
1008− ("mykey2", "myval2")
1009− ]
1010− }
1011−
1012− step "get the object - check the metadata matches"
1013− -- retrieve obj info to check
1014− gor <- getObject bucket object defaultGetObjectOptions
1015− let m = oiUserMetadata $ gorObjectInfo gor
1016− -- need to do a case-insensitive comparison
1017− sortedMeta =
1018− sort $
1019− map (bimap T.toLower T.toLower) $
1020− H.toList m
1021− ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
1022−
1023− liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
1024−
1025− step "get the object content"
1026− getObjectHash <- hashSHA256FromSource $ gorObjectStream gor
1027− inputHash <- hashSHA256FromSource $ C.sourceFile inputFile
1028− liftIO $ (getObjectHash == inputHash) @? "Input file and output file mismatched!"
1029−
1030− step "Cleanup actions"
1031− removeObject bucket object
1032−
1033−putObjectStorageClassTest :: TestTree
1034−putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
1035− \step bucket -> do
1036− step "fPutObject storage class test"
1037− let object = "xxx-storage-class-standard"
1038− object' = "xxx-storage-class-reduced"
1039− object'' = "xxx-storage-class-invalid"
1040− size1 = 100 :: Int64
1041− size0 = 0 :: Int64
1042−
1043− step "create server objects with storageClass"
1044− inputFile <- mkRandFile size1
1045− inputFile' <- mkRandFile size1
1046− inputFile'' <- mkRandFile size0
1047−
1048− fPutObject
1049− bucket
1050− object
1051− inputFile
1052− defaultPutObjectOptions
1053− { pooStorageClass = Just "STANDARD"
1054− }
1055−
1056− fPutObject
1057− bucket
1058− object'
1059− inputFile'
1060− defaultPutObjectOptions
1061− { pooStorageClass = Just "REDUCED_REDUNDANCY"
1062− }
1063−
1064− removeObject bucket object
1065−
1066− -- retrieve obj info to check
1067− oi' <- headObject bucket object' []
1068− let m' = oiMetadata oi'
1069−
1070− step "Validate x-amz-storage-class rrs"
1071− liftIO $
1072− assertEqual
1073− "storageClass did not match"
1074− (Just "REDUCED_REDUNDANCY")
1075− (H.lookup "X-Amz-Storage-Class" m')
1076−
1077− fpE <-
1078− try $
1079− fPutObject
1080− bucket
1081− object''
1082− inputFile''
1083− defaultPutObjectOptions
1084− { pooStorageClass = Just "INVALID_STORAGE_CLASS"
1085− }
1086− case fpE of
1087− Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
1088− _ -> return ()
1089−
1090− step "Cleanup actions"
1091−
1092− removeObject bucket object'
1093−
1094−copyObjectTests :: TestTree
1095−copyObjectTests = funTestWithBucket "copyObject related tests" $
1096− \step bucket -> do
1097− step "copyObjectSingle basic tests"
1098− let object = "xxx"
1099− objCopy = "xxxCopy"
1100− size1 = 100 :: Int64
1101−
1102− step "create server object to copy"
1103− inputFile <- mkRandFile size1
1104− fPutObject bucket object inputFile defaultPutObjectOptions
1105−
1106− step "copy object"
1107− let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
1108− (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
1109−
1110− -- retrieve obj info to check
1111− oi <- headObject bucket objCopy []
1112− let t = oiModTime oi
1113− let e = oiETag oi
1114− let s = oiSize oi
1115−
1116− let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
1117−
1118− liftIO $
1119− (s == size1 && e == etag && isMTimeDiffOk)
1120− @? "Copied object did not match expected."
1121−
1122− step "cleanup actions"
1123− removeObject bucket object
1124− removeObject bucket objCopy
1125−
1126− step "copyObjectPart basic tests"
1127− let srcObj = "XXX"
1128− copyObj = "XXXCopy"
1129−
1130− step "Prepare"
1131− let mb15 = 15 * 1024 * 1024
1132− mb5 = 5 * 1024 * 1024
1133− randFile <- mkRandFile mb15
1134− fPutObject bucket srcObj randFile defaultPutObjectOptions
1135−
1136− step "create new multipart upload"
1137− uid <- newMultipartUpload bucket copyObj []
1138− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
1139−
1140− step "put object parts 1-3"
1141− let srcInfo' = defaultSourceInfo {srcBucket = bucket, srcObject = srcObj}
1142− dstInfo' = defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1143− parts <- forM [1 .. 3] $ \p -> do
1144− (etag', _) <-
1145− copyObjectPart
1146− dstInfo'
1147− srcInfo'
1148− { srcRange = Just $ (,) ((p - 1) * mb5) ((p - 1) * mb5 + (mb5 - 1))
1149− }
1150− uid
1151− (fromIntegral p)
1152− []
1153− return (fromIntegral p, etag')
1154−
1155− step "complete multipart"
1156− void $ completeMultipartUpload bucket copyObj uid parts
1157−
1158− step "verify copied object size"
1159− oi' <- headObject bucket copyObj []
1160− let s' = oiSize oi'
1161−
1162− liftIO $ (s' == mb15) @? "Size failed to match"
1163−
1164− step "Cleanup actions"
1165− removeObject bucket srcObj
1166− removeObject bucket copyObj
1167−
1168− step "copyObject basic tests"
1169− let srcs = ["XXX", "XXXL"]
1170− copyObjs = ["XXXCopy", "XXXLCopy"]
1171− sizes = map (* (1024 * 1024)) [15, 65]
1172−
1173− step "Prepare"
1174− forM_ (zip srcs sizes) $ \(src, size) -> do
1175− inputFile' <- mkRandFile size
1176− fPutObject bucket src inputFile' defaultPutObjectOptions
1177−
1178− step "make small and large object copy"
1179− forM_ (zip copyObjs srcs) $ \(cp, src) ->
1180− copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = cp} defaultSourceInfo {srcBucket = bucket, srcObject = src}
1181−
1182− step "verify uploaded objects"
1183− uploadedSizes <- fmap oiSize <$> forM copyObjs (\o -> headObject bucket o [])
1184−
1185− liftIO $ (sizes == uploadedSizes) @? "Uploaded obj sizes failed to match"
1186−
1187− forM_ (srcs ++ copyObjs) (removeObject bucket)
1188−
1189− step "copyObject with offset test "
1190− let src = "XXX"
1191− size = 15 * 1024 * 1024
1192−
1193− step "Prepare"
1194− inputFile' <- mkRandFile size
1195− fPutObject bucket src inputFile' defaultPutObjectOptions
1196−
1197− step "copy last 10MiB of object"
1198− copyObject
1199− defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1200− defaultSourceInfo
1201− { srcBucket = bucket,
1202− srcObject = src,
1203− srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
1204− }
1205−
1206− step "verify uploaded object"
1207− cSize <- oiSize <$> headObject bucket copyObj []
1208−
1209− liftIO $ (cSize == 10 * 1024 * 1024) @? "Uploaded obj size mismatched!"
1210−
1211− forM_ [src, copyObj] (removeObject bucket)
1212−
1213−getNPutSSECTest :: TestTree
1214−getNPutSSECTest =
1215− funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
1216− -- Skip this test if the server is not using TLS as encryption is
1217− -- disabled anyway.
1218− isTLSConn <- asks (connectIsSecure . mcConnInfo)
1219− if isTLSConn
1220− then do
1221− step "Make an encryption key"
1222− key <- case mkSSECKey $ BS.pack [0 .. 31] of
1223− Nothing -> liftIO $ assertFailure "This should not happen"
1224− Just k -> return k
1225−
1226− let mb1 = 1024 * 1024
1227− obj = "1"
1228− step "Upload an object using the encryption key"
1229− rFile <- mkRandFile mb1
1230− let putOpts = defaultPutObjectOptions {pooSSE = Just $ SSEC key}
1231− fPutObject bucket obj rFile putOpts
1232−
1233− step "Stat object without key - should fail"
1234− headRes <- try $ statObject bucket obj defaultGetObjectOptions
1235− case headRes of
1236− Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
1237− Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
1238− | NC.responseStatus rsp == HT.status400 -> return ()
1239− | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1240− Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1241−
1242− step "Get file and check length"
1243− dstFile <- mkRandFile 0
1244− let getOpts = defaultGetObjectOptions {gooSSECKey = Just key}
1245− fGetObject bucket obj dstFile getOpts
1246−
1247− gotSize <- withNewHandle dstFile getFileSize
1248− liftIO $
1249− gotSize
1250− == Right (Just mb1)
1251− @? "Wrong file size of object when getting"
1252−
1253− step "Cleanup"
1254− deleteObject bucket obj
1255− else step "Skipping encryption test as server is not using TLS"
1256−
1257−assumeRoleRequestTest :: TestTree
1258−assumeRoleRequestTest = testCaseSteps "Assume Role STS API" $ \step -> do
1259− step "Load credentials"
1260− (_, (ak, sk)) <- lookupTestEnv
1261− val <- Env.lookupEnv "MINIO_LOCAL"
1262− isSecure <- Env.lookupEnv "MINIO_SECURE"
1263− let localMinioCred = Just $ CredentialValue ak sk mempty
1264− playCreds =
1265− case connectCreds minioPlayCI of
1266− CredsStatic c -> Just c
1267− _ -> Nothing
1268− (cvMay, loc) =
1269− case (val, isSecure) of
1270− (Just _, Just _) -> (localMinioCred, "https://localhost:9000")
1271− (Just _, Nothing) -> (localMinioCred, "http://localhost:9000")
1272− (Nothing, _) -> (playCreds, "https://play.min.io:9000")
1273− cv <- maybe (assertFailure "bad creds") return cvMay
1274− let assumeRole =
1275− STSAssumeRole cv $
1276− defaultSTSAssumeRoleOptions
1277− { saroLocation = Just "us-east-1",
1278− saroEndpoint = Just loc
1279− }
1280− step "AssumeRole request"
1281− res <- requestSTSCredential assumeRole
1282− let v = credentialValueText $ fst res
1283− print (v, snd res)
617+ where
618+
619+ postForm url formData inputFile = do
620+ req <- NC.parseRequest $ toS url
621+ let parts = map (\(x, y) -> Form.partBS x y) $
622+ Map.toList formData
623+ parts' = parts ++ [Form.partFile "file" inputFile]
624+ req' <- Form.formDataBody parts' req
625+ mgr <- NC.newManager NC.tlsManagerSettings
626+ NC.httpLbs req' mgr
test/Network/Minio/API/Test.hs+25−91
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017, 2018 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,102 +15,36 @@
1515--
1616
1717module Network.Minio.API.Test
18− ( bucketNameValidityTests,
19− objectNameValidityTests,
20− parseServerInfoJSONTest,
21− parseHealStatusTest,
22− parseHealStartRespTest,
23− )
24−where
18+ ( bucketNameValidityTests
19+ , objectNameValidityTests
20+ ) where
2521
26−import Data.Aeson (eitherDecode)
27−import Network.Minio.API
28−import Network.Minio.AdminAPI
2922import Test.Tasty
3023import Test.Tasty.HUnit
3124
32−assertBool' :: Bool -> Assertion
25+import Lib.Prelude
26+
27+import Network.Minio.API
28+
3329assertBool' = assertBool "Test failed!"
3430
3531bucketNameValidityTests :: 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− ]
32+bucketNameValidityTests = testGroup "Bucket Name Validity Tests"
33+ [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName ""
34+ , testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab"
35+ , testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
36+ , testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD"
37+ , testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2"
38+ , testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-"
39+ , testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg"
40+ , testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1"
41+ , testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea"
42+ , testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d"
43+ , testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
44+ ]
5145
5246objectNameValidityTests :: 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− ]
59−
60−parseServerInfoJSONTest :: TestTree
61−parseServerInfoJSONTest =
62− testGroup "Parse MinIO Admin API ServerInfo JSON test" $
63− map
64− ( \(tName, tDesc, tfn, tVal) ->
65− testCase tName $
66− assertBool tDesc $
67− tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
68− )
69− testCases
70− where
71− testCases =
72− [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON),
73− ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON),
74− ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
75− ]
76− fsJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":20530,\"Backend\":{\"Type\":1,\"OnlineDisks\":0,\"OfflineDisks\":0,\"StandardSCData\":0,\"StandardSCParity\":0,\"RRSCData\":0,\"RRSCParity\":0,\"Sets\":null}},\"network\":{\"transferred\":808,\"received\":1160},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":5992503019270,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
77− erasureJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":2,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
78− invalidJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":42,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
79−
80−parseHealStatusTest :: TestTree
81−parseHealStatusTest =
82− testGroup "Parse MinIO Admin API HealStatus JSON test" $
83− map
84− ( \(tName, tDesc, tfn, tVal) ->
85− testCase tName $
86− assertBool tDesc $
87− tfn (eitherDecode tVal :: Either [Char] HealStatus)
88− )
89− testCases
90− where
91− testCases =
92− [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON'),
93− ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON'),
94− ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
95− ]
96− erasureJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
97− invalidJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]"
98− invalidItemType = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"hello\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
99−
100−parseHealStartRespTest :: TestTree
101−parseHealStartRespTest =
102− testGroup "Parse MinIO Admin API HealStartResp JSON test" $
103− map
104− ( \(tName, tDesc, tfn, tVal) ->
105− testCase tName $
106− assertBool tDesc $
107− tfn (eitherDecode tVal :: Either [Char] HealStartResp)
108− )
109− testCases
110− where
111− testCases =
112− [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON),
113− ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
114− ]
115− hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
116− missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
47+objectNameValidityTests = testGroup "Object Name Validity Tests"
48+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""
49+ , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
50+ ]
test/Network/Minio/JsonParser/Test.hsdeleted−63
@@ -1,63 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−module Network.Minio.JsonParser.Test
18− ( jsonParserTests,
19− )
20−where
21−
22−import Lib.Prelude
23−import Network.Minio.Errors
24−import Network.Minio.JsonParser
25−import Test.Tasty
26−import Test.Tasty.HUnit
27−import UnliftIO (MonadUnliftIO)
28−
29−jsonParserTests :: TestTree
30−jsonParserTests =
31− testGroup
32− "JSON Parser Tests"
33− [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34− ]
35−
36−tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
37−tryValidationErr = try
38−
39−assertValidationErr :: MErrV -> Assertion
40−assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
41−
42−testParseErrResponseJSON :: Assertion
43−testParseErrResponseJSON = do
44− -- 1. Test parsing of an invalid error json.
45− parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
46− when (isRight parseResE) $
47− assertFailure $
48− "Parsing should have failed => " ++ show parseResE
49−
50− forM_ cases $ \(jsondata, sErr) -> do
51− parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
52− either assertValidationErr (@?= sErr) parseErr
53− where
54− cases =
55− [ -- 2. Test parsing of a valid error json.
56− ( "{\"Code\":\"InvalidAccessKeyId\",\"Message\":\"The access key ID you provided does not exist in our records.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
57− ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58− ),
59− -- 3. Test parsing of a valid, empty Resource.
60− ( "{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
61− ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
62− )
63− ]
test/Network/Minio/TestHelpers.hsdeleted−32
@@ -1,32 +0,0 @@
1−--
2−-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
3−--
4−-- Licensed under the Apache License, Version 2.0 (the "License");
5−-- you may not use this file except in compliance with the License.
6−-- You may obtain a copy of the License at
7−--
8−-- http://www.apache.org/licenses/LICENSE-2.0
9−--
10−-- Unless required by applicable law or agreed to in writing, software
11−-- distributed under the License is distributed on an "AS IS" BASIS,
12−-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−-- See the License for the specific language governing permissions and
14−-- limitations under the License.
15−--
16−
17−module Network.Minio.TestHelpers
18− ( runTestNS,
19− )
20−where
21−
22−import Network.Minio.Data
23−
24−newtype TestNS = TestNS {testNamespace :: Text}
25−
26−instance HasSvcNamespace TestNS where
27− getSvcNamespace = testNamespace
28−
29−runTestNS :: ReaderT TestNS m a -> m a
30−runTestNS =
31− flip runReaderT $
32− TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs+16−14
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -15,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
2322import Test.Tasty
2423import Test.Tasty.HUnit
2524
25+import Lib.Prelude
26+
27+import Network.Minio.Utils
28+
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+61−148
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,170 +13,83 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE QuasiQuotes #-}
1716
1817module Network.Minio.XmlGenerator.Test
19− ( xmlGeneratorTests,
20− )
21−where
18+ ( xmlGeneratorTests
19+ ) where
2220
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)
21+import Test.Tasty
22+import Test.Tasty.HUnit
23+
24+import Lib.Prelude
25+
26+import Data.Default (def)
27+
28+import Network.Minio.Data
29+import Network.Minio.XmlGenerator
30+import Network.Minio.XmlParser (parseNotification)
3331
3432xmlGeneratorTests :: 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− ]
33+xmlGeneratorTests = testGroup "XML Generator Tests"
34+ [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig
35+ , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest
36+ , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest
37+ ]
4338
4439testMkCreateBucketConfig :: Assertion
4540testMkCreateBucketConfig = do
46− let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
4741 assertEqual "CreateBucketConfiguration xml should match: " expected $
48− mkCreateBucketConfig ns "EU"
42+ mkCreateBucketConfig "EU"
4943 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>"
44+ expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
45+ \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
46+ \<LocationConstraint>EU</LocationConstraint>\
47+ \</CreateBucketConfiguration>"
5548
5649testMkCompleteMultipartUploadRequest :: Assertion
5750testMkCompleteMultipartUploadRequest =
5851 assertEqual "completeMultipartUpload xml should match: " expected $
59− mkCompleteMultipartUploadRequest [(1, "abc")]
52+ mkCompleteMultipartUploadRequest [(1, "abc")]
6053 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>"
54+ expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
55+ \<CompleteMultipartUpload>\
56+ \<Part>\
57+ \<PartNumber>1</PartNumber><ETag>abc</ETag>\
58+ \</Part>\
59+ \</CompleteMultipartUpload>"
6860
6961testMkPutNotificationRequest :: Assertion
7062testMkPutNotificationRequest =
7163 forM_ cases $ \val -> do
72− let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
73− result = fromStrictBS $ mkPutNotificationRequest ns val
74− ntf <- runExceptT $ runTestNS $ parseNotification result
75− either
76− (\_ -> assertFailure "XML Parse Error!")
77− (@?= val)
78− ntf
79− where
80− cases =
81− [ Notification
82− []
83− [ NotificationConfig
84− "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
85− "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
86− [ReducedRedundancyLostObject, ObjectCreated]
87− defaultFilter
88− ]
89− [],
90− Notification
91− [ NotificationConfig
92− "1"
93− "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
94− [ObjectCreatedPut]
95− ( Filter $
96− FilterKey $
97− FilterRules
98− [ FilterRule "prefix" "images/",
99− FilterRule "suffix" ".jpg"
100− ]
101− ),
102− NotificationConfig
103− ""
104− "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
105− [ObjectCreated]
106− defaultFilter
107− ]
108− [ NotificationConfig
109− ""
110− "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
111− [ReducedRedundancyLostObject]
112− defaultFilter
113− ]
114− [ NotificationConfig
115− "ObjectCreatedEvents"
116− "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
117− [ObjectCreated]
118− defaultFilter
119− ]
120− ]
121−
122−testMkSelectRequest :: Assertion
123−testMkSelectRequest = mapM_ assertFn cases
64+ let result = toS $ mkPutNotificationRequest val
65+ ntf <- runExceptT $ parseNotification result
66+ either (\_ -> assertFailure "XML Parse Error!")
67+ (@?= val) ntf
12468 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 "\""
144− )
145− )
146− ( OutputSerializationCSV $
147− quoteFields QuoteFieldsAsNeeded
148− <> recordDelimiter "\n"
149− <> fieldDelimiter ","
150− <> quoteCharacter "\""
151− <> quoteEscapeCharacter "\""
152− )
153− (Just False),
154− [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><FieldDelimiter>,</FieldDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><RecordDelimiter>
155−</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
156−</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
157− ),
158− ( setRequestProgressEnabled False $
159− setInputCompressionType CompressionTypeGzip $
160− selectRequest
161− "Select * from S3Object"
162− documentJsonInput
163− (outputJSONFromRecordDelimiter "\n"),
164− [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><JSON><Type>DOCUMENT</Type></JSON></InputSerialization><OutputSerialization><JSON><RecordDelimiter>
165−</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
166− ),
167− ( setRequestProgressEnabled False $
168− setInputCompressionType CompressionTypeNone $
169− selectRequest
170− "Select * from S3Object"
171− defaultParquetInput
172− ( outputCSVFromProps $
173− quoteFields QuoteFieldsAsNeeded
174− <> recordDelimiter "\n"
175− <> fieldDelimiter ","
176− <> quoteCharacter "\""
177− <> quoteEscapeCharacter "\""
178− ),
179− [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>NONE</CompressionType><Parquet/></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
180−</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
181− )
182− ]
69+ cases = [ Notification []
70+ [ NotificationConfig
71+ "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
72+ "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
73+ [ReducedRedundancyLostObject, ObjectCreated] def
74+ ]
75+ []
76+ , Notification
77+ [ NotificationConfig
78+ "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
79+ [ObjectCreatedPut]
80+ (Filter $ FilterKey $ FilterRules
81+ [ FilterRule "prefix" "images/"
82+ , FilterRule "suffix" ".jpg"])
83+ , NotificationConfig
84+ "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
85+ [ObjectCreated] def
86+ ]
87+ [ NotificationConfig
88+ "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
89+ [ReducedRedundancyLostObject] def
90+ ]
91+ [ NotificationConfig
92+ "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
93+ [ObjectCreated] def
94+ ]
95+ ]
test/Network/Minio/XmlParser/Test.hs+274−319
@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- Minio Haskell SDK, (C) 2017 Minio, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -13,49 +13,46 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−{-# LANGUAGE QuasiQuotes #-}
1716
1817module Network.Minio.XmlParser.Test
19− ( xmlParserTests,
20− )
21−where
18+ (
19+ xmlParserTests
20+ ) where
2221
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)
22+import qualified Control.Monad.Catch as MC
23+import Data.Time (fromGregorian)
24+import Test.Tasty
25+import Test.Tasty.HUnit
26+
27+import Lib.Prelude
28+
29+import Data.Default (def)
30+
31+import Network.Minio.Data
32+import Network.Minio.Errors
33+import Network.Minio.XmlParser
3434
3535xmlParserTests :: 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− ]
36+xmlParserTests = testGroup "XML Parser Tests"
37+ [ testCase "Test parseLocation" testParseLocation
38+ , testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload
39+ , testCase "Test parseListObjectsResponse" testParseListObjectsResult
40+ , testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result
41+ , testCase "Test parseListUploadsresponse" testParseListIncompleteUploads
42+ , testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse
43+ , testCase "Test parseListPartsResponse" testParseListPartsResponse
44+ , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse
45+ , testCase "Test parseNotification" testParseNotification
46+ ]
5047
51−tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
52−tryValidationErr = try
48+tryValidationErr :: (MC.MonadCatch m) => m a -> m (Either MErrV a)
49+tryValidationErr act = MC.try act
5350
5451assertValidtionErr :: MErrV -> Assertion
5552assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
5653
5754eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
58−eitherValidationErr (Left e) _ = assertValidtionErr e
55+eitherValidationErr (Left e) _ = assertValidtionErr e
5956eitherValidationErr (Right a) f = f a
6057
6158testParseLocation :: Assertion
@@ -63,341 +60,299 @@ testParseLocation = do
6360 -- 1. Test parsing of an invalid location constraint xml.
6461 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
6562 when (isRight parseResE) $
66− assertFailure $
67− "Parsing should have failed => " ++ show parseResE
63+ assertFailure $ "Parsing should have failed => " ++ show parseResE
6864
6965 forM_ cases $ \(xmldata, expectedLocation) -> do
7066 parseLocE <- tryValidationErr $ parseLocation xmldata
7167 either assertValidtionErr (@?= expectedLocation) parseLocE
7268 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− )
69+ cases = [
70+ -- 2. Test parsing of a valid location xml.
71+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
72+ \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
73+ "EU"
74+ )
75+ ,
76+ -- 3. Test parsing of a valid, empty location xml.
77+ ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
78+ "us-east-1"
79+ )
8380 ]
8481
82+
8583testParseNewMultipartUpload :: Assertion
8684testParseNewMultipartUpload = do
8785 forM_ cases $ \(xmldata, expectedUploadId) -> do
88− parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
86+ parsedUploadIdE <- tryValidationErr $ parseNewMultipartUpload xmldata
8987 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
9088 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− )
89+ cases = [
90+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
91+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
92+ \ <Bucket>example-bucket</Bucket>\
93+ \ <Key>example-object</Key>\
94+ \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
95+ \</InitiateMultipartUploadResult>",
96+ "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
97+ ),
98+ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
99+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
100+ \ <Bucket>example-bucket</Bucket>\
101+ \ <Key>example-object</Key>\
102+ \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
103+ \</InitiateMultipartUploadResult>",
104+ "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
105+ )
108106 ]
109107
110108testParseListObjectsResult :: Assertion
111109testParseListObjectsResult = 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
110+ let
111+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
112+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
113+ \<Name>bucket</Name>\
114+ \<Prefix/>\
115+ \<NextContinuationToken>opaque</NextContinuationToken>\
116+ \<KeyCount>1000</KeyCount>\
117+ \<MaxKeys>1000</MaxKeys>\
118+ \<IsTruncated>true</IsTruncated>\
119+ \<Contents>\
120+ \<Key>my-image.jpg</Key>\
121+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
122+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
123+ \<Size>434234</Size>\
124+ \<StorageClass>STANDARD</StorageClass>\
125+ \</Contents>\
126+ \</ListBucketResult>"
127+
128+ expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
129+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234
130+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
132131
133− parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
132+ parsedListObjectsResult <- tryValidationErr $ parseListObjectsResponse xmldata
134133 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
135134
136135testParseListObjectsV1Result :: Assertion
137136testParseListObjectsV1Result = 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
137+ let
138+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
139+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
140+ \<Name>bucket</Name>\
141+ \<Prefix/>\
142+ \<NextMarker>my-image1.jpg</NextMarker>\
143+ \<KeyCount>1000</KeyCount>\
144+ \<MaxKeys>1000</MaxKeys>\
145+ \<IsTruncated>true</IsTruncated>\
146+ \<Contents>\
147+ \<Key>my-image.jpg</Key>\
148+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
149+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
150+ \<Size>434234</Size>\
151+ \<StorageClass>STANDARD</StorageClass>\
152+ \</Contents>\
153+ \</ListBucketResult>"
158154
159− parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
155+ expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
156+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234
157+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
158+
159+ parsedListObjectsV1Result <- tryValidationErr $ parseListObjectsV1Response xmldata
160160 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
161161
162162testParseListIncompleteUploads :: Assertion
163163testParseListIncompleteUploads = 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/"]
164+ let
165+ xmldata = "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
166+ \<Bucket>example-bucket</Bucket>\
167+ \<KeyMarker/>\
168+ \<UploadIdMarker/>\
169+ \<NextKeyMarker>sample.jpg</NextKeyMarker>\
170+ \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
171+ \<Delimiter>/</Delimiter>\
172+ \<Prefix/>\
173+ \<MaxUploads>1000</MaxUploads>\
174+ \<IsTruncated>false</IsTruncated>\
175+ \<Upload>\
176+ \<Key>sample.jpg</Key>\
177+ \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
178+ \<Initiator>\
179+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
180+ \<DisplayName>s3-nickname</DisplayName>\
181+ \</Initiator>\
182+ \<Owner>\
183+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
184+ \<DisplayName>s3-nickname</DisplayName>\
185+ \</Owner>\
186+ \<StorageClass>STANDARD</StorageClass>\
187+ \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
188+ \</Upload>\
189+ \<CommonPrefixes>\
190+ \<Prefix>photos/</Prefix>\
191+ \</CommonPrefixes>\
192+ \<CommonPrefixes>\
193+ \<Prefix>videos/</Prefix>\
194+ \</CommonPrefixes>\
195+ \</ListMultipartUploadsResult>"
196+ expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
197+ uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
198+ initTime = UTCTime (fromGregorian 2010 11 26) 69857
199+ prefixes = ["photos/", "videos/"]
200200
201− parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
201+ parsedListUploadsResult <- tryValidationErr $ parseListUploadsResponse xmldata
202202 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
203203
204+
204205testParseCompleteMultipartUploadResponse :: Assertion
205206testParseCompleteMultipartUploadResponse = 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\""
207+ let
208+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
209+\<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
210+ \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
211+ \<Bucket>Example-Bucket</Bucket>\
212+ \<Key>Example-Object</Key>\
213+ \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
214+\</CompleteMultipartUploadResult>"
215+ expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
215216
216− parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
217+ parsedETagE <- runExceptT $ parseCompleteMultipartUploadResponse xmldata
217218 eitherValidationErr parsedETagE (@?= expectedETag)
218219
219220testParseListPartsResponse :: Assertion
220221testParseListPartsResponse = 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
222+ let
223+ xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
224+\<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
225+ \<Bucket>example-bucket</Bucket>\
226+ \<Key>example-object</Key>\
227+ \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
228+ \<Initiator>\
229+ \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
230+ \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
231+ \</Initiator>\
232+ \<Owner>\
233+ \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
234+ \<DisplayName>someName</DisplayName>\
235+ \</Owner>\
236+ \<StorageClass>STANDARD</StorageClass>\
237+ \<PartNumberMarker>1</PartNumberMarker>\
238+ \<NextPartNumberMarker>3</NextPartNumberMarker>\
239+ \<MaxParts>2</MaxParts>\
240+ \<IsTruncated>true</IsTruncated>\
241+ \<Part>\
242+ \<PartNumber>2</PartNumber>\
243+ \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
244+ \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
245+ \<Size>10485760</Size>\
246+ \</Part>\
247+ \<Part>\
248+ \<PartNumber>3</PartNumber>\
249+ \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
250+ \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
251+ \<Size>10485760</Size>\
252+ \</Part>\
253+\</ListPartsResult>"
254+
255+ expectedListResult = ListPartsResult True (Just 3) [part1, part2]
256+ part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
257+ modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
258+ part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
259+ modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
258260
259− parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
261+ parsedListPartsResult <- runExceptT $ parseListPartsResponse xmldata
260262 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
261263
262264testParseCopyObjectResponse :: Assertion
263265testParseCopyObjectResponse = 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− ]
266+ let
267+ cases = [ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
268+\<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
269+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
270+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
271+\</CopyObjectResult>",
272+ ("\"9b2cf535f27731c974343645a3985328\"",
273+ UTCTime (fromGregorian 2009 10 28) 81120))
274+ , ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
275+\<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
276+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
277+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
278+\</CopyPartResult>",
279+ ("\"9b2cf535f27731c974343645a3985328\"",
280+ UTCTime (fromGregorian 2009 10 28) 81120))]
284281
285282 forM_ cases $ \(xmldata, (etag, modTime)) -> do
286− parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
283+ parseResult <- runExceptT $ parseCopyObjectResponse xmldata
287284 eitherValidationErr parseResult (@?= (etag, modTime))
288285
289286testParseNotification :: Assertion
290287testParseNotification = 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
288+ let
289+ cases = [ ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
290+\ <TopicConfiguration>\
291+\ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
292+\ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
293+\ <Event>s3:ReducedRedundancyLostObject</Event>\
294+\ <Event>s3:ObjectCreated:*</Event>\
295+\ </TopicConfiguration>\
296+\</NotificationConfiguration>",
297+ Notification []
298+ [ NotificationConfig
303299 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
304300 "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− ]
301+ [ReducedRedundancyLostObject, ObjectCreated] def
302+ ]
303+ [])
304+ , ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
305+\ <CloudFunctionConfiguration>\
306+\ <Id>ObjectCreatedEvents</Id>\
307+\ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
308+\ <Event>s3:ObjectCreated:*</Event>\
309+\ </CloudFunctionConfiguration>\
310+\ <QueueConfiguration>\
311+\ <Id>1</Id>\
312+\ <Filter>\
313+\ <S3Key>\
314+\ <FilterRule>\
315+\ <Name>prefix</Name>\
316+\ <Value>images/</Value>\
317+\ </FilterRule>\
318+\ <FilterRule>\
319+\ <Name>suffix</Name>\
320+\ <Value>.jpg</Value>\
321+\ </FilterRule>\
322+\ </S3Key>\
323+\ </Filter>\
324+\ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
325+\ <Event>s3:ObjectCreated:Put</Event>\
326+\ </QueueConfiguration>\
327+\ <TopicConfiguration>\
328+\ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
329+\ <Event>s3:ReducedRedundancyLostObject</Event>\
330+\ </TopicConfiguration>\
331+\ <QueueConfiguration>\
332+\ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
333+\ <Event>s3:ObjectCreated:*</Event>\
334+\ </QueueConfiguration>)\
335+\</NotificationConfiguration>",
336+ Notification [ NotificationConfig
337+ "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
338+ [ObjectCreatedPut]
339+ (Filter $ FilterKey $ FilterRules
340+ [FilterRule "prefix" "images/",
341+ FilterRule "suffix" ".jpg"])
342+ , NotificationConfig
343+ "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
344+ [ObjectCreated] def
345+ ]
346+ [ NotificationConfig
347+ "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
348+ [ReducedRedundancyLostObject] def
349+ ]
350+ [ NotificationConfig
351+ "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
352+ [ObjectCreated] def
353+ ])
354+ ]
374355
375356 forM_ cases $ \(xmldata, val) -> do
376− result <- runExceptT $ runTestNS $ parseNotification xmldata
357+ result <- runExceptT $ parseNotification xmldata
377358 eitherValidationErr result (@?= val)
378−
379−-- | Tests parsing of both progress and stats
380−testParseSelectProgress :: Assertion
381−testParseSelectProgress = do
382− let cases =
383− [ ( [r|<?xml version="1.0" encoding="UTF-8"?>
384−<Progress>
385− <BytesScanned>512</BytesScanned>
386− <BytesProcessed>1024</BytesProcessed>
387− <BytesReturned>1024</BytesReturned>
388−</Progress>|],
389− Progress 512 1024 1024
390− ),
391− ( [r|<?xml version="1.0" encoding="UTF-8"?>
392−<Stats>
393− <BytesScanned>512</BytesScanned>
394− <BytesProcessed>1024</BytesProcessed>
395− <BytesReturned>1024</BytesReturned>
396−</Stats>|],
397− Progress 512 1024 1024
398− )
399− ]
400−
401− forM_ cases $ \(xmldata, progress) -> do
402− result <- runExceptT $ parseSelectProgress xmldata
403− eitherValidationErr result (@?= progress)
test/Spec.hs+66−81
@@ -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.
@@ -14,17 +14,18 @@
1414-- limitations under the License.
1515--
1616
17−import qualified Data.ByteString as B
17+import Test.Tasty
18+import Test.Tasty.QuickCheck as QC
19+
1820import 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
21+
22+import Lib.Prelude
23+
24+import Network.Minio.API.Test
25+import Network.Minio.PutObject
26+import Network.Minio.Utils.Test
27+import Network.Minio.XmlGenerator.Test
28+import Network.Minio.XmlParser.Test
2829
2930main :: IO ()
3031main = defaultMain tests
@@ -47,87 +48,71 @@ properties = testGroup "Properties" [qcProps] -- [scProps]
4748-- ]
4849
4950qcProps :: TestTree
50−qcProps =
51− testGroup
52− "(checked by QuickCheck)"
53− [ QC.testProperty "selectPartSizes:" $
54− \n ->
55− let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
51+qcProps = testGroup "(checked by QuickCheck)"
52+ [ QC.testProperty "selectPartSizes:" $
53+ \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
54+
5655 -- check that pns increments from 1.
57− isPNumsAscendingFrom1 = all (uncurry (==)) $ zip pns [1 ..]
56+ isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1..]
57+
5858 consPairs [] = []
5959 consPairs [_] = []
60− consPairs (a : (b : c)) = (a, b) : consPairs (b : c)
60+ consPairs (a:(b:c)) = (a, b):(consPairs (b:c))
61+
6162 -- check `offs` is monotonically increasing.
62− isOffsetsAsc = all (uncurry (<)) $ consPairs offs
63+ isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs
64+
6365 -- check sizes sums to n.
6466 isSumSizeOk = sum sizes == n
67+
6568 -- check sizes are constant except last
6669 isSizesConstantExceptLast =
67− all (uncurry (==)) (consPairs $ L.init sizes)
70+ all (\(a, b) -> a == b) (consPairs $ L.init sizes)
71+
6872 -- check each part except last is at least minPartSize;
6973 -- last part may be 0 only if it is the only part.
7074 nparts = length sizes
7175 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
118− Nothing -> B.length bs /= 32
119− ]
76+ if | nparts > 1 -> -- last part can be smaller but > 0
77+ all (>= minPartSize) (take (nparts - 1) sizes) &&
78+ all (\s -> s > 0) (drop (nparts - 1) sizes)
79+ | nparts == 1 -> -- size may be 0 here.
80+ maybe True (\x -> x >= 0 && x <= minPartSize) $
81+ headMay sizes
82+ | otherwise -> False
83+
84+ in n < 0 ||
85+ (isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk &&
86+ isSizesConstantExceptLast && isMinPartSizeOk)
87+
88+ , QC.testProperty "selectCopyRanges:" $
89+ \(start, end) ->
90+ let (_, pairs) = L.unzip (selectCopyRanges (start, end))
91+
92+ -- is last part's snd offset end?
93+ isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
94+ -- is first part's fst offset start
95+ isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
96+
97+ -- each pair is >=64MiB except last, and all those parts
98+ -- have same size.
99+ initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs
100+ isPartSizesOk = all (>= minPartSize) initSizes &&
101+ maybe True (\k -> all (== k) initSizes)
102+ (headMay initSizes)
103+
104+ -- returned offsets are contiguous.
105+ fsts = drop 1 $ map fst pairs
106+ snds = take (length pairs - 1) $ map snd pairs
107+ isContParts = length fsts == length snds &&
108+ and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
109+
110+ in start < 0 || start > end ||
111+ (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts)
112+ ]
120113
121114unitTests :: 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− ]
115+unitTests = testGroup "Unit tests" [xmlGeneratorTests, xmlParserTests,
116+ bucketNameValidityTests,
117+ objectNameValidityTests,
118+ limitedMapConcurrentlyTests]
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-----