git.iko.soy/typeable/minio-hs
on master help sign in
.github/workflows/ci.ymlnew file+266
since splitsnapshots
unifiedsplit
@@ -0,0 +1,266 @@
1+name: CI
2+
3+# Trigger the workflow on push or pull request, but only for the master branch
4+on:
5+ pull_request:
6+ branches: [master]
7+ push:
8+ branches: [master]
9+
10+# This ensures that previous jobs for the PR are canceled when the PR is
11+# updated.
12+concurrency:
13+ group: ${{ github.workflow }}-${{ github.head_ref }}
14+ cancel-in-progress: true
15+
16+# Env vars for tests
17+env:
18+ MINIO_ACCESS_KEY: minio
19+ MINIO_SECRET_KEY: minio123
20+ MINIO_LOCAL: 1
21+
22+jobs:
23+ ormolu:
24+ runs-on: ubuntu-latest
25+ steps:
26+ - uses: actions/checkout@v4
27+ - uses: haskell-actions/run-ormolu@v15
28+ with:
29+ version: "0.5.0.1"
30+
31+ hlint:
32+ runs-on: ubuntu-latest
33+ steps:
34+ - uses: actions/checkout@v4
35+
36+ - name: "Set up HLint"
37+ uses: haskell-actions/hlint-setup@v2
38+ with:
39+ version: "3.5"
40+
41+ - name: "Run HLint"
42+ uses: haskell-actions/hlint-run@v2
43+ with:
44+ path: '["src/", "test/", "examples"]'
45+ fail-on: warning
46+
47+ cabal:
48+ name: ${{ matrix.os }} / ghc-${{ matrix.ghc }} / cabal-${{ matrix.cabal }}
49+ runs-on: ${{ matrix.os }}
50+ needs: ormolu
51+ strategy:
52+ matrix:
53+ os: [ubuntu-latest, windows-latest, macos-latest]
54+ cabal: ["3.8", "latest"]
55+ ghc:
56+ - "9.8"
57+ - "9.6"
58+ - "9.4"
59+ - "9.2"
60+ - "9.0"
61+ - "8.10"
62+ exclude:
63+ # macos llvm issue for versions less than 9.2
64+ - os: macos-latest
65+ ghc: "8.10"
66+ - os: macos-latest
67+ ghc: "9.0"
68+ # Cabal 3.8 supports GHC < 9.6
69+ - cabal: "3.8"
70+ ghc: "9.6"
71+ - cabal: "3.8"
72+ ghc: "9.8"
73+
74+ steps:
75+ - uses: actions/checkout@v4
76+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
77+
78+ - uses: haskell-actions/setup@v2
79+ id: setup
80+ name: Setup Haskell
81+ with:
82+ ghc-version: ${{ matrix.ghc }}
83+ cabal-version: ${{ matrix.cabal }}
84+ cabal-update: true
85+
86+ - name: Configure
87+ run: |
88+ cabal configure --enable-tests --enable-benchmarks --test-show-details=direct -fexamples -fdev -flive-test
89+ cabal build all --dry-run
90+ # The last step generates dist-newstyle/cache/plan.json for the cache key.
91+
92+ - name: Restore cached dependencies
93+ uses: actions/cache/restore@v4
94+ id: cache
95+ env:
96+ key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
97+ with:
98+ path: ${{ steps.setup.outputs.cabal-store }}
99+ key: ${{ env.key }}-plan-${{ hashFiles('**/plan.json') }}
100+ restore-keys: ${{ env.key }}-
101+
102+ - name: Install dependencies
103+ # If we had an exact cache hit, the dependencies will be up to date.
104+ if: steps.cache.outputs.cache-hit != 'true'
105+ run: cabal build all --only-dependencies
106+
107+ # Cache dependencies already here, so that we do not have to rebuild them should the subsequent steps fail.
108+ - name: Save cached dependencies
109+ uses: actions/cache/save@v4
110+ # If we had an exact cache hit, trying to save the cache would error because of key clash.
111+ if: steps.cache.outputs.cache-hit != 'true'
112+ with:
113+ path: ${{ steps.setup.outputs.cabal-store }}
114+ key: ${{ steps.cache.outputs.cache-primary-key }}
115+
116+ - name: Build
117+ run: |
118+ cabal build all
119+
120+ - name: Setup TLS certs for MinIO for testing (Linux)
121+ if: matrix.os == 'ubuntu-latest'
122+ run: |
123+ mkdir -p /tmp/minio /tmp/minio-config/certs
124+ cp test/cert/* /tmp/minio-config/certs/
125+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
126+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
127+ sudo update-ca-certificates
128+
129+ ## Currently disable TLS setup for MacOS due to issues in trusting it on MacOS.
130+ - name: Setup TLS certs for MinIO for testing (MacOS)
131+ if: matrix.os == 'macos-latest'
132+ run: |
133+ mkdir -p /tmp/minio /tmp/minio-config/certs
134+ cp test/cert/* /tmp/minio-config/certs/
135+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
136+ # sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
137+
138+ - name: Setup MinIO for testing (Windows)
139+ if: matrix.os == 'windows-latest'
140+ run: |
141+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
142+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
143+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
144+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
145+
146+ - name: Test (Linux)
147+ if: matrix.os == 'ubuntu-latest'
148+ env:
149+ MINIO_SECURE: 1
150+ run: |
151+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
152+ ghc --version
153+ cabal --version
154+ cabal test all
155+
156+ - name: Test (MacOS)
157+ if: matrix.os == 'macos-latest'
158+ # # Leave MINIO_SECURE unset to disable TLS in tests.
159+ # env:
160+ # MINIO_SECURE: 1
161+ run: |
162+ /tmp/minio/minio server --quiet data1 data2 data3 data4 2>&1 > minio.log &
163+ ghc --version
164+ cabal --version
165+ cabal test all
166+
167+ - name: Test (Windows)
168+ if: matrix.os == 'windows-latest'
169+ env:
170+ MINIO_SECURE: 1
171+ run: |
172+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
173+ ghc --version
174+ cabal --version
175+ cabal test all
176+
177+ stack:
178+ name: stack / ghc ${{ matrix.ghc }}
179+ runs-on: ${{ matrix.os }}
180+ env:
181+ MINIO_SECURE: 1
182+ strategy:
183+ matrix:
184+ ghc:
185+ - "8.10.7"
186+ - "9.0.2"
187+ - "9.2.8"
188+ - "9.4.8"
189+ - "9.6.5"
190+ - "9.8.2"
191+ os: [ubuntu-latest]
192+
193+ steps:
194+ - uses: actions/checkout@v4
195+ if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'
196+
197+ - uses: haskell-actions/setup@v2
198+ with:
199+ ghc-version: ${{ matrix.ghc }}
200+ enable-stack: true
201+ stack-version: "latest"
202+
203+ - uses: actions/cache@v4
204+ name: Cache ~/.stack
205+ with:
206+ path: ~/.stack
207+ key: ${{ runner.os }}-stack-global-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}
208+ restore-keys: |
209+ ${{ runner.os }}-stack-global-
210+ - uses: actions/cache@v4
211+ name: Cache .stack-work
212+ with:
213+ path: .stack-work
214+ key: ${{ runner.os }}-stack-work-${{ hashFiles('stack.yaml') }}-${{ hashFiles('package.yaml') }}-${{ hashFiles('**/*.hs') }}
215+ restore-keys: |
216+ ${{ runner.os }}-stack-work-
217+
218+ - name: Install dependencies
219+ run: |
220+ stack --version
221+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies
222+
223+ - name: Build
224+ run: |
225+ stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --flag minio-hs:examples --flag minio-hs:live-test --flag minio-hs:dev
226+
227+ - name: Setup MinIO for testing (Linux)
228+ if: matrix.os == 'ubuntu-latest'
229+ run: |
230+ mkdir -p /tmp/minio /tmp/minio-config/certs
231+ cp test/cert/* /tmp/minio-config/certs/
232+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/linux-amd64/minio; chmod +x ./minio)
233+ sudo cp /tmp/minio-config/certs/public.crt /usr/local/share/ca-certificates/
234+ sudo update-ca-certificates
235+
236+ - name: Setup MinIO for testing (MacOS)
237+ if: matrix.os == 'macos-latest'
238+ run: |
239+ mkdir -p /tmp/minio /tmp/minio-config/certs
240+ cp test/cert/* /tmp/minio-config/certs/
241+ (cd /tmp/minio; wget -q https://dl.min.io/server/minio/release/darwin-amd64/minio; chmod +x ./minio)
242+ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/minio-config/certs/public.crt
243+
244+ - name: Setup MinIO for testing (Windows)
245+ if: matrix.os == 'windows-latest'
246+ run: |
247+ New-Item -ItemType Directory -Path "$env:temp/minio-config/certs/"
248+ Copy-Item -Path test\cert\* -Destination "$env:temp/minio-config/certs/"
249+ Invoke-WebRequest -Uri https://dl.minio.io/server/minio/release/windows-amd64/minio.exe -OutFile $HOME/minio.exe
250+ Import-Certificate -FilePath "$env:temp/minio-config/certs/public.crt" -CertStoreLocation Cert:\LocalMachine\Root
251+
252+ - name: Test (Non-Windows)
253+ if: matrix.os != 'windows-latest'
254+ run: |
255+ /tmp/minio/minio server --quiet --certs-dir /tmp/minio-config/certs data1 data2 data3 data4 2>&1 > minio.log &
256+ ghc --version
257+ stack --version
258+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
259+
260+ - name: Test (Windows)
261+ if: matrix.os == 'windows-latest'
262+ run: |
263+ Start-Process -NoNewWindow -FilePath "$HOME/minio.exe" -ArgumentList "--certs-dir", "$env:temp/minio-config/certs", "server", "$env:temp/data1", "$env:temp/data2", "$env:temp/data3", "$env:temp/data4"
264+ ghc --version
265+ cabal --version
266+ stack test --system-ghc --flag minio-hs:live-test --flag minio-hs:dev
.gitignore+1
@@ -17,3 +17,4 @@ cabal.sandbox.config
17*.eventlog
17*.eventlog
18.stack-work/
18.stack-work/
19cabal.project.local
19cabal.project.local
20+*~
.travis.ymldeleted−40
@@ -1,40 +0,0 @@
1−# This is the simple Travis configuration, which is intended for use
2−# on applications which do not require cross-platform and
3−# multiple-GHC-version support. For more information and other
4−# options, see:
5−#
6−# https://docs.haskellstack.org/en/stable/travis_ci/
7−#
8−# Copy these contents into the root directory of your Github project in a file
9−# named .travis.yml
10−
11−# Use new container infrastructure to enable caching
12−sudo: false
13−
14−# Do not choose a language; we provide our own build tools.
15−language: generic
16−
17−# Caching so the next build will be fast too.
18−cache:
19− directories:
20− - $HOME/.stack
21−
22−# Ensure necessary system libraries are present
23−addons:
24− apt:
25− packages:
26− - libgmp-dev
27−
28−before_install:
29−# Download and unpack the stack executable
30−- mkdir -p ~/.local/bin
31−- export PATH=$HOME/.local/bin:$PATH
32−- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
33−
34−install:
35−# Build dependencies
36−- stack --no-terminal --install-ghc test --only-dependencies
37−
38−script:
39−# Build the package, its tests, and its docs and run the tests
40−- stack --no-terminal test --haddock --no-haddock-deps --test-arguments --num-threads=1
CHANGELOG.md+115−1
@@ -1,6 +1,120 @@
1Changelog
1Changelog
2==========
2==========
3
3
4+## Version 1.7.0 -- Unreleased
5+
6+* Fix data type `EventMessage` to not export partial fields (#179)
7+* Bump up min bound on time dep and fix deprecation warnings (#181)
8+* Add `dev` flag to cabal for building with warnings as errors (#182)
9+* Fix AWS region map (#185)
10+* Fix XML generator tests (#187)
11+* Add support for STS Assume Role API (#188)
12+
13+### Breaking changes in 1.7.0
14+
15+* `Credentials` type has been removed. Use `CredentialValue` instead.
16+* `Provider` type has been replaced with `CredentialLoader`.
17+* `EventMessage` data type is updated.
18+
19+## Version 1.6.0
20+
21+* HLint fixes - some types were changed to newtype (#173)
22+* Fix XML generation test for S3 SELECT (#161)
23+* Use region specific endpoints for AWS S3 in presigned Urls (#164)
24+* Replace protolude with relude and build with GHC 9.0.2 (#168)
25+* Support aeson 2 (#169)
26+* CI updates and code formatting changes with ormolu 0.5.0.0
27+
28+## Version 1.5.3
29+
30+* Fix windows build
31+* Fix support for Yandex Storage (#147)
32+* Fix for HEAD requests to S3/Minio (#155)
33+* Bump up some dependencies, new code formatting, Github CI, example fixes and other minor improvements.
34+
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+
4## Version 0.3.2
118## Version 0.3.2
5
119
6This release brings the following changes:
120This release brings the following changes:
@@ -19,7 +133,7 @@ This release brings the following changes:
19This is a bug-fix release:
133This is a bug-fix release:
20
134
21* Fix concurrency bug in `limitedMapConcurrently` (#53)
135* Fix concurrency bug in `limitedMapConcurrently` (#53)
22−* Fix tests related to listing incomplete uploads to accommodate Minio
136+* Fix tests related to listing incomplete uploads to accommodate MinIO
23 server's changed behaviour to not list incomplete uploads. Note that
137 server's changed behaviour to not list incomplete uploads. Note that
24 running these tests against AWS S3 are expected to fail. (#54)
138 running these tests against AWS S3 are expected to fail. (#54)
25
139
CNAMEnew file+1
@@ -0,0 +1 @@
1+minio-hs.min.io
README.md+128−43
@@ -1,64 +1,94 @@
1−# Minio Client SDK for Haskell [![Build Status](https://travis-ci.org/minio/minio-hs.svg?branch=master)](https://travis-ci.org/minio/minio-hs)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
1+# MinIO Haskell Client SDK for Amazon S3 Compatible Cloud Storage [![CI](https://github.com/minio/minio-hs/actions/workflows/ci.yml/badge.svg)](https://github.com/minio/minio-hs/actions/workflows/ci.yml)[![Hackage](https://img.shields.io/hackage/v/minio-hs.svg)](https://hackage.haskell.org/package/minio-hs)[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
2
2
3−The Minio Haskell Client SDK provides simple APIs to access [Minio](https://minio.io) and Amazon S3 compatible object storage server.
3+The MinIO Haskell Client SDK provides simple APIs to access [MinIO](https://min.io) and any Amazon S3 compatible object storage.
4
4
5−## Minimum Requirements
5+This guide assumes that you have a working [Haskell development environment](https://www.haskell.org/downloads/).
6−
7−- The Haskell [stack](https://docs.haskellstack.org/en/stable/README/)
8
6
9## Installation
7## Installation
10
8
11−```sh
9+### Add to your project
12−git clone https://github.com/minio/minio-hs.git
13
10
14−cd minio-hs/
11+Simply add `minio-hs` to your project's `.cabal` dependencies section or if you are using hpack, to your `package.yaml` file as usual.
15
12
16−stack install
13+### Try it out in a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
17−```
18
14
19−Tests can be run with:
15+#### For a cabal based environment
20
16
17+Download the library source and change to the extracted directory:
18+
21−```sh
19+``` sh
20+$ cabal get minio-hs
21+$ cd minio-hs-1.6.0/ # directory name could be different
22+```
22
23
23−stack test
24+Then load the `ghci` REPL environment with the library and browse the available APIs:
24
25
26+``` sh
27+$ cabal repl
28+ghci> :browse Network.Minio
25```
29```
26
30
27−A section of the tests use the remote Minio Play server at
31+#### For a stack based environment
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).
33
32
34−Documentation can be locally built with:
33+From your home folder or any non-haskell project directory, just run:
35
34
36```sh
35```sh
36+stack install minio-hs
37+```
37
38
38−stack haddock
39+Then start an interpreter session and browse the available APIs with:
39
40
41+```sh
42+$ stack ghci
43+> :browse Network.Minio
40```
44```
41
45
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+
42−## Quick-Start Example - File Uploader
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.
43
55
44### FileUploader.hs
56### FileUploader.hs
45``` haskell
57``` haskell
46#!/usr/bin/env stack
58#!/usr/bin/env stack
47−-- stack --resolver lts-9.1 runghc --package minio-hs --package optparse-applicative --package filepath
59+-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
60+
61+--
62+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
63+--
64+-- Licensed under the Apache License, Version 2.0 (the "License");
65+-- you may not use this file except in compliance with the License.
66+-- You may obtain a copy of the License at
67+--
68+-- http://www.apache.org/licenses/LICENSE-2.0
69+--
70+-- Unless required by applicable law or agreed to in writing, software
71+-- distributed under the License is distributed on an "AS IS" BASIS,
72+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
73+-- See the License for the specific language governing permissions and
74+-- limitations under the License.
75+--
48
76
49−{-# Language OverloadedStrings, ScopedTypeVariables #-}
50−import Network.Minio
51
77
52−import Control.Monad.Catch (catchIf)
78+{-# LANGUAGE OverloadedStrings #-}
53−import Control.Monad.IO.Class (liftIO)
79+{-# LANGUAGE ScopedTypeVariables #-}
80+import Network.Minio
81+
54−import Data.Monoid ((<>))
82+import Data.Monoid ((<>))
55−import Data.Text (pack)
83+import Data.Text (pack)
56−import Options.Applicative
84+import Options.Applicative
57−import Prelude
58−import System.FilePath.Posix
85+import System.FilePath.Posix
86+import UnliftIO (throwIO, try)
87+
88+import Prelude
59
89
60-- | The following example uses minio's play server at
90-- | The following example uses minio's play server at
61−-- https://play.minio.io:9000. The endpoint and associated
91+-- https://play.min.io. The endpoint and associated
62-- credentials are provided via the libary constant,
92-- credentials are provided via the libary constant,
63--
93--
64-- > minioPlayCI :: ConnectInfo
94-- > minioPlayCI :: ConnectInfo
@@ -68,7 +98,7 @@ import System.FilePath.Posix
68fileNameArgs :: Parser FilePath
98fileNameArgs :: Parser FilePath
69fileNameArgs = strArgument
99fileNameArgs = strArgument
70 (metavar "FILENAME"
100 (metavar "FILENAME"
71− <> help "Name of file to upload to AWS S3 or a Minio server")
101+ <> help "Name of file to upload to AWS S3 or a MinIO server")
72
102
73cmdParser = info
103cmdParser = info
74 (helper <*> fileNameArgs)
104 (helper <*> fileNameArgs)
@@ -77,27 +107,30 @@ cmdParser = info
77 <> header
107 <> header
78 "FileUploader - a simple file-uploader program using minio-hs")
108 "FileUploader - a simple file-uploader program using minio-hs")
79
109
80−ignoreMinioErr :: ServiceErr -> Minio ()
81−ignoreMinioErr = return . const ()
82−
83−
84main :: IO ()
110main :: IO ()
85main = do
111main = do
86 let bucket = "my-bucket"
112 let bucket = "my-bucket"
87
113
88− -- Parse command line argument, namely --filename.
114+ -- Parse command line argument
89 filepath <- execParser cmdParser
115 filepath <- execParser cmdParser
90 let object = pack $ takeBaseName filepath
116 let object = pack $ takeBaseName filepath
91
117
92 res <- runMinio minioPlayCI $ do
118 res <- runMinio minioPlayCI $ do
93 -- Make a bucket; catch bucket already exists exception if thrown.
119 -- Make a bucket; catch bucket already exists exception if thrown.
94− catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
120+ bErr <- try $ makeBucket bucket Nothing
121+
122+ -- If the bucket already exists, we would get a specific
123+ -- `ServiceErr` exception thrown.
124+ case bErr of
125+ Left BucketAlreadyOwnedByYou -> return ()
126+ Left e -> throwIO e
127+ Right _ -> return ()
95
128
96− -- Upload filepath to bucket; object is derived from filepath.
129+ -- Upload filepath to bucket; object name is derived from filepath.
97− fPutObject bucket object filepath
130+ fPutObject bucket object filepath defaultPutObjectOptions
98
131
99 case res of
132 case res of
100− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
133+ Left e -> putStrLn $ "file upload failed due to " ++ show e
101 Right () -> putStrLn "file upload succeeded."
134 Right () -> putStrLn "file upload succeeded."
102```
135```
103
136
@@ -111,3 +144,55 @@ main = do
111## Contribute
144## Contribute
112
145
113[Contributors Guide](https://github.com/minio/minio-hs/blob/master/CONTRIBUTING.md)
146[Contributors Guide](https://github.com/minio/minio-hs/blob/master/CONTRIBUTING.md)
147+
148+### Development
149+
150+#### Download the source
151+
152+```sh
153+$ git clone https://github.com/minio/minio-hs.git
154+$ cd minio-hs/
155+```
156+
157+#### Build the package:
158+
159+With `cabal`:
160+
161+```sh
162+$ # Configure cabal for development enabling all optional flags defined by the package.
163+$ cabal configure --enable-tests --test-show-details=direct -fexamples -fdev -flive-test
164+$ cabal build
165+```
166+
167+With `stack`:
168+
169+``` sh
170+$ stack build --test --no-run-tests --flag minio-hs:live-test --flag minio-hs:dev --flag minio-hs:examples
171+```
172+#### Running tests:
173+
174+A section of the tests use the remote MinIO Play server at `https://play.min.io` by default. For library development, using this remote server maybe slow. To run the tests against a locally running MinIO live server at `http://localhost:9000` with the credentials `access_key=minio` and `secret_key=minio123`, just set the environment `MINIO_LOCAL` to any value (and unset it to switch back to Play).
175+
176+With `cabal`:
177+
178+```sh
179+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
180+$ cabal test
181+```
182+
183+With `stack`:
184+
185+``` sh
186+$ export MINIO_LOCAL=1 # to run live tests against local MinIO server
187+stack test --flag minio-hs:live-test --flag minio-hs:dev
188+```
189+
190+This will run all the test suites.
191+
192+#### Building documentation:
193+
194+```sh
195+$ cabal haddock
196+$ # OR
197+$ stack haddock
198+```
docs/API.md+352−227
@@ -1,8 +1,8 @@
1−# Minio Haskell SDK API Reference
1+# MinIO Haskell SDK API Reference
2
2
3−## Initialize Minio Client object.
3+## Initialize MinIO Client object.
4
4
5−### Minio - for public Play server
5+### MinIO - for public Play server
6
6
7```haskell
7```haskell
8minioPlayCI :: ConnectInfo
8minioPlayCI :: ConnectInfo
@@ -20,20 +20,20 @@ awsCI { connectAccesskey = "your-access-key"
20
20
21```
21```
22
22
23−|Bucket operations|Object Operations|Presigned Operations|
23+| Bucket operations | Object Operations | Presigned Operations |
24−|:---|:---|:---|
24+|:--------------------------------------------------|:----------------------------------------------|:--------------------------------------------------|
25−|[`listBuckets`](#listBuckets) |[`getObject`](#getObject)|[`presignedGetObjectUrl`](#presignedGetObjectUrl)|
25+| [`listBuckets`](#listBuckets) | [`getObject`](#getObject) | [`presignedGetObjectUrl`](#presignedGetObjectUrl) |
26−|[`makeBucket`](#makeBucket)|[`putObject`](#putObject)|[`presignedPutObjectUrl`](#presignedPutObjectUrl)|
26+| [`makeBucket`](#makeBucket) | [`putObject`](#putObject) | [`presignedPutObjectUrl`](#presignedPutObjectUrl) |
27−|[`removeBucket`](#removeBucket)|[`fGetObject`](#fGetObject)|[`presignedPostPolicy`](#presignedPostPolicy)|
27+| [`removeBucket`](#removeBucket) | [`fGetObject`](#fGetObject) | [`presignedPostPolicy`](#presignedPostPolicy) |
28−|[`listObjects`](#listObjects)|[`fPutObject`](#fPutObject)||
28+| [`listObjects`](#listObjects) | [`fPutObject`](#fPutObject) | |
29−|[`listObjectsV1`](#listObjectsV1)|[`copyObject`](#copyObject)||
29+| [`listObjectsV1`](#listObjectsV1) | [`copyObject`](#copyObject) | |
30−|[`listIncompleteUploads`](#listIncompleteUploads)|[`removeObject`](#removeObject)||
30+| [`listIncompleteUploads`](#listIncompleteUploads) | [`removeObject`](#removeObject) | |
31−|[`bucketExists`](#bucketExists)|||
31+| [`bucketExists`](#bucketExists) | [`selectObjectContent`](#selectObjectContent) | |
32
32
33## 1. Connecting and running operations on the storage service
33## 1. Connecting and running operations on the storage service
34
34
35−The Haskell Minio SDK provides high-level functionality to perform
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
36+operations on a MinIO server or any AWS S3-like API compatible storage
37service.
37service.
38
38
39### The `ConnectInfo` type
39### The `ConnectInfo` type
@@ -69,42 +69,42 @@ enable/disable the automatic region discovery behaviour.
69
69
70The parameters in the expression `awsWithRegion region autoDiscover` are:
70The parameters in the expression `awsWithRegion region autoDiscover` are:
71
71
72−|Parameter|Type|Description|
72+| Parameter | Type | Description |
73−|:---|:---|:---|
73+|:---------------|:----------------------------|:---------------------------------------------------------------------------------------------------------------------------|
74−| `region` | _Region_ (alias for `Text`) | The region to connect to by default for all requests. |
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.|
75+| `autoDiscover` | _Bool_ | If `True`, region discovery will be enabled. If `False`, discovery is disabled, and all requests go the given region only. |
76
76
77#### minioPlayCI :: ConnectInfo
77#### minioPlayCI :: ConnectInfo
78
78
79This constructor provides connection and authentication information to
79This constructor provides connection and authentication information to
80−connect to the public Minio Play server at
80+connect to the public MinIO Play server at
81−`https://play.minio.io:9000/`.
81+`https://play.min.io/`.
82
82
83#### minioCI :: Text -> Int -> Bool -> ConnectInfo
83#### minioCI :: Text -> Int -> Bool -> ConnectInfo
84
84
85−Use to connect to a Minio server.
85+Use to connect to a MinIO server.
86
86
87The parameters in the expression `minioCI host port isSecure` are:
87The parameters in the expression `minioCI host port isSecure` are:
88
88
89−|Parameter|Type|Description|
89+| Parameter | Type | Description |
90−|:---|:---|:---|
90+|:-----------|:-------|:--------------------------------------------------------|
91−| `host` | _Text_ | Hostname of the Minio or other S3-API compatible server |
91+| `host` | _Text_ | Hostname of the MinIO or other S3-API compatible server |
92−| `port` | _Int_ | Port number to connect to|
92+| `port` | _Int_ | Port number to connect to |
93−| `isSecure` | _Bool_ | Does the server use HTTPS? |
93+| `isSecure` | _Bool_ | Does the server use HTTPS? |
94
94
95#### The ConnectInfo fields and Default instance
95#### The ConnectInfo fields and Default instance
96
96
97The following table shows the fields in the `ConnectInfo` record-type:
97The following table shows the fields in the `ConnectInfo` record-type:
98
98
99−| Field | Type | Description |
99+| Field | Type | Description |
100−|:---|:---|:---|
100+|:----------------------------|:----------------------------|:--------------------------------------------------------------------------------------------------|
101−| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
101+| `connectHost` | _Text_ | Host name of the server. Defaults to `localhost`. |
102−| `connectPort` | _Int_ | Port number on which the server listens. Defaults to `9000`. |
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`. |
103+| `connectAccessKey` | _Text_ | Access key to use in authentication. Defaults to `minio`. |
104−| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
104+| `connectSecretkey` | _Text_ | Secret key to use in authentication. Defaults to `minio123`. |
105−| `connectIsSecure` | _Bool_ | Specifies if the server used TLS. Defaults to `False` |
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' |
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`|
107+| `connectAutoDiscoverRegion` | _Bool_ | Specifies if the library should automatically discover the region of a bucket. Defaults to `True` |
108
108
109The `def` value of type `ConnectInfo` has all the above default
109The `def` value of type `ConnectInfo` has all the above default
110values.
110values.
@@ -112,7 +112,7 @@ values.
112### The Minio Monad
112### The Minio Monad
113
113
114This monad provides the required environment to perform requests
114This 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
116connection information from the `ConnectInfo` value provided to it. It
116connection information from the `ConnectInfo` value provided to it. It
117performs connection pooling, bucket location caching, error handling
117performs connection pooling, bucket location caching, error handling
118and resource clean-up actions.
118and resource clean-up actions.
@@ -148,17 +148,17 @@ Lists buckets.
148
148
149__Return Value__
149__Return Value__
150
150
151−|Return type |Description |
151+| Return type | Description |
152−|:---|:---|
152+|:---------------------|:----------------|
153−| _Minio [BucketInfo]_| List of buckets |
153+| _Minio [BucketInfo]_ | List of buckets |
154
154
155
155
156__BucketInfo record type__
156__BucketInfo record type__
157
157
158−|Field |Type |Description |
158+| Field | Type | Description |
159−|:---|:---| :---|
159+|:-----------------|:---------------------------|:----------------------------|
160−| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
160+| `biName` | _Bucket_ (alias of `Text`) | Name of the bucket |
161−| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
161+| `biCreationDate` | _UTCTime_ | Creation time of the bucket |
162
162
163
163
164<a name="makeBucket"></a>
164<a name="makeBucket"></a>
@@ -171,10 +171,10 @@ __Parameters__
171
171
172In the expression `makeBucket bucketName region` the arguments are:
172In the expression `makeBucket bucketName region` the arguments are:
173
173
174−| Param | Type | Description |
174+| Param | Type | Description |
175−|---|---|---|
175+|--------------|-----------------------------|-----------------------------------------------------------------------------------------------------|
176−|`bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
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`.|
177+| `region` | _Maybe Region_ | Region where the bucket is to be created. If not specified, default to the region in `ConnectInfo`. |
178
178
179__Example__
179__Example__
180
180
@@ -185,7 +185,6 @@ main :: IO ()
185main = do
185main = do
186 res <- runMinio minioPlayCI $ do
186 res <- runMinio minioPlayCI $ do
187 makeBucket bucketName (Just "us-east-1")
187 makeBucket bucketName (Just "us-east-1")
188−
189 case res of
188 case res of
190 Left err -> putStrLn $ "Failed to make bucket: " ++ (show res)
189 Left err -> putStrLn $ "Failed to make bucket: " ++ (show res)
191 Right _ -> putStrLn $ "makeBucket successful."
190 Right _ -> putStrLn $ "makeBucket successful."
@@ -201,9 +200,9 @@ __Parameters__
201
200
202In the expression `removeBucket bucketName` the arguments are:
201In the expression `removeBucket bucketName` the arguments are:
203
202
204−| Param | Type | Description |
203+| Param | Type | Description |
205−|---|---|---|
204+|--------------|-----------------------------|--------------------|
206−|`bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
205+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
207
206
208
207
209__Example__
208__Example__
@@ -225,7 +224,7 @@ main = do
225
224
226
225
227<a name="listObjects"></a>
226<a name="listObjects"></a>
228−### listObjects :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
227+### listObjects :: Bucket -> Maybe Text -> Bool -> C.ConduitM () ObjectInfo Minio ()
229
228
230List objects in the given bucket, implements version 2 of AWS S3 API.
229List objects in the given bucket, implements version 2 of AWS S3 API.
231
230
@@ -234,34 +233,44 @@ __Parameters__
234In the expression `listObjects bucketName prefix recursive` the
233In the expression `listObjects bucketName prefix recursive` the
235arguments are:
234arguments are:
236
235
237−|Param |Type |Description |
236+| Param | Type | Description |
238−|:---|:---| :---|
237+|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
239−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
238+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
240−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
239+| `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 '/'. |
240+| `recursive` | _Bool_ | `True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
242
241
243__Return Value__
242__Return Value__
244
243
245−|Return type |Description |
244+| Return type | Description |
246−|:---|:---|
245+|:------------------------------------|:------------------------------------------------------------------------|
247−| _C.Producer Minio ObjectInfo_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
246+| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
248
247
249__ObjectInfo record type__
248__ObjectInfo record type__
250
249
251−|Field |Type |Description |
250+| Field | Type | Description |
252−|:---|:---| :---|
251+|:-------------|:----------------------------|:-------------------------------------|
253−|`oiObject` | _Object_ (alias for `Text`) | Name of object |
252+| `oiObject` | _Object_ (alias for `Text`) | Name of object |
254−|`oiModTime` | _UTCTime_ | Last modified time of the object |
253+| `oiModTime` | _UTCTime_ | Last modified time of the object |
255−|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
254+| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
256−|`oiSize` | _Int64_ | Size of the object in bytes |
255+| `oiSize` | _Int64_ | Size of the object in bytes |
256+| `oiMetadata` | _HashMap Text Text_ | Map of key-value user-metadata pairs |
257
257
258__Example__
258__Example__
259
259
260``` haskell
260``` haskell
261−{-# Language OverloadedStrings #-}
261+{-# LANGUAGE OverloadedStrings #-}
262+import Network.Minio
262
263
263−import Data.Conduit (($$))
264+import Conduit
264−import Conduit.Combinators (sinkList)
265+import Prelude
266+
267+
268+-- | The following example uses MinIO play server at
269+-- https://play.min.io. The endpoint and associated
270+-- credentials are provided via the libary constant,
271+--
272+-- > minioPlayCI :: ConnectInfo
273+--
265
274
266main :: IO ()
275main :: IO ()
267main = do
276main = do
@@ -269,15 +278,14 @@ main = do
269 bucket = "test"
278 bucket = "test"
270
279
271 -- Performs a recursive listing of all objects under bucket "test"
280 -- Performs a recursive listing of all objects under bucket "test"
272− -- on play.minio.io.
281+ -- on play.min.io.
273− res <- runMinio minioPlayCI $ do
282+ res <- runMinio minioPlayCI $
274− listObjects bucket Nothing True $$ sinkList
283+ runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
275 print res
284 print res
276−
277```
285```
278
286
279<a name="listObjectsV1"></a>
287<a name="listObjectsV1"></a>
280−### listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
288+### listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.ConduitM () ObjectInfo Minio ()
281
289
282List objects in the given bucket, implements version 1 of AWS S3 API. This API
290List objects in the given bucket, implements version 1 of AWS S3 API. This API
283is provided for legacy S3 compatible object storage endpoints.
291is provided for legacy S3 compatible object storage endpoints.
@@ -287,34 +295,43 @@ __Parameters__
287In the expression `listObjectsV1 bucketName prefix recursive` the
295In the expression `listObjectsV1 bucketName prefix recursive` the
288arguments are:
296arguments are:
289
297
290−|Param |Type |Description |
298+| Param | Type | Description |
291−|:---|:---| :---|
299+|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
292−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
300+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
293−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have |
301+| `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 '/'. |
302+| `recursive` | _Bool_ | `True` indicates recursive style listing and `False` indicates directory style listing delimited by '/'. |
295
303
296__Return Value__
304__Return Value__
297
305
298−|Return type |Description |
306+| Return type | Description |
299−|:---|:---|
307+|:------------------------------------|:------------------------------------------------------------------------|
300−| _C.Producer Minio ObjectInfo_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
308+| _C.ConduitM () ObjectInfo Minio ()_ | A Conduit Producer of `ObjectInfo` values corresponding to each object. |
301
309
302__ObjectInfo record type__
310__ObjectInfo record type__
303
311
304−|Field |Type |Description |
312+| Field | Type | Description |
305−|:---|:---| :---|
313+|:------------|:----------------------------|:---------------------------------|
306−|`oiObject` | _Object_ (alias for `Text`) | Name of object |
314+| `oiObject` | _Object_ (alias for `Text`) | Name of object |
307−|`oiModTime` | _UTCTime_ | Last modified time of the object |
315+| `oiModTime` | _UTCTime_ | Last modified time of the object |
308−|`oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
316+| `oiETag` | _ETag_ (alias for `Text`) | ETag of the object |
309−|`oiSize` | _Int64_ | Size of the object in bytes |
317+| `oiSize` | _Int64_ | Size of the object in bytes |
310
318
311__Example__
319__Example__
312
320
313``` haskell
321``` haskell
314−{-# Language OverloadedStrings #-}
322+{-# LANGUAGE OverloadedStrings #-}
323+import Network.Minio
324+
325+import Conduit
326+import Prelude
315
327
316−import Data.Conduit (($$))
328+
317−import Conduit.Combinators (sinkList)
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+--
318
335
319main :: IO ()
336main :: IO ()
320main = do
337main = do
@@ -322,11 +339,10 @@ main = do
322 bucket = "test"
339 bucket = "test"
323
340
324 -- Performs a recursive listing of all objects under bucket "test"
341 -- Performs a recursive listing of all objects under bucket "test"
325− -- on play.minio.io.
342+ -- on play.min.io.
326− res <- runMinio minioPlayCI $ do
343+ res <- runMinio minioPlayCI $
327− listObjectsV1 bucket Nothing True $$ sinkList
344+ runConduit $ listObjectsV1 bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
328 print res
345 print res
329−
330```
346```
331
347
332<a name="listIncompleteUploads"></a>
348<a name="listIncompleteUploads"></a>
@@ -339,43 +355,51 @@ __Parameters__
339In the expression `listIncompleteUploads bucketName prefix recursive`
355In the expression `listIncompleteUploads bucketName prefix recursive`
340the parameters are:
356the parameters are:
341
357
342−|Param |Type |Description |
358+| Param | Type | Description |
343−|:---|:---| :---|
359+|:-------------|:----------------------------|:---------------------------------------------------------------------------------------------------------|
344−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
360+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
345−| `prefix` | _Maybe Text_ | Optional prefix that listed objects should have. |
361+| `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 '/'. |
362+| `recursive` | _Bool_ | `True` indicates recursive style listing and `Talse` indicates directory style listing delimited by '/'. |
347
363
348__Return Value__
364__Return Value__
349
365
350|Return type |Description |
366|Return type |Description |
351|:---|:---|
367|:---|:---|
352−| _C.Producer Minio UploadInfo_ | A Conduit Producer of `UploadInfo` values corresponding to each incomplete multipart upload |
368+| _C.ConduitM () UploadInfo Minio ()_ | A Conduit Producer of `UploadInfo` values corresponding to each incomplete multipart upload |
353
369
354__UploadInfo record type__
370__UploadInfo record type__
355
371
356−|Field |Type |Description |
372+| Field | Type | Description |
357−|:---|:---| :---|
373+|:-------------|:---------|:------------------------------------------|
358−|`uiKey` | _Object_ |Name of incompletely uploaded object |
374+| `uiKey` | _Object_ | Name of incompletely uploaded object |
359−|`uiUploadId` | _String_ |Upload ID of incompletely uploaded object |
375+| `uiUploadId` | _String_ | Upload ID of incompletely uploaded object |
360−|`uiSize` | _Int64_ |Size of incompletely uploaded object |
376+| `uiSize` | _Int64_ | Size of incompletely uploaded object |
361
377
362__Example__
378__Example__
363
379
364```haskell
380```haskell
365−{-# Language OverloadedStrings #-}
381+{-# LANGUAGE OverloadedStrings #-}
382+import Network.Minio
383+
384+import Conduit
385+import Prelude
366
386
367−import Data.Conduit (($$))
387+-- | The following example uses MinIO play server at
368−import Conduit.Combinators (sinkList)
388+-- https://play.min.io. The endpoint and associated
389+-- credentials are provided via the libary constant,
390+--
391+-- > minioPlayCI :: ConnectInfo
392+--
369
393
370main :: IO ()
394main :: IO ()
371main = do
395main = do
372 let
396 let
373 bucket = "test"
397 bucket = "test"
374
398
375− -- Performs a recursive listing of all incompletely uploaded objects
399+ -- Performs a recursive listing of incomplete uploads under bucket "test"
376− -- under bucket "test" on play.minio.io.
400+ -- on a local MinIO server.
377− res <- runMinio minioPlayCI $ do
401+ res <- runMinio minioPlayCI $
378− listIncompleteUploads bucket Nothing True $$ sinkList
402+ runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
379 print res
403 print res
380
404
381```
405```
@@ -383,97 +407,126 @@ main = do
383## 3. Object operations
407## 3. Object operations
384
408
385<a name="getObject"></a>
409<a name="getObject"></a>
386−### getObject :: Bucket -> Object -> Minio (C.ResumableSource Minio ByteString)
410+### getObject :: Bucket -> Object -> GetObjectOptions -> Minio (C.ConduitM () ByteString Minio ())
387
411
388−Get an object from the service.
412+Get an object from the S3 service, optionally object ranges can be provided as well.
389
413
390__Parameters__
414__Parameters__
391
415
392−In the expression `getObject bucketName objectName` the parameters
416+In the expression `getObject bucketName objectName opts` the parameters
393are:
417are:
394
418
395−|Param |Type |Description |
419+| Param | Type | Description |
396−|:---|:---| :---|
420+|:-------------|:----------------------------|:----------------------------------------------------------------------------|
397−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
421+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
398−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
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 |
399
434
400__Return Value__
435__Return Value__
401
436
402The return value can be incrementally read to process the contents of
437The return value can be incrementally read to process the contents of
403the object.
438the object.
404−|Return type |Description |
439+| Return type | Description |
405−|:---|:---|
440+|:--------------------------------------------|:-----------------------------------------|
406−| _C.ResumableSource Minio ByteString_ | A Conduit ResumableSource of `ByteString` values. |
441+| _Minio (C.ConduitM () ByteString Minio ())_ | A Conduit source of `ByteString` values. |
407
442
408__Example__
443__Example__
409
444
410```haskell
445```haskell
411−{-# Language OverloadedStrings #-}
446+{-# LANGUAGE OverloadedStrings #-}
447+import Network.Minio
412
448
413−import Network.Minio
414−import Data.Conduit (($$+-))
449+import qualified Data.Conduit as C
415−import Data.Conduit.Binary (sinkLbs)
450+import qualified Data.Conduit.Binary as CB
416−import qualified Data.ByteString.Lazy as LB
451+
452+import Prelude
453+
454+-- | The following example uses MinIO play server at
455+-- https://play.min.io. The endpoint and associated
456+-- credentials are provided via the libary constant,
457+--
458+-- > minioPlayCI :: ConnectInfo
459+--
417
460
418main :: IO ()
461main :: IO ()
419main = do
462main = do
420 let
463 let
421− bucket = "mybucket"
464+ bucket = "my-bucket"
422− object = "myobject"
465+ object = "my-object"
423−
424− -- Lists the parts in an incompletely uploaded object identified by
425− -- bucket, object and upload ID.
426 res <- runMinio minioPlayCI $ do
466 res <- runMinio minioPlayCI $ do
427− source <- getObject bucket object
467+ src <- getObject bucket object def
428− source $$+- sinkLbs
468+ C.connect src $ CB.sinkFileCautious "/tmp/my-object"
429
469
430− -- the following the prints the contents of the object.
431− putStrLn $ either
432− (("Failed to getObject: " ++) . show)
433− (("Read an object of length: " ++) . show . LB.length)
434− res
470+ case res of
471+ Left e -> putStrLn $ "getObject failed." ++ (show e)
472+ Right _ -> putStrLn "getObject succeeded."
435```
473```
436
474
437<a name="putObject"></a>
475<a name="putObject"></a>
438−### putObject :: Bucket -> Object -> C.Producer Minio ByteString -> Maybe Int64 -> Minio ()
476+### putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio () -> Maybe Int64 -> PutObjectOptions -> Minio ()
439Uploads an object to a bucket in the service, from the given input
477Uploads an object to a bucket in the service, from the given input
440−byte stream of optionally supplied length
478+byte stream of optionally supplied length. Optionally you can also specify
479+additional metadata for the object.
441
480
442__Parameters__
481__Parameters__
443
482
444In the expression `putObject bucketName objectName inputSrc` the parameters
483In the expression `putObject bucketName objectName inputSrc` the parameters
445are:
484are:
446
485
447−|Param |Type |Description |
486+| Param | Type | Description |
448−|:---|:---| :---|
487+|:-------------|:------------------------------------|:------------------------------------------------------------------|
449−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
488+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
450−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
489+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
451−| `inputSrc` | _C.Producer Minio ByteString_ | A Conduit Producer of `ByteString` values |
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 |
452
493
453__Example__
494__Example__
454
495
455```haskell
496```haskell
456−{-# Language OverloadedStrings #-}
497+{-# LANGUAGE OverloadedStrings #-}
457−import Network.Minio
498+import Network.Minio
499+
458import qualified Data.Conduit.Combinators as CC
500import qualified Data.Conduit.Combinators as CC
459
501
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+
460main :: IO ()
511main :: IO ()
461main = do
512main = do
462 let
513 let
463− bucket = "mybucket"
514+ bucket = "test"
464− object = "myobject"
515+ object = "obj"
516+ localFile = "/etc/lsb-release"
465− kb15 = 15 * 1024
517+ kb15 = 15 * 1024
466−
467− res <- runMinio minioPlayCI $ do
468− putObject bucket object (CC.repeat "a") (Just kb15)
469
518
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
470 case res of
522 case res of
471− Left e -> putStrLn $ "Failed to putObject " ++ show bucket ++ "/" ++ show object
523+ Left e -> putStrLn $ "putObject failed." ++ show e
472− Right _ -> putStrLn "PutObject was successful"
524+ Right () -> putStrLn "putObject succeeded."
525+
473```
526```
474
527
475<a name="fGetObject"></a>
528<a name="fGetObject"></a>
476−### fGetObject :: Bucket -> Object -> FilePath -> Minio ()
529+### fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
477Downloads an object from a bucket in the service, to the given file
530Downloads an object from a bucket in the service, to the given file
478
531
479__Parameters__
532__Parameters__
@@ -481,11 +534,23 @@ __Parameters__
481In the expression `fGetObject bucketName objectName inputFile` the parameters
534In the expression `fGetObject bucketName objectName inputFile` the parameters
482are:
535are:
483
536
484−|Param |Type |Description |
537+| Param | Type | Description |
485−|:---|:---| :---|
538+|:-------------|:----------------------------|:----------------------------------------------------------------------------|
486−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
539+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
487−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
540+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
488−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
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 |
489
554
490``` haskell
555``` haskell
491
556
@@ -496,8 +561,8 @@ import Data.Conduit (($$+-))
496import Data.Conduit.Binary (sinkLbs)
561import Data.Conduit.Binary (sinkLbs)
497import Prelude
562import Prelude
498
563
499−-- | The following example uses minio's play server at
564+-- | The following example uses MinIO play server at
500−-- https://play.minio.io:9000. The endpoint and associated
565+-- https://play.min.io. The endpoint and associated
501-- credentials are provided via the libary constant,
566-- credentials are provided via the libary constant,
502--
567--
503-- > minioPlayCI :: ConnectInfo
568-- > minioPlayCI :: ConnectInfo
@@ -511,7 +576,7 @@ main = do
511 localFile = "/etc/lsb-release"
576 localFile = "/etc/lsb-release"
512
577
513 res <- runMinio minioPlayCI $ do
578 res <- runMinio minioPlayCI $ do
514− src <- fGetObject bucket object localFile
579+ src <- fGetObject bucket object localFile def
515 (src $$+- sinkLbs)
580 (src $$+- sinkLbs)
516
581
517 case res of
582 case res of
@@ -528,11 +593,11 @@ __Parameters__
528In the expression `fPutObject bucketName objectName inputFile` the parameters
593In the expression `fPutObject bucketName objectName inputFile` the parameters
529are:
594are:
530
595
531−|Param |Type |Description |
596+| Param | Type | Description |
532−|:---|:---| :---|
597+|:-------------|:----------------------------|:--------------------------------|
533−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
598+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
534−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
599+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
535−| `inputFile` | _FilePath_ | Path to the file to be uploaded |
600+| `inputFile` | _FilePath_ | Path to the file to be uploaded |
536
601
537__Example__
602__Example__
538
603
@@ -557,31 +622,38 @@ main = do
557```
622```
558
623
559<a name="copyObject"></a>
624<a name="copyObject"></a>
560−### copyObject :: Bucket -> Object -> CopyPartSource -> Minio ()
625+### copyObject :: DestinationInfo -> SourceInfo -> Minio ()
561Copies content of an object from the service to another
626Copies content of an object from the service to another
562
627
563__Parameters__
628__Parameters__
564
629
565−In the expression `copyObject bucketName objectName cps` the parameters
630+In the expression `copyObject dstInfo srcInfo` the parameters
566are:
631are:
567
632
568−|Param |Type |Description |
633+| Param | Type | Description |
569−|:---|:---| :---|
634+|:----------|:------------------|:----------------------------------------------------------|
570−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
635+| `dstInfo` | _DestinationInfo_ | A value representing properties of the destination object |
571−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
572−| `cps` | _CopyPartSource_ | A value representing properties of the source object |
636+| `srcInfo` | _SourceInfo_ | A value representing properties of the source object |
637+
573
638
639+__SourceInfo record type__
574
640
575−__CopyPartSource record type__
641+| Field | Type | Description |
642+|:-----------------------|:-----------------------|:----------------------------------------------------------------------------------------------------------|
643+| `srcBucket` | `Bucket` | Name of source bucket |
644+| `srcObject` | `Object` | Name of source object |
645+| `srcRange` | `Maybe (Int64, Int64)` | (Optional) Represents the byte range of source object. (0, 9) represents first ten bytes of source object |
646+| `srcIfMatch` | `Maybe Text` | (Optional) ETag source object should match |
647+| `srcIfNoneMatch` | `Maybe Text` | (Optional) ETag source object shouldn't match |
648+| `srcIfUnmodifiedSince` | `Maybe UTCTime` | (Optional) Time since source object wasn't modified |
649+| `srcIfModifiedSince` | `Maybe UTCTime` | (Optional) Time since source object was modified |
576
650
651+__Destination record type__
652+
577−|Field |Type |Description |
653+| Field | Type | Description |
578−|:---|:---| :---|
654+|:------------|:---------|:-----------------------------------------------------|
579−| `cpSource` | `Text`| Name of source object formatted as "/srcBucket/srcObject" |
655+| `dstBucket` | `Bucket` | Name of destination bucket in server-side copyObject |
580−| `cpSourceRange` | `Maybe (Int64, Int64)` | Represents the byte range of source object. (0, 9) represents first ten bytes of source object|
656+| `dstObject` | `Object` | Name of destination object in server-side copyObject |
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 |
585
657
586__Example__
658__Example__
587
659
@@ -594,13 +666,13 @@ main = do
594 let
666 let
595 bucket = "mybucket"
667 bucket = "mybucket"
596 object = "myobject"
668 object = "myobject"
597− srcObject = "/mybucket/srcObject"
669+ objectCopy = "obj-copy"
598
670
599 res <- runMinio minioPlayCI $ do
671 res <- runMinio minioPlayCI $ do
600− copyObject bucket object def { cpSource = srcObject }
672+ copyObject def { dstBucket = bucket, dstObject = objectCopy } def { srcBucket = bucket, srcObject = object }
601
673
602 case res of
674 case res of
603− Left e -> putStrLn $ "Failed to copyObject " ++ show srcObject"
675+ Left e -> putStrLn $ "Failed to copyObject " ++ show bucket ++ show "/" ++ show object
604 Right _ -> putStrLn "copyObject was successful"
676 Right _ -> putStrLn "copyObject was successful"
605```
677```
606
678
@@ -613,10 +685,10 @@ __Parameters__
613In the expression `removeObject bucketName objectName` the parameters
685In the expression `removeObject bucketName objectName` the parameters
614are:
686are:
615
687
616−|Param |Type |Description |
688+| Param | Type | Description |
617−|:---|:---| :---|
689+|:-------------|:----------------------------|:-------------------|
618−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
690+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
619−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
691+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
620
692
621__Example__
693__Example__
622
694
@@ -647,10 +719,10 @@ __Parameters__
647In the expression `removeIncompleteUpload bucketName objectName` the parameters
719In the expression `removeIncompleteUpload bucketName objectName` the parameters
648are:
720are:
649
721
650−|Param |Type |Description |
722+| Param | Type | Description |
651−|:---|:---| :---|
723+|:-------------|:----------------------------|:-------------------|
652−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
724+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
653−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
725+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
654
726
655__Example__
727__Example__
656
728
@@ -672,6 +744,59 @@ main = do
672 Right _ -> putStrLn "Removed incomplete upload successfully"
744 Right _ -> putStrLn "Removed incomplete upload successfully"
673```
745```
674
746
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+
675<a name="BucketExists"></a>
800<a name="BucketExists"></a>
676### bucketExists :: Bucket -> Minio Bool
801### bucketExists :: Bucket -> Minio Bool
677Checks if a bucket exists.
802Checks if a bucket exists.
@@ -680,9 +805,9 @@ __Parameters__
680
805
681In the expression `bucketExists bucketName` the parameters are:
806In the expression `bucketExists bucketName` the parameters are:
682
807
683−|Param |Type |Description |
808+| Param | Type | Description |
684−|:---|:---| :---|
809+|:-------------|:----------------------------|:-------------------|
685−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
810+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
686
811
687
812
688## 4. Presigned operations
813## 4. Presigned operations
@@ -704,22 +829,22 @@ __Parameters__
704In the expression `presignedGetObjectUrl bucketName objectName expiry queryParams headers`
829In the expression `presignedGetObjectUrl bucketName objectName expiry queryParams headers`
705the parameters are:
830the parameters are:
706
831
707−|Param |Type |Description |
832+| Param | Type | Description |
708−|:---|:---| :---|
833+|:--------------|:---------------------------------------------------------------|:------------------------------------------------|
709−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
834+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
710−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
835+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
711−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
836+| `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 |
837+| `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 |
838+| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
714
839
715__Return Value__
840__Return Value__
716
841
717Returns the generated URL - it will include authentication
842Returns the generated URL - it will include authentication
718information.
843information.
719
844
720−|Return type |Description |
845+| Return type | Description |
721−|:---|:---|
846+|:-------------|:------------------------|
722−| _ByteString_ | Generated presigned URL |
847+| _ByteString_ | Generated presigned URL |
723
848
724__Example__
849__Example__
725
850
@@ -762,21 +887,21 @@ __Parameters__
762In the expression `presignedPutObjectUrl bucketName objectName expiry headers`
887In the expression `presignedPutObjectUrl bucketName objectName expiry headers`
763the parameters are:
888the parameters are:
764
889
765−|Param |Type |Description |
890+| Param | Type | Description |
766−|:---|:---| :---|
891+|:-------------|:---------------------------------------------------------------|:------------------------------------------------|
767−| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
892+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
768−| `objectName` | _Object_ (alias for `Text`) | Name of the object |
893+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
769−| `expiry` | _UrlExpiry_ (alias for `Int`) | Url expiry time in seconds |
894+| `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 |
895+| `headers` | _RequestHeaders_ (from package `http-types:Network.HTTP.Types` | Request headers that would be used with the URL |
771
896
772__Return Value__
897__Return Value__
773
898
774Returns the generated URL - it will include authentication
899Returns the generated URL - it will include authentication
775information.
900information.
776
901
777−|Return type |Description |
902+| Return type | Description |
778−|:---|:---|
903+|:-------------|:------------------------|
779−| _ByteString_ | Generated presigned URL |
904+| _ByteString_ | Generated presigned URL |
780
905
781__Example__
906__Example__
782
907
@@ -804,7 +929,7 @@ main = do
804```
929```
805
930
806<a name="presignedPostPolicy"></a>
931<a name="presignedPostPolicy"></a>
807−### presignedPostPolicy :: PostPolicy -> Minio (ByteString, Map.Map Text ByteString)
932+### presignedPostPolicy :: PostPolicy -> Minio (ByteString, HashMap Text ByteString)
808
933
809Generate a presigned URL and POST policy to upload files via a POST
934Generate a presigned URL and POST policy to upload files via a POST
810request. This is intended for browser uploads and generates form data
935request. This is intended for browser uploads and generates form data
@@ -816,10 +941,10 @@ The `PostPolicy` argument is created using the `newPostPolicy` function:
816
941
817In the expression `newPostPolicy expirationTime conditions` the parameters are:
942In the expression `newPostPolicy expirationTime conditions` the parameters are:
818
943
819−|Param | Type| Description |
944+| Param | Type | Description |
820−|:---|:---|:---|
945+|:-----------------|:--------------------------------------------------|:---------------------------------------------|
821−| `expirationTime` | _UTCTime_ (from package `time:Data.Time.UTCTime`) | The expiration time for the policy |
946+| `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 |
947+| `conditions` | _[PostPolicyConditions]_ | List of conditions to be added to the policy |
823
948
824The policy conditions are created using various helper functions -
949The policy conditions are created using various helper functions -
825please refer to the Haddocks for details.
950please refer to the Haddocks for details.
@@ -841,7 +966,7 @@ import Network.Minio
841
966
842import qualified Data.ByteString as B
967import qualified Data.ByteString as B
843import qualified Data.ByteString.Char8 as Char8
968import qualified Data.ByteString.Char8 as Char8
844−import qualified Data.Map.Strict as Map
969+import qualified Data.HashMap.Strict as H
845import qualified Data.Text.Encoding as Enc
970import qualified Data.Text.Encoding as Enc
846import qualified Data.Time as Time
971import qualified Data.Time as Time
847
972
@@ -881,7 +1006,7 @@ main = do
881 let
1006 let
882 formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
1007 formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
883 "'", v, "'"]
1008 "'", v, "'"]
884− formOptions = B.intercalate " " $ map formFn $ Map.toList formData
1009+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
885
1010
886
1011
887 return $ B.intercalate " " $
1012 return $ B.intercalate " " $
examples/AssumeRole.hsnew file+47
@@ -0,0 +1,47 @@
1+--
2+-- MinIO Haskell SDK, (C) 2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE OverloadedStrings #-}
17+
18+import Control.Monad.IO.Class (liftIO)
19+import Network.Minio
20+import Prelude
21+
22+main :: IO ()
23+main = do
24+ -- Use play credentials for example.
25+ let assumeRole =
26+ STSAssumeRole
27+ ( CredentialValue
28+ "Q3AM3UQ867SPQQA43P2F"
29+ "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
30+ Nothing
31+ )
32+ $ defaultSTSAssumeRoleOptions
33+ { saroLocation = Just "us-east-1",
34+ saroEndpoint = Just "https://play.min.io:9000"
35+ }
36+
37+ -- Retrieve temporary credentials and print them.
38+ cv <- requestSTSCredential assumeRole
39+ print $ "Temporary credentials" ++ show (credentialValueText $ fst cv)
40+ print $ "Expiry" ++ show (snd cv)
41+
42+ -- Configure 'ConnectInfo' to request temporary credentials on demand.
43+ ci <- setSTSCredential assumeRole "https://play.min.io"
44+ res <- runMinio ci $ do
45+ buckets <- listBuckets
46+ liftIO $ print $ "Top 5 buckets: " ++ show (take 5 buckets)
47+ print res
examples/BucketExists.hs+5−8
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,20 +16,17 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−{-# Language OverloadedStrings #-}
19+{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23import Control.Monad.IO.Class (liftIO)
21import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
24import Prelude
23import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35 let bucket = "missingbucket"
32 let bucket = "missingbucket"
examples/CopyObject.hs+17−21
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,44 +16,40 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.Catch (catchIf)
21+import Network.Minio
24−import qualified Data.Text as T
22+import UnliftIO.Exception (catch, throwIO)
25−import Prelude
26
23
27-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
28−-- https://play.minio.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
29-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
30--
27--
31-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
32−--
33−
34−ignoreMinioErr :: ServiceErr -> Minio ()
35−ignoreMinioErr = return . const ()
36−
37main :: IO ()
29main :: IO ()
38main = do
30main = do
39− let
40− bucket = "test"
31+ let bucket = "test"
41 object = "obj"
32 object = "obj"
42 objectCopy = "obj-copy"
33 objectCopy = "obj-copy"
43 localFile = "/etc/lsb-release"
34 localFile = "/etc/lsb-release"
44
35
45 res1 <- runMinio minioPlayCI $ do
36 res1 <- runMinio minioPlayCI $ do
46 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
37 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
38+ catch
47− catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
39+ (makeBucket bucket Nothing)
40+ ( \e -> case e of
41+ BucketAlreadyOwnedByYou -> return ()
42+ _ -> throwIO e
43+ )
48
44
49 -- 2. Upload a file to bucket/object.
45 -- 2. Upload a file to bucket/object.
50− fPutObject bucket object localFile
46+ fPutObject bucket object localFile defaultPutObjectOptions
51
47
52 -- 3. Copy bucket/object to bucket/objectCopy.
48 -- 3. Copy bucket/object to bucket/objectCopy.
53− copyObject bucket objectCopy def {
49+ copyObject
54− cpSource = T.concat ["/", bucket, "/", object]
50+ defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
55− }
51+ defaultSourceInfo {srcBucket = bucket, srcObject = object}
56
52
57 case res1 of
53 case res1 of
58− Left e -> putStrLn $ "copyObject failed." ++ (show e)
54+ Left e -> putStrLn $ "copyObject failed." ++ show e
59 Right () -> putStrLn "copyObject succeeded."
55 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+31−31
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs --package optparse-applicative --package filepath
2+-- stack --resolver lts-14.11 runghc --package minio-hs --package optparse-applicative --package filepath
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,43 +16,39 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−
21−{-# LANGUAGE OverloadedStrings #-}
19+{-# LANGUAGE OverloadedStrings #-}
22{-# LANGUAGE ScopedTypeVariables #-}
20{-# LANGUAGE ScopedTypeVariables #-}
23−import Network.Minio
24
21
25−import Control.Monad.Catch (catchIf)
26−import Control.Monad.IO.Class (liftIO)
27−import Data.Monoid ((<>))
28−import Data.Text (pack)
22+import Data.Text (pack)
23+import Network.Minio
29−import Options.Applicative
24+import Options.Applicative
30−import Prelude
31−import System.FilePath.Posix
25+import System.FilePath.Posix
26+import UnliftIO (throwIO, try)
27+import Prelude
32
28
33-- | The following example uses minio's play server at
29-- | The following example uses minio's play server at
34−-- https://play.minio.io:9000. The endpoint and associated
30+-- https://play.min.io. The endpoint and associated
35-- credentials are provided via the libary constant,
31-- credentials are provided via the libary constant,
36--
32--
37-- > minioPlayCI :: ConnectInfo
33-- > minioPlayCI :: ConnectInfo
38−--
39
34
40-- optparse-applicative package based command-line parsing.
35-- optparse-applicative package based command-line parsing.
41fileNameArgs :: Parser FilePath
36fileNameArgs :: Parser FilePath
42−fileNameArgs = strArgument
37+fileNameArgs =
38+ strArgument
43− (metavar "FILENAME"
39+ ( metavar "FILENAME"
44− <> help "Name of file to upload to AWS S3 or a Minio server")
40+ <> help "Name of file to upload to AWS S3 or a MinIO server"
45−
41+ )
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 ()
55
42
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+ )
56
52
57main :: IO ()
53main :: IO ()
58main = do
54main = do
@@ -64,11 +60,15 @@ main = do
64
60
65 res <- runMinio minioPlayCI $ do
61 res <- runMinio minioPlayCI $ do
66 -- Make a bucket; catch bucket already exists exception if thrown.
62 -- Make a bucket; catch bucket already exists exception if thrown.
67− catchIf (== BucketAlreadyOwnedByYou) (makeBucket bucket Nothing) ignoreMinioErr
63+ bErr <- try $ makeBucket bucket Nothing
64+ case bErr of
65+ Left BucketAlreadyOwnedByYou -> return ()
66+ Left e -> throwIO e
67+ Right _ -> return ()
68
68
69 -- Upload filepath to bucket; object is derived from filepath.
69 -- Upload filepath to bucket; object is derived from filepath.
70− fPutObject bucket object filepath
70+ fPutObject bucket object filepath defaultPutObjectOptions
71
71
72 case res of
72 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
74 Right () -> putStrLn "file upload succeeded."
74 Right () -> putStrLn "file upload succeeded."
Setup.hs → examples/GetConfig.hsrenamed+15−3
@@ -1,5 +1,8 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
1--
4--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
3--
6--
4-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -14,5 +17,14 @@
14-- limitations under the License.
17-- limitations under the License.
15--
18--
16
19
17−import Distribution.Simple
20+import Network.Minio
18−main = defaultMain
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ getConfig
30+ print res
examples/GetObject.hs+11−15
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,30 +16,26 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Data.Conduit (($$+-))
21+import qualified Data.Conduit as C
24−import Data.Conduit.Binary (sinkLbs)
22+import qualified Data.Conduit.Binary as CB
23+import Network.Minio
25−import Prelude
24+import Prelude
26
25
27-- | The following example uses minio's play server at
26-- | The following example uses minio's play server at
28−-- https://play.minio.io:9000. The endpoint and associated
27+-- https://play.min.io. The endpoint and associated
29-- credentials are provided via the libary constant,
28-- credentials are provided via the libary constant,
30--
29--
31-- > minioPlayCI :: ConnectInfo
30-- > minioPlayCI :: ConnectInfo
32−--
33−
34main :: IO ()
31main :: IO ()
35main = do
32main = do
36− let
37− bucket = "my-bucket"
33+ let bucket = "my-bucket"
38 object = "my-object"
34 object = "my-object"
39 res <- runMinio minioPlayCI $ do
35 res <- runMinio minioPlayCI $ do
40− src <- getObject bucket object
36+ src <- getObject bucket object defaultGetObjectOptions
41− (src $$+- sinkLbs)
37+ C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
42
38
43 case res of
39 case res of
44− Left e -> putStrLn $ "getObject failed." ++ (show e)
40+ Left e -> putStrLn $ "getObject failed." ++ show e
45 Right _ -> putStrLn "getObject succeeded."
41 Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs+11−14
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,28 +16,25 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.S3API
23
20
21+import Network.Minio
22+import Network.Minio.S3API
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
37 object = "passwd"
33 object = "passwd"
34+ res <-
38− res <- runMinio minioPlayCI $
35+ runMinio minioPlayCI $
39− headObject bucket object
36+ headObject bucket object []
40
37
41 case res of
38 case res of
42− Left e -> putStrLn $ "headObject failed." ++ show e
39+ Left e -> putStrLn $ "headObject failed." ++ show e
43 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
40 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
examples/Heal.hsnew file+37
@@ -0,0 +1,37 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <- runMinio minioPlayCI $
27+ do
28+ hsr <-
29+ startHeal
30+ Nothing
31+ Nothing
32+ HealOpts
33+ { hoRecursive = True,
34+ hoDryRun = False
35+ }
36+ getHealStatus Nothing Nothing (hsrClientToken hsr)
37+ print res
examples/ListBuckets.hs+6−8
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,19 +16,17 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.IO.Class (liftIO)
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
24−import Prelude
23+import Prelude
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32
30
33-- This example list buckets that belongs to the user and returns
31-- This example list buckets that belongs to the user and returns
34-- region of the first bucket returned.
32-- region of the first bucket returned.
examples/ListIncompleteUploads.hs+20−23
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,39 +16,36 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Data.Conduit (($$))
21+import Conduit
24−import Data.Conduit.Combinators (sinkList)
22+import Network.Minio
25−import Prelude
23+import Prelude
26
24
27-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
28−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
29-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
30--
28--
31-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
32−--
33−
34main :: IO ()
30main :: IO ()
35main = do
31main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
38
33
39 -- Performs a recursive listing of incomplete uploads under bucket "test"
34 -- Performs a recursive listing of incomplete uploads under bucket "test"
40 -- on a local minio server.
35 -- on a local minio server.
36+ res <-
41− res <- runMinio minioPlayCI $
37+ runMinio minioPlayCI $
38+ runConduit $
42− listIncompleteUploads bucket Nothing True $$ sinkList
39+ listIncompleteUploads bucket Nothing True .| mapM_C (liftIO . print)
43 print res
40 print res
44
41
45− {-
42+{-
46− Following is the output of the above program on a local Minio server.
43+ Following is the output of the above program on a local MinIO server.
47
44
48− Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
45+ Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
49− , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
46+ , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
50− , uiInitTime = 2017-03-01 10:16:25.698 UTC
47+ , uiInitTime = 2017-03-01 10:16:25.698 UTC
51− , uiSize = 17731794
48+ , uiSize = 17731794
52− }
49+ }
53− ]
50+ ]
54− -}
51+-}
examples/ListObjects.hs+16−20
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,35 +16,31 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−
23−import qualified Data.Conduit as C
24−import qualified Data.Conduit.Combinators as CC
25−import Prelude
26
20
21+import Conduit
22+import Network.Minio
23+import Prelude
27
24
28-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
29−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
30-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
31--
28--
32-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
33−--
34−
35main :: IO ()
30main :: IO ()
36main = do
31main = do
37− let
38− bucket = "test"
32+ let bucket = "test"
39
33
40 -- Performs a recursive listing of all objects under bucket "test"
34 -- Performs a recursive listing of all objects under bucket "test"
41− -- on play.minio.io.
35+ -- on play.min.io.
36+ res <-
42− res <- runMinio minioPlayCI $
37+ runMinio minioPlayCI $
38+ runConduit $
43− listObjects bucket Nothing True C.$$ CC.sinkList
39+ listObjects bucket Nothing True .| mapM_C (liftIO . print)
44 print res
40 print res
45
41
46− {-
42+{-
47− Following is the output of the above program on a local Minio server.
43+ Following is the output of the above program on a local MinIO server.
48
44
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}]
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}]
50− -}
46+-}
examples/Makebucket.hs → examples/MakeBucket.hsrenamed+10−14
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,25 +16,21 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−
21{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
23
20
21+import Network.Minio
24−import Prelude
22+import Prelude
25
23
26-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
29--
27--
30-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
29main :: IO ()
34main = do
30main = do
35− let
36− bucket = "my-bucket"
31+ let bucket = "my-bucket"
32+ res <-
37− res <- runMinio minioPlayCI $
33+ runMinio minioPlayCI $
38− -- N B the region provided for makeBucket is optional.
34+ -- N B the region provided for makeBucket is optional.
39− makeBucket bucket (Just "us-east-1")
35+ makeBucket bucket (Just "us-east-1")
40 print res
36 print res
examples/PresignedGetObject.hs+39−34
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,42 +16,41 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import Control.Monad.IO.Class (liftIO)
21+import Control.Monad.IO.Class (liftIO)
24−import qualified Data.ByteString.Char8 as B
22+import qualified Data.ByteString.Char8 as B
25−import Data.CaseInsensitive (original)
23+import Data.CaseInsensitive (original)
26import qualified Data.Conduit.Combinators as CC
24import qualified Data.Conduit.Combinators as CC
27−import qualified Data.Text.Encoding as E
25+import qualified Data.Text.Encoding as E
26+import Network.Minio
28
27
29-- | The following example uses minio's play server at
28-- | The following example uses minio's play server at
30−-- https://play.minio.io:9000. The endpoint and associated
29+-- https://play.min.io. The endpoint and associated
31-- credentials are provided via the libary constant,
30-- credentials are provided via the libary constant,
32--
31--
33-- > minioPlayCI :: ConnectInfo
32-- > minioPlayCI :: ConnectInfo
34−--
35−
36main :: IO ()
33main :: IO ()
37main = do
34main = do
38− let
39− bucket = "my-bucket"
35+ let bucket = "my-bucket"
40− object = "my-object"
36+ object = "my-object"
41− kb15 = 15*1024
37+ kb15 = 15 * 1024
42−
43− -- Set query parameter to modify content disposition response
38+ -- Set query parameter to modify content disposition response
44− -- header
39+ -- header
40+ queryParam =
45− queryParam = [("response-content-disposition",
41+ [ ( "response-content-disposition",
46− Just "attachment; filename=\"your-filename.txt\"")]
42+ Just "attachment; filename=\"your-filename.txt\""
43+ )
44+ ]
47
45
48 res <- runMinio minioPlayCI $ do
46 res <- runMinio minioPlayCI $ do
49 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
47 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
50− putObject bucket object (CC.repeat "a") (Just kb15)
48+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
51− liftIO $ putStrLn $ "Done. Object created at: my-bucket/my-object"
49+ liftIO $ putStrLn "Done. Object created at: my-bucket/my-object"
52
50
53 -- Extract Etag of uploaded object
51 -- Extract Etag of uploaded object
54− (ObjectInfo _ _ etag _) <- statObject bucket object
52+ oi <- statObject bucket object defaultGetObjectOptions
53+ let etag = oiETag oi
55
54
56 -- Set header to add an if-match constraint - this makes sure
55 -- Set header to add an if-match constraint - this makes sure
57 -- the fetching fails if the object is changed on the server
56 -- the fetching fails if the object is changed on the server
@@ -60,23 +59,29 @@ main = do
60 -- Generate a URL with 7 days expiry time - note that the headers
59 -- Generate a URL with 7 days expiry time - note that the headers
61 -- used above must be added to the request with the signed URL
60 -- used above must be added to the request with the signed URL
62 -- generated.
61 -- generated.
62+ url <-
63− url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)
63+ presignedGetObjectUrl
64+ "my-bucket"
65+ "my-object"
66+ (7 * 24 * 3600)
64− queryParam headers
67+ queryParam
68+ headers
65
69
66 return (headers, etag, url)
70 return (headers, etag, url)
67
71
68 case res of
72 case res of
69 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
73 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
70− Right (headers, etag, url) -> do
74+ Right (headers, _, url) -> do
71−
72 -- We generate a curl command to demonstrate usage of the signed
75 -- We generate a curl command to demonstrate usage of the signed
73 -- URL.
76 -- URL.
74− let
75− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
77+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
78+ curlCmd =
76− curlCmd = B.intercalate " " $
79+ B.intercalate " " $
80+ ["curl --fail"]
77− ["curl --fail"] ++ map hdrOpt headers ++
81+ ++ map hdrOpt headers
78− ["-o /tmp/myfile", B.concat ["'", url, "'"]]
82+ ++ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
79
83
84+ putStrLn $
80− putStrLn $ "The following curl command would use the presigned " ++
85+ "The following curl command would use the presigned "
81− "URL to fetch the object and write it to \"/tmp/myfile\":"
86+ ++ "URL to fetch the object and write it to \"/tmp/myfile\":"
82 B.putStrLn curlCmd
87 B.putStrLn curlCmd
examples/PresignedPostPolicy.hs+45−42
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,69 +16,72 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23−import qualified Data.ByteString as B
21+import qualified Data.ByteString as B
24import qualified Data.ByteString.Char8 as Char8
22import qualified Data.ByteString.Char8 as Char8
25−import qualified Data.Map.Strict as Map
23+import qualified Data.HashMap.Strict as H
26−import qualified Data.Text.Encoding as Enc
24+import qualified Data.Text.Encoding as Enc
27−import qualified Data.Time as Time
25+import qualified Data.Time as Time
26+import Network.Minio
28
27
29-- | The following example uses minio's play server at
28-- | The following example uses minio's play server at
30−-- https://play.minio.io:9000. The endpoint and associated
29+-- https://play.min.io. The endpoint and associated
31-- credentials are provided via the libary constant,
30-- credentials are provided via the libary constant,
32--
31--
33-- > minioPlayCI :: ConnectInfo
32-- > minioPlayCI :: ConnectInfo
34−--
35−
36main :: IO ()
33main :: IO ()
37main = do
34main = do
38 now <- Time.getCurrentTime
35 now <- Time.getCurrentTime
39− let
40− bucket = "my-bucket"
36+ let bucket = "my-bucket"
41− object = "my-object"
37+ object = "photos/my-object"
42−
43− -- set an expiration time of 10 days
38+ -- set an expiration time of 10 days
44− expireTime = Time.addUTCTime (3600 * 24 * 10) now
39+ expireTime = Time.addUTCTime (3600 * 24 * 10) now
45−
46− -- create a policy with expiration time and conditions - since the
40+ -- create a policy with expiration time and conditions - since the
47− -- conditions are validated, newPostPolicy returns an Either value
41+ -- conditions are validated, newPostPolicy returns an Either value
42+ policyE =
48− policyE = newPostPolicy expireTime
43+ newPostPolicy
44+ expireTime
49− [ -- set the object name condition
45+ [ -- set the object name condition
50− ppCondKey "photos/my-object"
46+ ppCondKey object,
51− -- set the bucket name condition
47+ -- set the bucket name condition
52− , ppCondBucket "my-bucket"
48+ ppCondBucket bucket,
53− -- set the size range of object as 1B to 10MiB
49+ -- set the size range of object as 1B to 10MiB
54− , ppCondContentLengthRange 1 (10*1024*1024)
50+ ppCondContentLengthRange 1 (10 * 1024 * 1024),
55− -- set content type as jpg image
51+ -- set content type as jpg image
56− , ppCondContentType "image/jpeg"
52+ ppCondContentType "image/jpeg",
57− -- on success set the server response code to 200
53+ -- on success set the server response code to 200
58− , ppCondSuccessActionStatus 200
54+ ppCondSuccessActionStatus 200
59− ]
55+ ]
60
56
61 case policyE of
57 case policyE of
62− Left err -> putStrLn $ show err
58+ Left err -> print err
63 Right policy -> do
59 Right policy -> do
64 res <- runMinio minioPlayCI $ do
60 res <- runMinio minioPlayCI $ do
65 (url, formData) <- presignedPostPolicy policy
61 (url, formData) <- presignedPostPolicy policy
66
62
67 -- a curl command is output to demonstrate using the generated
63 -- a curl command is output to demonstrate using the generated
68 -- URL and form-data
64 -- URL and form-data
69− let
65+ let formFn (k, v) =
66+ B.concat
67+ [ "-F ",
70− formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
68+ Enc.encodeUtf8 k,
69+ "=",
70+ "'",
71− "'", v, "'"]
71+ v,
72+ "'"
73+ ]
72− formOptions = B.intercalate " " $ map formFn $ Map.toList formData
74+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
73−
74
75
76+ return $
75− return $ B.intercalate " " $
77+ B.intercalate
78+ " "
76− ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
79+ ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
77
80
78 case res of
81 case res of
79− Left e -> putStrLn $ "post-policy error: " ++ (show e)
82+ Left e -> putStrLn $ "post-policy error: " ++ show e
80 Right cmd -> do
83 Right cmd -> do
81− putStrLn $ "Put a photo at /tmp/photo.jpg and run command:\n"
84+ putStrLn "Put a photo at /tmp/photo.jpg and run command:\n"
82
85
83 -- print the generated curl command
86 -- print the generated curl command
84 Char8.putStrLn cmd
87 Char8.putStrLn cmd
examples/PresignedPutObject.hs+23−24
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,44 +16,43 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23import qualified Data.ByteString.Char8 as B
21import qualified Data.ByteString.Char8 as B
24−import Data.CaseInsensitive (original)
22+import Data.CaseInsensitive (original)
23+import Network.Minio
25
24
26-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
29--
28--
30-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
30main :: IO ()
34main = do
31main = do
35− let
36− -- Use headers to set user-metadata - note that this header will
32+ let -- Use headers to set user-metadata - note that this header will
37− -- need to be set when the URL is used to make an upload.
33+ -- need to be set when the URL is used to make an upload.
34+ headers =
38− headers = [("x-amz-meta-url-creator",
35+ [ ( "x-amz-meta-url-creator",
39− "minio-hs-presigned-put-example")]
36+ "minio-hs-presigned-put-example"
37+ )
38+ ]
40 res <- runMinio minioPlayCI $ do
39 res <- runMinio minioPlayCI $ do
41−
42 -- generate a URL with 7 days expiry time
40 -- generate a URL with 7 days expiry time
43− presignedPutObjectUrl "my-bucket" "my-object" (7*24*3600) headers
41+ presignedPutObjectUrl "my-bucket" "my-object" (7 * 24 * 3600) headers
44
42
45 case res of
43 case res of
46 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
44 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
47 Right url -> do
45 Right url -> do
48−
49 -- We generate a curl command to demonstrate usage of the signed
46 -- We generate a curl command to demonstrate usage of the signed
50 -- URL.
47 -- URL.
51− let
52− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
48+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
49+ curlCmd =
53− curlCmd = B.intercalate " " $
50+ B.intercalate " " $
51+ ["curl "]
54− ["curl "] ++ map hdrOpt headers ++
52+ ++ map hdrOpt headers
55− ["-T /tmp/myfile", B.concat ["'", url, "'"]]
53+ ++ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
56−
54+
55+ putStrLn $
57− putStrLn $ "The following curl command would use the presigned " ++
56+ "The following curl command would use the presigned "
58− "URL to upload the file at \"/tmp/myfile\":"
57+ ++ "URL to upload the file at \"/tmp/myfile\":"
59 B.putStrLn curlCmd
58 B.putStrLn curlCmd
examples/PutObject.hs+16−18
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,38 +16,36 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
23import qualified Data.Conduit.Combinators as CC
21import qualified Data.Conduit.Combinators as CC
22+import Network.Minio
23+import Prelude
24
24
25-- | The following example uses minio's play server at
25-- | The following example uses minio's play server at
26−-- https://play.minio.io:9000. The endpoint and associated
26+-- https://play.min.io. The endpoint and associated
27-- credentials are provided via the libary constant,
27-- credentials are provided via the libary constant,
28--
28--
29-- > minioPlayCI :: ConnectInfo
29-- > minioPlayCI :: ConnectInfo
30−--
31−
32main :: IO ()
30main :: IO ()
33main = do
31main = do
34− let
35− bucket = "test"
32+ let bucket = "test"
36 object = "obj"
33 object = "obj"
37 localFile = "/etc/lsb-release"
34 localFile = "/etc/lsb-release"
38 kb15 = 15 * 1024
35 kb15 = 15 * 1024
39
36
40− -- Eg 1. Upload a stream of repeating "a" using putObject.
37+ -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
38+ res1 <-
41− res1 <- runMinio minioPlayCI $
39+ runMinio minioPlayCI $
42− putObject bucket object (CC.repeat "a") (Just kb15)
40+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
43 case res1 of
41 case res1 of
44− Left e -> putStrLn $ "putObject failed." ++ show e
42+ Left e -> putStrLn $ "putObject failed." ++ show e
45 Right () -> putStrLn "putObject succeeded."
43 Right () -> putStrLn "putObject succeeded."
46
44
47−
48− -- Eg 2. Upload a file using fPutObject.
45+ -- Eg 2. Upload a file using fPutObject with default options.
46+ res2 <-
49− res2 <- runMinio minioPlayCI $
47+ runMinio minioPlayCI $
50− fPutObject bucket object localFile
48+ fPutObject bucket object localFile defaultPutObjectOptions
51 case res2 of
49 case res2 of
52− Left e -> putStrLn $ "fPutObject failed." ++ show e
50+ Left e -> putStrLn $ "fPutObject failed." ++ show e
53 Right () -> putStrLn "fPutObject succeeded."
51 Right () -> putStrLn "fPutObject succeeded."
examples/README.mdnew file+17
@@ -0,0 +1,17 @@
1+# Examples
2+
3+The examples in this directory illustrate usage of various APIs provided by this library. Each file is self-contained and can be run like a script directly.
4+
5+To build the examples, the build flag `examples` needs to be turned on:
6+
7+```sh
8+stack build --flag minio-hs:examples
9+```
10+
11+Now to run and example script [BucketExists.hs](https://github.com/minio/minio-hs/blob/master/examples/BucketExists.hs):
12+
13+```sh
14+stack exec BucketExists
15+```
16+
17+The CI system is configured to build these examples with every change, so they should be current.
examples/Removebucket.hs → examples/RemoveBucket.hsrenamed+6−11
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,23 +16,18 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20−
21{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
23
20
21+import Network.Minio
24−import Prelude
22+import Prelude
25
23
26-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
27−-- https://play.minio.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
28-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
29--
27--
30-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
31−--
32−
33main :: IO ()
29main :: IO ()
34main = do
30main = do
35− let
36− bucket = "my-bucket"
31+ let bucket = "my-bucket"
37 res <- runMinio minioPlayCI $ removeBucket bucket
32 res <- runMinio minioPlayCI $ removeBucket bucket
38 print res
33 print res
examples/RemoveIncompleteUpload.hs+10−13
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,27 +16,24 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22
20
21+import Network.Minio
23−import Prelude
22+import Prelude
24
23
25-- | The following example uses minio's play server at
24-- | The following example uses minio's play server at
26−-- https://play.minio.io:9000. The endpoint and associated
25+-- https://play.min.io. The endpoint and associated
27-- credentials are provided via the libary constant,
26-- credentials are provided via the libary constant,
28--
27--
29-- > minioPlayCI :: ConnectInfo
28-- > minioPlayCI :: ConnectInfo
30−--
31−
32main :: IO ()
29main :: IO ()
33main = do
30main = do
34− let
35− bucket = "mybucket"
31+ let bucket = "mybucket"
36− object = "myobject"
32+ object = "myobject"
37
33
34+ res <-
38− res <- runMinio minioPlayCI $
35+ runMinio minioPlayCI $
39− removeIncompleteUpload bucket object
36+ removeIncompleteUpload bucket object
40
37
41 case res of
38 case res of
42 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
39 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/RemoveObject.hs+9−10
@@ -1,8 +1,8 @@
1#!/usr/bin/env stack
1#!/usr/bin/env stack
2−-- stack --resolver lts-9.1 runghc --package minio-hs
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3
3
4--
4--
5−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6--
6--
7-- Licensed under the Apache License, Version 2.0 (the "License");
7-- Licensed under the Apache License, Version 2.0 (the "License");
8-- you may not use this file except in compliance with the License.
8-- you may not use this file except in compliance with the License.
@@ -16,20 +16,19 @@
16-- See the License for the specific language governing permissions and
16-- See the License for the specific language governing permissions and
17-- limitations under the License.
17-- limitations under the License.
18--
18--
19−
20{-# LANGUAGE OverloadedStrings #-}
19{-# LANGUAGE OverloadedStrings #-}
21
20
22−import Network.Minio
21+import Network.Minio
23−import Prelude
22+import Prelude
24
23
25main :: IO ()
24main :: IO ()
26main = do
25main = do
27− let
28− bucket = "mybucket"
26+ let bucket = "mybucket"
29− object = "myobject"
27+ object = "myobject"
30
28
29+ res <-
31− res <- runMinio minioPlayCI $
30+ runMinio minioPlayCI $
32− removeObject bucket object
31+ removeObject bucket object
33
32
34 case res of
33 case res of
35 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
34 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/SelectObject.hsnew file+47
@@ -0,0 +1,47 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2019 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+{-# LANGUAGE OverloadedStrings #-}
20+
21+import qualified Conduit as C
22+import Control.Monad (unless)
23+import Network.Minio
24+import Prelude
25+
26+main :: IO ()
27+main = do
28+ let bucket = "selectbucket"
29+ object = "1.csv"
30+ content =
31+ "Name,Place,Temperature\n"
32+ <> "James,San Jose,76\n"
33+ <> "Alicia,San Leandro,88\n"
34+ <> "Mark,San Carlos,90\n"
35+
36+ res <- runMinio minioPlayCI $ do
37+ exists <- bucketExists bucket
38+ unless exists $
39+ makeBucket bucket Nothing
40+
41+ C.liftIO $ putStrLn "Uploading csv object"
42+ putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
43+
44+ let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
45+ res <- selectObjectContent bucket object sr
46+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
47+ print res
examples/ServerInfo.hsnew file+30
@@ -0,0 +1,30 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ getServerInfo
30+ print res
examples/ServiceSendRestart.hsnew file+29
@@ -0,0 +1,29 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio minioPlayCI $
28+ serviceSendAction ServiceActionRestart
29+ print res
examples/ServiceSendStop.hsnew file+29
@@ -0,0 +1,29 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio minioPlayCI $
28+ serviceSendAction ServiceActionStop
29+ print res
examples/ServiceStatus.hsnew file+30
@@ -0,0 +1,30 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+import Network.Minio
21+import Network.Minio.AdminAPI
22+import Prelude
23+
24+main :: IO ()
25+main = do
26+ res <-
27+ runMinio
28+ minioPlayCI
29+ serviceStatus
30+ print res
examples/SetConfig.hsnew file+31
@@ -0,0 +1,31 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-14.11 runghc --package minio-hs
3+
4+--
5+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+{-# LANGUAGE OverloadedStrings #-}
20+
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
24+
25+main :: IO ()
26+main = do
27+ res <- runMinio minioPlayCI $
28+ do
29+ let config = "{\"version\":\"25\",\"credential\":{\"accessKey\":\"minio\",\"secretKey\":\"minio123\"},\"region\":\"\",\"browser\":\"on\",\"worm\":\"off\",\"domain\":\"\",\"storageclass\":{\"standard\":\"\",\"rrs\":\"\"},\"cache\":{\"drives\":[],\"expiry\":90,\"exclude\":[]},\"notify\":{\"amqp\":{\"2\":{\"enable\":false,\"url\":\"amqp://guest:guest@localhost:5672/\",\"exchange\":\"minio\",\"routingKey\":\"minio\",\"exchangeType\":\"direct\",\"deliveryMode\":0,\"mandatory\":false,\"immediate\":false,\"durable\":false,\"internal\":false,\"noWait\":false,\"autoDeleted\":false}},\"elasticsearch\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"url\":\"http://localhost:9200\",\"index\":\"minio_events\"}},\"kafka\":{\"1\":{\"enable\":false,\"brokers\":null,\"topic\":\"\"}},\"mqtt\":{\"1\":{\"enable\":false,\"broker\":\"\",\"topic\":\"\",\"qos\":0,\"clientId\":\"\",\"username\":\"\",\"password\":\"\",\"reconnectInterval\":0,\"keepAliveInterval\":0}},\"mysql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"dsnString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"nats\":{\"1\":{\"enable\":false,\"address\":\"\",\"subject\":\"\",\"username\":\"\",\"password\":\"\",\"token\":\"\",\"secure\":false,\"pingInterval\":0,\"streaming\":{\"enable\":false,\"clusterID\":\"\",\"clientID\":\"\",\"async\":false,\"maxPubAcksInflight\":0}}},\"postgresql\":{\"1\":{\"enable\":false,\"format\":\"namespace\",\"connectionString\":\"\",\"table\":\"\",\"host\":\"\",\"port\":\"\",\"user\":\"\",\"password\":\"\",\"database\":\"\"}},\"redis\":{\"test1\":{\"enable\":true,\"format\":\"namespace\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_ns\"},\"test2\":{\"enable\":true,\"format\":\"access\",\"address\":\"127.0.0.1:6379\",\"password\":\"\",\"key\":\"bucketevents_log\"}},\"webhook\":{\"1\":{\"enable\":true,\"endpoint\":\"http://localhost:3000\"},\"2\":{\"enable\":true,\"endpoint\":\"http://localhost:3001\"}}}}"
30+ setConfig config
31+ print res
minio-hs.cabal+283−182
@@ -1,252 +1,353 @@
1+cabal-version: 2.4
1name: minio-hs
2name: minio-hs
2−version: 0.3.2
3+version: 1.7.0
3−synopsis: A Minio Haskell Library for Amazon S3 compatible cloud
4+synopsis: A MinIO Haskell Library for Amazon S3 compatible cloud
4 storage.
5 storage.
5−description: The Minio Haskell client library provides simple APIs to
6+description: The MinIO Haskell client library provides simple APIs to
6− access Minio, Amazon S3 and other API compatible cloud
7+ access MinIO, Amazon S3 and other API compatible cloud
7 storage servers.
8 storage servers.
8homepage: https://github.com/minio/minio-hs#readme
9homepage: https://github.com/minio/minio-hs#readme
9license: Apache-2.0
10license: Apache-2.0
10license-file: LICENSE
11license-file: LICENSE
11−author: Aditya Manthramurthy, Krishnan Parthasarathi
12+author: MinIO Dev Team
12−maintainer: dev@minio.io
13+maintainer: dev@min.io
13category: Network, AWS, Object Storage
14category: Network, AWS, Object Storage
14build-type: Simple
15build-type: Simple
15stability: Experimental
16stability: Experimental
16−extra-source-files:
17+extra-doc-files:
17 CHANGELOG.md
18 CHANGELOG.md
18 CONTRIBUTING.md
19 CONTRIBUTING.md
19 docs/API.md
20 docs/API.md
20− examples/*.hs
21 README.md
21 README.md
22+extra-source-files:
23+ examples/*.hs
22 stack.yaml
24 stack.yaml
25+tested-with: GHC == 8.10.7
26+ , GHC == 9.0.2
27+ , GHC == 9.2.8
28+ , GHC == 9.4.8
29+ , GHC == 9.6.5
30+ , GHC == 9.8.2
31+
32+source-repository head
33+ type: git
34+ location: https://github.com/minio/minio-hs.git
23
35
24−cabal-version: >=1.10
36+Flag dev
37+ Description: Build package in development mode
38+ Default: False
39+ Manual: True
25
40
26−library
41+common base-settings
27− hs-source-dirs: src
28 ghc-options: -Wall
42 ghc-options: -Wall
29− exposed-modules: Network.Minio
43+ -Wcompat
30− , Network.Minio.S3API
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+
31 other-modules: Lib.Prelude
81 other-modules: Lib.Prelude
32 , Network.Minio.API
82 , Network.Minio.API
83+ , Network.Minio.APICommon
33 , Network.Minio.Data
84 , Network.Minio.Data
34 , Network.Minio.Data.ByteString
85 , Network.Minio.Data.ByteString
35 , Network.Minio.Data.Crypto
86 , Network.Minio.Data.Crypto
36 , Network.Minio.Data.Time
87 , Network.Minio.Data.Time
88+ , Network.Minio.CopyObject
37 , Network.Minio.Errors
89 , Network.Minio.Errors
38 , Network.Minio.ListOps
90 , Network.Minio.ListOps
39 , Network.Minio.PresignedOperations
91 , Network.Minio.PresignedOperations
40 , Network.Minio.PutObject
92 , Network.Minio.PutObject
93+ , Network.Minio.SelectAPI
41 , Network.Minio.Sign.V4
94 , Network.Minio.Sign.V4
42 , Network.Minio.Utils
95 , Network.Minio.Utils
43 , Network.Minio.XmlGenerator
96 , Network.Minio.XmlGenerator
44 , Network.Minio.XmlParser
97 , 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+
45 build-depends: base >= 4.7 && < 5
108 build-depends: base >= 4.7 && < 5
46− , protolude >= 0.1.6
109+ , relude >= 0.7 && < 2
47− , aeson
110+ , aeson >= 1.2 && < 3
48− , async
49− , base64-bytestring
111+ , base64-bytestring >= 1.0
112+ , binary >= 0.8.5.0
50− , bytestring
113+ , bytestring >= 0.10
51− , case-insensitive
114+ , case-insensitive >= 1.2
52− , conduit
53− , conduit-combinators
115+ , conduit >= 1.3
54− , conduit-extra
116+ , conduit-extra >= 1.3
55− , containers
117+ , crypton-connection
56− , cryptonite
118+ , cryptonite >= 0.25
57− , cryptonite-conduit
119+ , cryptonite-conduit >= 0.2
58− , data-default
120+ , data-default-class
59− , exceptions
121+ , digest >= 0.0.1
122+ , directory
60− , filepath
123+ , filepath >= 1.4
61− , http-client
124+ , http-client >= 0.5
125+ , http-client-tls
62− , http-conduit
126+ , http-conduit >= 2.3
63− , http-types
127+ , http-types >= 0.12
64− , lifted-async
128+ , ini
65− , lifted-base
66− , memory
129+ , memory >= 0.14
67− , monad-control
130+ , network-uri
68− , resourcet
131+ , resourcet >= 1.2
69− , text
132+ , retry
70− , text-format
133+ , text >= 1.2
71− , time
134+ , time >= 1.9
135+ , time-units ^>= 1.0.0
72− , transformers
136+ , transformers >= 0.5
73− , transformers-base
137+ , unliftio >= 0.2 && < 0.3
74− , vector
138+ , unliftio-core >= 0.2 && < 0.3
139+ , unordered-containers >= 0.2
75− , xml-conduit
140+ , xml-conduit >= 1.8
76− default-language: Haskell2010
141+
77− default-extensions: FlexibleContexts
142+library
78− , FlexibleInstances
143+ import: base-settings
79− , BangPatterns
144+ hs-source-dirs: src
80− , MultiParamTypeClasses
145+ exposed-modules: Network.Minio
81− , MultiWayIf
146+ , Network.Minio.AdminAPI
82− , NoImplicitPrelude
147+ , Network.Minio.S3API
83− , OverloadedStrings
84− , RankNTypes
85− , ScopedTypeVariables
86− , TypeFamilies
87− , TupleSections
88
148
89Flag live-test
149Flag live-test
150+ Description: Build the test suite that runs against a live MinIO server
90− Default: True
151+ Default: False
91 Manual: True
152 Manual: True
92
153
93test-suite minio-hs-live-server-test
154test-suite minio-hs-live-server-test
155+ import: base-settings
94 type: exitcode-stdio-1.0
156 type: exitcode-stdio-1.0
95 hs-source-dirs: test, src
157 hs-source-dirs: test, src
96 main-is: LiveServer.hs
158 main-is: LiveServer.hs
97 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
159 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
98− default-language: Haskell2010
99− default-extensions: BangPatterns
100− , FlexibleContexts
101− , FlexibleInstances
102− , OverloadedStrings
103− , NoImplicitPrelude
104− , MultiParamTypeClasses
105− , MultiWayIf
106− , ScopedTypeVariables
107− , RankNTypes
108− , TupleSections
109− , TypeFamilies
110− other-modules: Lib.Prelude
111− , Network.Minio
112− , Network.Minio.API
113− , Network.Minio.Data
114− , Network.Minio.Data.ByteString
115− , Network.Minio.Data.Crypto
116− , Network.Minio.Data.Time
117− , Network.Minio.Errors
160+ other-modules: Network.Minio
118− , Network.Minio.ListOps
119− , Network.Minio.PresignedOperations
120− , Network.Minio.PutObject
121 , Network.Minio.S3API
161 , Network.Minio.S3API
122− , Network.Minio.Sign.V4
162+ , Network.Minio.AdminAPI
123− , Network.Minio.Utils
124− , Network.Minio.Utils.Test
125 , Network.Minio.API.Test
163 , Network.Minio.API.Test
126− , Network.Minio.XmlGenerator
164+ , Network.Minio.JsonParser.Test
165+ , Network.Minio.TestHelpers
166+ , Network.Minio.Utils.Test
127 , Network.Minio.XmlGenerator.Test
167 , Network.Minio.XmlGenerator.Test
128− , Network.Minio.XmlParser
129 , Network.Minio.XmlParser.Test
168 , Network.Minio.XmlParser.Test
169+ , Network.Minio.Credentials
130− build-depends: base
170+ build-depends: minio-hs
131− , minio-hs
171+ , raw-strings-qq
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
157 , tasty
172 , tasty
158 , tasty-hunit
173 , tasty-hunit
159 , tasty-quickcheck
174 , tasty-quickcheck
160 , tasty-smallcheck
175 , tasty-smallcheck
161− , temporary
176+ , QuickCheck
162− , text
163− , text-format
164− , time
165− , transformers
166− , transformers-base
167− , vector
168− , xml-conduit
169 if !flag(live-test)
177 if !flag(live-test)
170 buildable: False
178 buildable: False
171
179
172test-suite minio-hs-test
180test-suite minio-hs-test
181+ import: base-settings
173 type: exitcode-stdio-1.0
182 type: exitcode-stdio-1.0
174 hs-source-dirs: test, src
183 hs-source-dirs: test, src
175 main-is: Spec.hs
184 main-is: Spec.hs
176− build-depends: base
185+ build-depends: minio-hs
177− , minio-hs
186+ , raw-strings-qq
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
201 , QuickCheck
187 , QuickCheck
202− , resourcet
203 , tasty
188 , tasty
204 , tasty-hunit
189 , tasty-hunit
205 , tasty-quickcheck
190 , tasty-quickcheck
206 , tasty-smallcheck
191 , tasty-smallcheck
207− , temporary
208− , text
209− , text-format
210− , time
211− , transformers
212− , transformers-base
213− , vector
214− , xml-conduit
215 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
192 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
228 other-modules: Lib.Prelude
193 other-modules: Lib.Prelude
229 , Network.Minio
194 , 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
239 , Network.Minio.S3API
195 , Network.Minio.S3API
240− , Network.Minio.Sign.V4
196+ , Network.Minio.AdminAPI
241− , Network.Minio.Utils
242− , Network.Minio.Utils.Test
197+ , Network.Minio.TestHelpers
243 , Network.Minio.API.Test
198 , Network.Minio.API.Test
244− , Network.Minio.XmlGenerator
199+ , Network.Minio.JsonParser.Test
200+ , Network.Minio.Utils.Test
245 , Network.Minio.XmlGenerator.Test
201 , Network.Minio.XmlGenerator.Test
246− , Network.Minio.XmlParser
247 , Network.Minio.XmlParser.Test
202 , Network.Minio.XmlParser.Test
203+ , Network.Minio.Credentials
248
204
205+Flag examples
206+ Description: Build the examples
207+ Default: False
208+ Manual: True
249
209
250−source-repository head
210+common examples-settings
251− type: git
211+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
252− location: https://github.com/minio/minio-hs
212+ default-language: Haskell2010
213+ default-extensions: OverloadedStrings
214+ build-depends: base >= 4.7 && < 5
215+ , bytestring
216+ , case-insensitive
217+ , conduit
218+ , conduit-extra
219+ , filepath
220+ , minio-hs
221+ , optparse-applicative
222+ , text
223+ , time
224+ , unliftio
225+ , unordered-containers
226+ hs-source-dirs: examples
227+ if !flag(examples)
228+ buildable: False
229+
230+executable BucketExists
231+ import: examples-settings
232+ scope: private
233+ main-is: BucketExists.hs
234+
235+executable CopyObject
236+ import: examples-settings
237+ scope: private
238+ main-is: CopyObject.hs
239+
240+executable FileUploader
241+ import: examples-settings
242+ scope: private
243+ main-is: FileUploader.hs
244+
245+executable GetConfig
246+ import: examples-settings
247+ scope: private
248+ main-is: GetConfig.hs
249+
250+executable GetObject
251+ import: examples-settings
252+ scope: private
253+ main-is: GetObject.hs
254+
255+executable HeadObject
256+ import: examples-settings
257+ scope: private
258+ main-is: HeadObject.hs
259+
260+executable Heal
261+ import: examples-settings
262+ scope: private
263+ main-is: Heal.hs
264+
265+executable ListBuckets
266+ import: examples-settings
267+ scope: private
268+ main-is: ListBuckets.hs
269+
270+executable ListIncompleteUploads
271+ import: examples-settings
272+ scope: private
273+ main-is: ListIncompleteUploads.hs
274+
275+executable ListObjects
276+ import: examples-settings
277+ scope: private
278+ main-is: ListObjects.hs
279+
280+executable MakeBucket
281+ import: examples-settings
282+ scope: private
283+ main-is: MakeBucket.hs
284+
285+executable PresignedGetObject
286+ import: examples-settings
287+ scope: private
288+ main-is: PresignedGetObject.hs
289+
290+executable PresignedPostPolicy
291+ import: examples-settings
292+ scope: private
293+ main-is: PresignedPostPolicy.hs
294+
295+executable PresignedPutObject
296+ import: examples-settings
297+ scope: private
298+ main-is: PresignedPutObject.hs
299+
300+executable PutObject
301+ import: examples-settings
302+ scope: private
303+ main-is: PutObject.hs
304+
305+executable RemoveBucket
306+ import: examples-settings
307+ scope: private
308+ main-is: RemoveBucket.hs
309+
310+executable RemoveIncompleteUpload
311+ import: examples-settings
312+ scope: private
313+ main-is: RemoveIncompleteUpload.hs
314+
315+executable RemoveObject
316+ import: examples-settings
317+ scope: private
318+ main-is: RemoveObject.hs
319+
320+executable SelectObject
321+ import: examples-settings
322+ scope: private
323+ main-is: SelectObject.hs
324+
325+executable ServerInfo
326+ import: examples-settings
327+ scope: private
328+ main-is: ServerInfo.hs
329+
330+executable ServiceSendRestart
331+ import: examples-settings
332+ scope: private
333+ main-is: ServiceSendRestart.hs
334+
335+executable ServiceSendStop
336+ import: examples-settings
337+ scope: private
338+ main-is: ServiceSendStop.hs
339+
340+executable ServiceStatus
341+ import: examples-settings
342+ scope: private
343+ main-is: ServiceStatus.hs
344+
345+executable SetConfig
346+ import: examples-settings
347+ scope: private
348+ main-is: SetConfig.hs
349+
350+executable AssumeRole
351+ import: examples-settings
352+ scope: private
353+ main-is: AssumeRole.hs
src/Lib/Prelude.hs+34−25
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,32 +15,41 @@
15--
15--
16
16
17module Lib.Prelude
17module Lib.Prelude
18− ( module Exports
18+ ( module Exports,
19− , both
19+ both,
20+ showBS,
21+ toStrictBS,
22+ fromStrictBS,
23+ lastMay,
24+ )
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+ )
20
40
21− , format
41+-- | Apply a function on both elements of a pair
22− , formatBS
42+both :: (a -> b) -> (a, a) -> (b, b)
23− ) where
43+both f (a, b) = (f a, f b)
24−
25−import Protolude as Exports
26−
27−import Data.Time as Exports (UTCTime(..), diffUTCTime)
28−import Control.Monad.Trans.Maybe as Exports (runMaybeT, MaybeT(..))
29−
30−import Control.Monad.Catch as Exports (throwM, MonadThrow, MonadCatch)
31−
32−import Data.Text.Format as Exports (Shown(..))
33−import qualified Data.Text.Format as TF
34−import Data.Text.Format.Params (Params)
35
44
36−format :: Params ps => TF.Format -> ps -> Text
45+showBS :: (Show a) => a -> ByteString
37−format f args = toS $ TF.format f args
46+showBS a = encodeUtf8 (show a :: Text)
38
47
39−formatBS :: Params ps => TF.Format -> ps -> ByteString
48+toStrictBS :: LByteString -> ByteString
40−formatBS f args = toS $ TF.format f args
49+toStrictBS = LB.toStrict
41
50
42−-- import Data.Tuple as Exports (uncurry)
51+fromStrictBS :: ByteString -> LByteString
52+fromStrictBS = LB.fromStrict
43
53
44−-- | Apply a function on both elements of a pair
54+lastMay :: [a] -> Maybe a
45−both :: (a -> b) -> (a, a) -> (b, b)
55+lastMay a = last <$> nonEmpty a
46−both f (a, b) = (f a, f b)
src/Network/Minio.hs+280−160
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,143 +14,239 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17−
17+-- |
18−
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.
19module Network.Minio
25module Network.Minio
20− (
26+ ( -- * Credentials
21−
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+
22− -- * Connecting to object storage
43+ -- * Connecting to object storage
23− ---------------------------------
24− ConnectInfo(..)
44+ ConnectInfo,
25− , awsCI
45+ setRegion,
26−
46+ setCreds,
47+ setCredsFrom,
48+ isConnectInfoSecure,
49+ disableTLSCertValidation,
50+ MinioConn,
51+ mkMinioConn,
52+
27− -- ** Connection helpers
53+ -- ** Connection helpers
28− ------------------------
54+
29− , awsWithRegionCI
55+ -- | These are helpers to construct 'ConnectInfo' values for common
56+ -- cases.
30− , minioPlayCI
57+ minioPlayCI,
31− , minioCI
58+ awsCI,
32−
59+ gcsCI,
60+
61+ -- ** STS Credential types
62+ STSAssumeRole (..),
63+ STSAssumeRoleOptions (..),
64+ defaultSTSAssumeRoleOptions,
65+ requestSTSCredential,
66+ setSTSCredential,
67+ ExpiryTime (..),
68+ STSCredentialProvider,
69+
33− -- * Minio Monad
70+ -- * Minio Monad
71+
34− ----------------
72+ ----------------
73+
35− -- | The Minio monad provides connection-reuse, bucket-location
74+ -- | The Minio Monad provides connection-reuse, bucket-location
36− -- caching, resource management and simpler error handling
75+ -- caching, resource management and simpler error handling
37− -- functionality. All actions on object storage are performed within
76+ -- functionality. All actions on object storage are performed within
38− -- this Monad.
77+ -- this Monad.
39−
78+ Minio,
40− , Minio
79+ runMinioWith,
41− , runMinio
80+ runMinio,
42− , def
81+ runMinioResWith,
43−
82+ runMinioRes,
83+
44− -- * Bucket Operations
84+ -- * Bucket Operations
45− ----------------------
85+
46−
47− -- ** Creation, removal and querying
86+ -- ** Creation, removal and querying
48− , Bucket
87+ Bucket,
49− , makeBucket
88+ makeBucket,
50− , removeBucket
89+ removeBucket,
51− , bucketExists
90+ bucketExists,
52− , Region
91+ Region,
53− , getLocation
92+ getLocation,
54−
93+
55− -- ** Listing
94+ -- ** Listing buckets
56− , BucketInfo(..)
95+ BucketInfo (..),
57− , listBuckets
96+ listBuckets,
58− , ObjectInfo(..)
97+
98+ -- ** Listing objects
59− , listObjects
99+ listObjects,
60− , listObjectsV1
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,
61− , UploadId
112+ UploadId,
62− , UploadInfo(..)
113+ UploadInfo (..),
63− , listIncompleteUploads
114+ listIncompleteParts,
64− , ObjectPartInfo(..)
115+ ObjectPartInfo (..),
65− , listIncompleteParts
116+
66−
67− -- ** Notifications
117+ -- ** Bucket Notifications
118+ getBucketNotification,
119+ putBucketNotification,
120+ removeAllBucketNotification,
68− , Notification(..)
121+ Notification (..),
122+ defaultNotification,
69− , NotificationConfig(..)
123+ NotificationConfig (..),
70− , Arn
124+ Arn,
71− , Event(..)
125+ Event (..),
72− , Filter(..)
126+ Filter (..),
127+ defaultFilter,
73− , FilterKey(..)
128+ FilterKey (..),
129+ defaultFilterKey,
74− , FilterRules(..)
130+ FilterRules (..),
131+ defaultFilterRules,
75− , FilterRule(..)
132+ FilterRule (..),
76− , getBucketNotification
133+
77− , putBucketNotification
78− , removeAllBucketNotification
79−
80− -- * Object Operations
134+ -- * Object Operations
81− ----------------------
135+ Object,
82−
136+
83− , Object
84−
85− -- ** File operations
137+ -- ** File-based operations
86− , fGetObject
138+ fGetObject,
87− , fPutObject
139+ fPutObject,
88−
140+
89− -- ** Conduit-based streaming operations
141+ -- ** Conduit-based streaming operations
90− , putObject
142+ putObject,
143+ PutObjectOptions,
144+ defaultPutObjectOptions,
145+ pooContentType,
146+ pooContentEncoding,
147+ pooContentDisposition,
148+ pooContentLanguage,
149+ pooCacheControl,
150+ pooStorageClass,
151+ pooUserMetadata,
152+ pooNumThreads,
153+ pooSSE,
91− , getObject
154+ getObject,
92−
155+ GetObjectOptions,
156+ defaultGetObjectOptions,
157+ gooRange,
158+ gooIfMatch,
159+ gooIfNoneMatch,
160+ gooIfModifiedSince,
161+ gooIfUnmodifiedSince,
162+ gooSSECKey,
163+ GetObjectResponse,
164+ gorObjectInfo,
165+ gorObjectStream,
166+
93− -- ** Server-side copying
167+ -- ** Server-side object copying
94− , CopyPartSource(..)
95− , copyObject
168+ copyObject,
96−
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+
97− -- ** Querying
183+ -- ** Querying object info
98− , statObject
184+ statObject,
99−
185+
100− -- ** Object removal functions
186+ -- ** Object removal operations
101− , removeObject
187+ removeObject,
102− , removeIncompleteUpload
188+ removeIncompleteUpload,
103−
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+
104− -- * Presigned Operations
198+ -- * Presigned Operations
105− -------------------------
106− , UrlExpiry
107− , presignedPutObjectUrl
199+ presignedPutObjectUrl,
108− , presignedGetObjectUrl
200+ presignedGetObjectUrl,
109− , presignedHeadObjectUrl
201+ presignedHeadObjectUrl,
110−
202+ UrlExpiry,
203+
111− -- ** Utilities for POST (browser) uploads
204+ -- ** POST (browser) upload helpers
112− , PostPolicy
205+
113− , PostPolicyError(..)
206+ -- | Please see
207+ -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
208+ -- for detailed information.
114− , newPostPolicy
209+ newPostPolicy,
115− , presignedPostPolicy
210+ presignedPostPolicy,
116− , showPostPolicy
211+ showPostPolicy,
117−
212+ PostPolicy,
118− -- *** Utilities to specify Post Policy conditions
213+ PostPolicyError (..),
214+
215+ -- *** Post Policy condition helpers
119− , PostPolicyCondition
216+ PostPolicyCondition,
120− , ppCondBucket
217+ ppCondBucket,
121− , ppCondContentLengthRange
218+ ppCondContentLengthRange,
122− , ppCondContentType
219+ ppCondContentType,
123− , ppCondKey
220+ ppCondKey,
124− , ppCondKeyStartsWith
221+ ppCondKeyStartsWith,
125− , ppCondSuccessActionStatus
222+ ppCondSuccessActionStatus,
126−
223+
127− -- * Error handling
224+ -- * Error handling
128− -----------------------
225+
129− -- | Data types representing various errors that may occur while working
226+ -- | Data types representing various errors that may occur while
130− -- with an object storage service.
227+ -- working with an object storage service.
131− , MinioErr(..)
228+ MinioErr (..),
132− , MErrV(..)
229+ MErrV (..),
133− , ServiceErr(..)
230+ ServiceErr (..),
134−
231+ )
135−) where
232+where
136
233
137{-
234{-
138−This module exports the high-level Minio API for object storage.
235+This module exports the high-level MinIO API for object storage.
139-}
236-}
140
237
141−import qualified Data.Conduit as C
238+import qualified Data.Conduit as C
142−import qualified Data.Conduit.Binary as CB
239+import qualified Data.Conduit.Binary as CB
143import qualified Data.Conduit.Combinators as CC
240import qualified Data.Conduit.Combinators as CC
144−import Data.Default (def)
241+import Network.Minio.API
145−import qualified Data.Map as Map
242+import Network.Minio.CopyObject
146−
243+import Network.Minio.Credentials
147−import Lib.Prelude
148−
149−import Network.Minio.Data
244+import Network.Minio.Data
150−import Network.Minio.Errors
245+import Network.Minio.Errors
151−import Network.Minio.ListOps
246+import Network.Minio.ListOps
152−import Network.Minio.PutObject
247+import Network.Minio.PutObject
153−import Network.Minio.S3API
248+import Network.Minio.S3API
249+import Network.Minio.SelectAPI
154
250
155-- | Lists buckets.
251-- | Lists buckets.
156listBuckets :: Minio [BucketInfo]
252listBuckets :: Minio [BucketInfo]
@@ -159,43 +255,65 @@ listBuckets = getService
159-- | Fetch the object and write it to the given file safely. The
255-- | Fetch the object and write it to the given file safely. The
160-- object is first written to a temporary file in the same directory
256-- object is first written to a temporary file in the same directory
161-- and then moved to the given path.
257-- and then moved to the given path.
162−fGetObject :: Bucket -> Object -> FilePath -> Minio ()
258+fGetObject :: Bucket -> Object -> FilePath -> GetObjectOptions -> Minio ()
163−fGetObject bucket object fp = do
259+fGetObject bucket object fp opts = do
164− src <- getObject bucket object
260+ src <- getObject bucket object opts
165− src C.$$+- CB.sinkFileCautious fp
261+ C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
166
262
167-- | Upload the given file to the given object.
263-- | Upload the given file to the given object.
168−fPutObject :: Bucket -> Object -> FilePath -> Minio ()
264+fPutObject ::
265+ Bucket ->
266+ Object ->
267+ FilePath ->
268+ PutObjectOptions ->
269+ Minio ()
270+fPutObject bucket object f opts =
169−fPutObject bucket object f = void $ putObjectInternal bucket object $
271+ void $ putObjectInternal bucket object opts $ ODFile f Nothing
170− ODFile f Nothing
171
272
172-- | Put an object from a conduit source. The size can be provided if
273-- | Put an object from a conduit source. The size can be provided if
173-- known; this helps the library select optimal part sizes to perform
274-- known; this helps the library select optimal part sizes to perform
174-- a multipart upload. If not specified, it is assumed that the object
275-- a multipart upload. If not specified, it is assumed that the object
175-- can be potentially 5TiB and selects multipart sizes appropriately.
276-- can be potentially 5TiB and selects multipart sizes appropriately.
176−putObject :: Bucket -> Object -> C.Producer Minio ByteString
277+putObject ::
278+ Bucket ->
279+ Object ->
280+ C.ConduitM () ByteString Minio () ->
177− -> Maybe Int64 -> Minio ()
281+ Maybe Int64 ->
282+ PutObjectOptions ->
283+ Minio ()
178−putObject bucket object src sizeMay =
284+putObject bucket object src sizeMay opts =
179− void $ putObjectInternal bucket object $ ODStream src sizeMay
285+ void $ putObjectInternal bucket object opts $ ODStream src sizeMay
180−
286+
181−-- | Perform a server-side copy operation to create an object with the
287+-- | Perform a server-side copy operation to create an object based on
182−-- given bucket and object name from the source specification in
288+-- the destination specification in DestinationInfo from the source
183−-- CopyPartSource. This function performs a multipart copy operation
289+-- specification in SourceInfo. This function performs a multipart
184−-- if the new object is to be greater than 5GiB in size.
290+-- copy operation if the new object is to be greater than 5GiB in
185−copyObject :: Bucket -> Object -> CopyPartSource -> Minio ()
291+-- size.
292+copyObject :: DestinationInfo -> SourceInfo -> Minio ()
293+copyObject dstInfo srcInfo =
294+ void $
186−copyObject bucket object cps = void $ copyObjectInternal bucket object cps
295+ copyObjectInternal
296+ (dstBucket dstInfo)
297+ (dstObject dstInfo)
298+ srcInfo
187
299
188-- | Remove an object from the object store.
300-- | Remove an object from the object store.
189removeObject :: Bucket -> Object -> Minio ()
301removeObject :: Bucket -> Object -> Minio ()
190removeObject = deleteObject
302removeObject = deleteObject
191
303
192−-- | Get an object from the object store as a resumable source (conduit).
304+-- | Get an object from the object store.
193−getObject :: Bucket -> Object -> Minio (C.ResumableSource Minio ByteString)
305+getObject ::
306+ Bucket ->
307+ Object ->
308+ GetObjectOptions ->
309+ Minio GetObjectResponse
194−getObject bucket object = snd <$> getObject' bucket object [] []
310+getObject bucket object opts =
311+ getObject' bucket object [] $ gooToHeaders opts
195
312
196−-- | Get an object's metadata from the object store.
313+-- | Get an object's metadata from the object store. It accepts the
314+-- same options as GetObject.
197−statObject :: Bucket -> Object -> Minio ObjectInfo
315+statObject :: Bucket -> Object -> GetObjectOptions -> Minio ObjectInfo
198−statObject = headObject
316+statObject b o opts = headObject b o $ gooToHeaders opts
199
317
200-- | Creates a new bucket in the object store. The Region can be
318-- | Creates a new bucket in the object store. The Region can be
201-- optionally specified. If not specified, it will use the region
319-- optionally specified. If not specified, it will use the region
@@ -205,21 +323,23 @@ makeBucket :: Bucket -> Maybe Region -> Minio ()
205makeBucket bucket regionMay = do
323makeBucket bucket regionMay = do
206 region <- maybe (asks $ connectRegion . mcConnInfo) return regionMay
324 region <- maybe (asks $ connectRegion . mcConnInfo) return regionMay
207 putBucket bucket region
325 putBucket bucket region
208− modify (Map.insert bucket region)
326+ addToRegionCache bucket region
209
327
210-- | Removes a bucket from the object store.
328-- | Removes a bucket from the object store.
211removeBucket :: Bucket -> Minio ()
329removeBucket :: Bucket -> Minio ()
212removeBucket bucket = do
330removeBucket bucket = do
213 deleteBucket bucket
331 deleteBucket bucket
214− modify (Map.delete bucket)
332+ deleteFromRegionCache bucket
215
333
216-- | Query the object store if a given bucket is present.
334-- | Query the object store if a given bucket is present.
217bucketExists :: Bucket -> Minio Bool
335bucketExists :: Bucket -> Minio Bool
218bucketExists = headBucket
336bucketExists = headBucket
219
337
220−
221-- | Removes an ongoing multipart upload of an object.
338-- | Removes an ongoing multipart upload of an object.
222removeIncompleteUpload :: Bucket -> Object -> Minio ()
339removeIncompleteUpload :: Bucket -> Object -> Minio ()
223removeIncompleteUpload bucket object = do
340removeIncompleteUpload bucket object = do
341+ uploads <-
342+ C.runConduit $
224− uploads <- listIncompleteUploads bucket (Just object) False C.$$ CC.sinkList
343+ listIncompleteUploads bucket (Just object) False
344+ C..| CC.sinkList
225 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
345 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs+284−140
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,196 +15,340 @@
15--
15--
16
16
17module Network.Minio.API
17module Network.Minio.API
18− (
19− connect
18+ ( connect,
20− , RequestInfo(..)
19+ S3ReqInfo (..),
21− , runMinio
20+ runMinio,
22− , executeRequest
21+ executeRequest,
22+ buildRequest,
23− , mkStreamRequest
23+ mkStreamRequest,
24− , getLocation
24+ getLocation,
25−
26− , isValidBucketName
25+ isValidBucketName,
27− , checkBucketNameValidity
26+ checkBucketNameValidity,
28− , isValidObjectName
27+ isValidObjectName,
29− , checkObjectNameValidity
28+ checkObjectNameValidity,
29+ requestSTSCredential,
30+ )
30− ) where
31+where
31−
32+
32−import qualified Data.Conduit as C
33+import Control.Retry
33−import Data.Conduit.Binary (sourceHandleRange)
34+ ( fullJitterBackoff,
34−import Data.Default (def)
35+ limitRetriesByCumulativeDelay,
36+ retrying,
37+ )
35−import qualified Data.Map as Map
38+import qualified Data.ByteString as B
36import qualified Data.Char as C
39import qualified Data.Char as C
40+import qualified Data.Conduit as C
41+import qualified Data.HashMap.Strict as H
37import qualified Data.Text as T
42import qualified Data.Text as T
38−import qualified Data.ByteString as B
43+import qualified Data.Time.Clock as Time
39−
44+import Lib.Prelude
45+import Network.HTTP.Client (defaultManagerSettings)
46+import qualified Network.HTTP.Client as NClient
40−import Network.HTTP.Conduit (Response)
47+import Network.HTTP.Conduit (Response)
41import qualified Network.HTTP.Conduit as NC
48import qualified Network.HTTP.Conduit as NC
49+import Network.HTTP.Types (simpleQueryToQuery)
42import qualified Network.HTTP.Types as HT
50import qualified Network.HTTP.Types as HT
43import Network.HTTP.Types.Header (hHost)
51import Network.HTTP.Types.Header (hHost)
44−
52+import Network.Minio.APICommon
45−import Lib.Prelude
46−
47−import Network.Minio.Data
53+import Network.Minio.Credentials
48−import Network.Minio.Data.Crypto
54+import Network.Minio.Data
49−import Network.Minio.Errors
55+import Network.Minio.Errors
50−import Network.Minio.Sign.V4
56+import Network.Minio.Sign.V4
51−import Network.Minio.Utils
57+import Network.Minio.Utils
52−import Network.Minio.XmlParser
58+import Network.Minio.XmlParser
53−
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−
72
59
73-- | Fetch bucket location (region)
60-- | Fetch bucket location (region)
74getLocation :: Bucket -> Minio Region
61getLocation :: Bucket -> Minio Region
75getLocation bucket = do
62getLocation bucket = do
63+ resp <-
76− resp <- executeRequest $ def {
64+ executeRequest $
65+ defaultS3ReqInfo
77− riBucket = Just bucket
66+ { riBucket = Just bucket,
78− , riQueryParams = [("location", Nothing)]
67+ riQueryParams = [("location", Nothing)],
79− , riNeedsLocation = False
68+ riNeedsLocation = False
80− }
69+ }
81 parseLocation $ NC.responseBody resp
70 parseLocation $ NC.responseBody resp
82
71
83−
84-- | Looks for region in RegionMap and updates it using getLocation if
72-- | Looks for region in RegionMap and updates it using getLocation if
85-- absent.
73-- absent.
86−discoverRegion :: RequestInfo -> Minio (Maybe Region)
74+discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
87discoverRegion ri = runMaybeT $ do
75discoverRegion ri = runMaybeT $ do
88 bucket <- MaybeT $ return $ riBucket ri
76 bucket <- MaybeT $ return $ riBucket ri
89− regionMay <- gets (Map.lookup bucket)
77+ regionMay <- lift $ lookupRegionCache bucket
90− maybe (do
78+ maybe
79+ ( do
91− l <- lift $ getLocation bucket
80+ l <- lift $ getLocation bucket
92− modify $ Map.insert bucket l
81+ lift $ addToRegionCache bucket l
93− return l
82+ return l
94− ) return regionMay
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
95
105
106+ if "amazonaws.com" `T.isSuffixOf` connectHost ci
107+ then
108+ maybe
109+ (throwIO $ MErrVRegionNotSupported r)
110+ return
111+ (H.lookup r awsRegionMap)
112+ else return $ connectHost ci
96
113
114+-- | Computes the appropriate host, path and region for the request.
115+--
116+-- For AWS, always use virtual bucket style, unless bucket has periods. For
117+-- MinIO and other non-AWS, default to path style.
118+getHostPathRegion :: S3ReqInfo -> Minio (Text, ByteString, Maybe Region)
119+getHostPathRegion ri = do
120+ ci <- asks mcConnInfo
121+ regionMay <- getRegion ri
122+ case riBucket ri of
123+ Nothing ->
124+ -- Implies a ListBuckets request.
125+ return (connectHost ci, "/", regionMay)
126+ Just bucket -> do
127+ regionHost <- case regionMay of
128+ Nothing -> return $ connectHost ci
129+ Just "" -> return $ connectHost ci
130+ Just r -> getRegionHost r
131+ let pathStyle =
132+ ( regionHost,
133+ getS3Path (riBucket ri) (riObject ri),
134+ regionMay
135+ )
136+ virtualStyle =
137+ ( bucket <> "." <> regionHost,
138+ encodeUtf8 $ "/" <> fromMaybe "" (riObject ri),
139+ regionMay
140+ )
141+ ( if isAWSConnectInfo ci
142+ then
143+ return $
144+ if bucketHasPeriods bucket
145+ then pathStyle
146+ else virtualStyle
147+ else return pathStyle
148+ )
149+
150+-- | requestSTSCredential requests temporary credentials using the Security Token
151+-- Service API. The returned credential will include a populated 'SessionToken'
152+-- and an 'ExpiryTime'.
153+requestSTSCredential :: (STSCredentialProvider p) => p -> IO (CredentialValue, ExpiryTime)
154+requestSTSCredential p = do
155+ endpoint <- maybe (throwIO $ MErrValidation MErrVSTSEndpointNotFound) return $ getSTSEndpoint p
156+ let endPt = NC.parseRequest_ $ toString endpoint
157+ settings
158+ | NC.secure endPt = NC.tlsManagerSettings
159+ | otherwise = defaultManagerSettings
160+
161+ mgr <- NC.newManager settings
162+ liftIO $ retrieveSTSCredentials p ("", 0, False) mgr
163+
97−buildRequest :: RequestInfo -> Minio NC.Request
164+buildRequest :: S3ReqInfo -> Minio NC.Request
98buildRequest ri = do
165buildRequest ri = do
99 maybe (return ()) checkBucketNameValidity $ riBucket ri
166 maybe (return ()) checkBucketNameValidity $ riBucket ri
100 maybe (return ()) checkObjectNameValidity $ riObject ri
167 maybe (return ()) checkObjectNameValidity $ riObject ri
101
168
102 ci <- asks mcConnInfo
169 ci <- asks mcConnInfo
103
170
104− -- getService/makeBucket/getLocation -- don't need
171+ (host, path, regionMay) <- getHostPathRegion ri
105− -- location
172+
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
173+ let ci' = ci {connectHost = host}
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,
174+ hostHeader = (hHost, getHostAddr ci')
133− show $ connectPort ci])
175+ ri' =
134−
176+ ri
135− newRi = ri { riPayloadHash = Just sha256Hash
136− , riHeaders = hostHeader
177+ { riHeaders = hostHeader : riHeaders ri,
137− : sha256Header sha256Hash
138− : riHeaders ri
139− , riRegion = region
178+ riRegion = regionMay
140− }
179+ }
141− newCi = ci { connectHost = regionHost }
180+ -- Does not contain body and auth info.
142−
181+ baseRequest =
143− signHeaders <- liftIO $ signV4 newCi newRi Nothing
144−
145− return NC.defaultRequest {
182+ NC.defaultRequest
146− NC.method = riMethod newRi
183+ { NC.method = riMethod ri',
147− , NC.secure = connectIsSecure newCi
184+ NC.secure = connectIsSecure ci',
148− , NC.host = encodeUtf8 $ connectHost newCi
185+ NC.host = encodeUtf8 $ connectHost ci',
149− , NC.port = connectPort newCi
186+ NC.port = connectPort ci',
150− , NC.path = getPathFromRI newRi
187+ NC.path = path,
188+ NC.requestHeaders = riHeaders ri',
151− , NC.queryString = HT.renderQuery False $ riQueryParams newRi
189+ NC.queryString = HT.renderQuery False $ riQueryParams ri'
152− , NC.requestHeaders = riHeaders newRi ++ mkHeaderFromPairs signHeaders
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,
153− , NC.requestBody = getRequestBody (riPayload newRi)
256+ NC.requestBody = getRequestBody (riPayload ri')
154− }
257+ }
155−
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+
156−executeRequest :: RequestInfo -> Minio (Response LByteString)
291+executeRequest :: S3ReqInfo -> Minio (Response LByteString)
157executeRequest ri = do
292executeRequest ri = do
158 req <- buildRequest ri
293 req <- buildRequest ri
159 mgr <- asks mcConnManager
294 mgr <- asks mcConnManager
160− httpLbs req mgr
295+ retryAPIRequest $ httpLbs req mgr
161−
162
296
163−mkStreamRequest :: RequestInfo
297+mkStreamRequest ::
298+ S3ReqInfo ->
164− -> Minio (Response (C.ResumableSource Minio ByteString))
299+ Minio (Response (C.ConduitM () ByteString Minio ()))
165mkStreamRequest ri = do
300mkStreamRequest ri = do
166 req <- buildRequest ri
301 req <- buildRequest ri
167 mgr <- asks mcConnManager
302 mgr <- asks mcConnManager
168− http req mgr
303+ retryAPIRequest $ http req mgr
169
304
170-- Bucket name validity check according to AWS rules.
305-- Bucket name validity check according to AWS rules.
171isValidBucketName :: Bucket -> Bool
306isValidBucketName :: Bucket -> Bool
172isValidBucketName bucket =
307isValidBucketName bucket =
308+ not
309+ ( or
173− not (or [ len < 3 || len > 63
310+ [ len < 3 || len > 63,
174− , or (map labelCheck labels)
311+ any labelCheck labels,
175− , or (map labelCharsCheck labels)
312+ any labelCharsCheck labels,
176− , isIPCheck
313+ isIPCheck
177− ])
314+ ]
315+ )
178 where
316 where
179 len = T.length bucket
317 len = T.length bucket
180 labels = T.splitOn "." bucket
318 labels = T.splitOn "." bucket
181−
182 -- does label `l` fail basic checks of length and start/end?
319 -- does label `l` fail basic checks of length and start/end?
183 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
320 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
184−
185 -- does label `l` have non-allowed characters?
321 -- does label `l` have non-allowed characters?
186− labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
322+ labelCharsCheck l =
323+ isJust $
324+ T.find
325+ ( \x ->
326+ not
327+ ( C.isAsciiLower x
187− x == '-' ||
328+ || x == '-'
188− C.isDigit x)) l
329+ || C.isDigit x
189−
330+ )
331+ )
332+ l
190 -- does label `l` have non-digit characters?
333 -- does label `l` have non-digit characters?
191 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
334 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
192 labelAsNums = map (not . labelNonDigits) labels
335 labelAsNums = map (not . labelNonDigits) labels
193−
194 -- check if bucket name looks like an IP
336 -- check if bucket name looks like an IP
195 isIPCheck = and labelAsNums && length labelAsNums == 4
337 isIPCheck = and labelAsNums && length labelAsNums == 4
196
338
197-- Throws exception iff bucket name is invalid according to AWS rules.
339-- Throws exception iff bucket name is invalid according to AWS rules.
198−checkBucketNameValidity :: MonadThrow m => Bucket -> m ()
340+checkBucketNameValidity :: (MonadIO m) => Bucket -> m ()
199checkBucketNameValidity bucket =
341checkBucketNameValidity bucket =
200− when (not $ isValidBucketName bucket) $
342+ unless (isValidBucketName bucket) $
343+ throwIO $
201− throwM $ MErrVInvalidBucketName bucket
344+ MErrVInvalidBucketName bucket
202
345
203isValidObjectName :: Object -> Bool
346isValidObjectName :: Object -> Bool
204isValidObjectName object =
347isValidObjectName object =
205 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
348 T.length object > 0 && B.length (encodeUtf8 object) <= 1024
206
349
207−checkObjectNameValidity :: MonadThrow m => Object -> m ()
350+checkObjectNameValidity :: (MonadIO m) => Object -> m ()
208checkObjectNameValidity object =
351checkObjectNameValidity object =
209− when (not $ isValidObjectName object) $
352+ unless (isValidObjectName object) $
353+ throwIO $
210− throwM $ MErrVInvalidObjectName object
354+ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hsnew file+80
@@ -0,0 +1,80 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.APICommon where
18+
19+import qualified Conduit as C
20+import qualified Data.ByteString as BS
21+import qualified Data.ByteString.Lazy as LB
22+import Data.Conduit.Binary (sourceHandleRange)
23+import qualified Data.Text as T
24+import Lib.Prelude
25+import qualified Network.HTTP.Conduit as NC
26+import qualified Network.HTTP.Types as HT
27+import Network.Minio.Data
28+import Network.Minio.Data.Crypto
29+import Network.Minio.Errors
30+
31+sha256Header :: ByteString -> HT.Header
32+sha256Header = ("x-amz-content-sha256",)
33+
34+-- | This function throws an error if the payload is a conduit (as it
35+-- will not be possible to re-read the conduit after it is consumed).
36+getPayloadSHA256Hash :: Payload -> Minio ByteString
37+getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
38+getPayloadSHA256Hash (PayloadH h off size) =
39+ hashSHA256FromSource $
40+ sourceHandleRange
41+ h
42+ (return . fromIntegral $ off)
43+ (return . fromIntegral $ size)
44+getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
45+
46+getRequestBody :: Payload -> NC.RequestBody
47+getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
48+getRequestBody (PayloadH h off size) =
49+ NC.requestBodySource size $
50+ sourceHandleRange
51+ h
52+ (return . fromIntegral $ off)
53+ (return . fromIntegral $ size)
54+getRequestBody (PayloadC n src) = NC.requestBodySource n src
55+
56+mkStreamingPayload :: Payload -> Payload
57+mkStreamingPayload payload =
58+ case payload of
59+ PayloadBS bs ->
60+ PayloadC
61+ (fromIntegral $ BS.length bs)
62+ (C.sourceLazy $ LB.fromStrict bs)
63+ PayloadH h off len ->
64+ PayloadC len $
65+ sourceHandleRange
66+ h
67+ (return . fromIntegral $ off)
68+ (return . fromIntegral $ len)
69+ _ -> payload
70+
71+isStreamingPayload :: Payload -> Bool
72+isStreamingPayload (PayloadC _ _) = True
73+isStreamingPayload _ = False
74+
75+-- | Checks if the connect info is for Amazon S3.
76+isAWSConnectInfo :: ConnectInfo -> Bool
77+isAWSConnectInfo ci = ".amazonaws.com" `T.isSuffixOf` connectHost ci
78+
79+bucketHasPeriods :: Bucket -> Bool
80+bucketHasPeriods b = isJust $ T.find (== '.') b
src/Network/Minio/AdminAPI.hsnew file+711
@@ -0,0 +1,711 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.AdminAPI
18+ ( -- * MinIO Admin API
19+
20+ --------------------
21+
22+ -- | Provides MinIO admin API and related types. It is in
23+ -- experimental state.
24+ DriveInfo (..),
25+ ErasureInfo (..),
26+ Backend (..),
27+ ConnStats (..),
28+ HttpStats (..),
29+ ServerProps (..),
30+ CountNAvgTime (..),
31+ StorageClass (..),
32+ StorageInfo (..),
33+ SIData (..),
34+ ServerInfo (..),
35+ getServerInfo,
36+ HealOpts (..),
37+ HealResultItem (..),
38+ HealStatus (..),
39+ HealStartResp (..),
40+ startHeal,
41+ forceStartHeal,
42+ getHealStatus,
43+ SetConfigResult (..),
44+ NodeSummary (..),
45+ setConfig,
46+ getConfig,
47+ ServerVersion (..),
48+ ServiceStatus (..),
49+ serviceStatus,
50+ ServiceAction (..),
51+ serviceSendAction,
52+ )
53+where
54+
55+import Data.Aeson
56+ ( FromJSON,
57+ ToJSON,
58+ Value (Object),
59+ eitherDecode,
60+ object,
61+ pairs,
62+ parseJSON,
63+ toEncoding,
64+ toJSON,
65+ withObject,
66+ withText,
67+ (.:),
68+ (.:?),
69+ (.=),
70+ )
71+import qualified Data.Aeson as A
72+import Data.Aeson.Types (typeMismatch)
73+import qualified Data.ByteString as B
74+import qualified Data.ByteString.Lazy as LBS
75+import qualified Data.Text as T
76+import Data.Time (NominalDiffTime, getCurrentTime)
77+import Lib.Prelude
78+import Network.HTTP.Conduit (Response)
79+import qualified Network.HTTP.Conduit as NC
80+import qualified Network.HTTP.Types as HT
81+import Network.HTTP.Types.Header (hHost)
82+import Network.Minio.APICommon
83+import Network.Minio.Credentials
84+import Network.Minio.Data
85+import Network.Minio.Errors
86+import Network.Minio.Sign.V4
87+import Network.Minio.Utils
88+
89+data DriveInfo = DriveInfo
90+ { diUuid :: Text,
91+ diEndpoint :: Text,
92+ diState :: Text
93+ }
94+ deriving stock (Show, Eq)
95+
96+instance FromJSON DriveInfo where
97+ parseJSON = withObject "DriveInfo" $ \v ->
98+ DriveInfo
99+ <$> v
100+ .: "uuid"
101+ <*> v
102+ .: "endpoint"
103+ <*> v
104+ .: "state"
105+
106+data StorageClass = StorageClass
107+ { scParity :: Int,
108+ scData :: Int
109+ }
110+ deriving stock (Show, Eq)
111+
112+data ErasureInfo = ErasureInfo
113+ { eiOnlineDisks :: Int,
114+ eiOfflineDisks :: Int,
115+ eiStandard :: StorageClass,
116+ eiReducedRedundancy :: StorageClass,
117+ eiSets :: [[DriveInfo]]
118+ }
119+ deriving stock (Show, Eq)
120+
121+instance FromJSON ErasureInfo where
122+ parseJSON = withObject "ErasureInfo" $ \v -> do
123+ onlineDisks <- v .: "OnlineDisks"
124+ offlineDisks <- v .: "OfflineDisks"
125+ stdClass <-
126+ StorageClass
127+ <$> v
128+ .: "StandardSCData"
129+ <*> v
130+ .: "StandardSCParity"
131+ rrClass <-
132+ StorageClass
133+ <$> v
134+ .: "RRSCData"
135+ <*> v
136+ .: "RRSCParity"
137+ sets <- v .: "Sets"
138+ return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
139+
140+data Backend
141+ = BackendFS
142+ | BackendErasure ErasureInfo
143+ deriving stock (Show, Eq)
144+
145+instance FromJSON Backend where
146+ parseJSON = withObject "Backend" $ \v -> do
147+ typ <- v .: "Type"
148+ case typ :: Int of
149+ 1 -> return BackendFS
150+ 2 -> BackendErasure <$> parseJSON (Object v)
151+ _ -> typeMismatch "BackendType" (Object v)
152+
153+data ConnStats = ConnStats
154+ { csTransferred :: Int64,
155+ csReceived :: Int64
156+ }
157+ deriving stock (Show, Eq)
158+
159+instance FromJSON ConnStats where
160+ parseJSON = withObject "ConnStats" $ \v ->
161+ ConnStats
162+ <$> v
163+ .: "transferred"
164+ <*> v
165+ .: "received"
166+
167+data ServerProps = ServerProps
168+ { spUptime :: NominalDiffTime,
169+ spVersion :: Text,
170+ spCommitId :: Text,
171+ spRegion :: Text,
172+ spSqsArns :: [Text]
173+ }
174+ deriving stock (Show, Eq)
175+
176+instance FromJSON ServerProps where
177+ parseJSON = withObject "SIServer" $ \v -> do
178+ uptimeNs <- v .: "uptime"
179+ let uptime = uptimeNs / 1e9
180+ ver <- v .: "version"
181+ commitId <- v .: "commitID"
182+ region <- v .: "region"
183+ arn <- v .: "sqsARN"
184+ return $ ServerProps uptime ver commitId region arn
185+
186+data StorageInfo = StorageInfo
187+ { siUsed :: Int64,
188+ siBackend :: Backend
189+ }
190+ deriving stock (Show, Eq)
191+
192+instance FromJSON StorageInfo where
193+ parseJSON = withObject "StorageInfo" $ \v ->
194+ StorageInfo
195+ <$> v
196+ .: "Used"
197+ <*> v
198+ .: "Backend"
199+
200+data CountNAvgTime = CountNAvgTime
201+ { caCount :: Int64,
202+ caAvgDuration :: Text
203+ }
204+ deriving stock (Show, Eq)
205+
206+instance FromJSON CountNAvgTime where
207+ parseJSON = withObject "CountNAvgTime" $ \v ->
208+ CountNAvgTime
209+ <$> v
210+ .: "count"
211+ <*> v
212+ .: "avgDuration"
213+
214+data HttpStats = HttpStats
215+ { hsTotalHeads :: CountNAvgTime,
216+ hsSuccessHeads :: CountNAvgTime,
217+ hsTotalGets :: CountNAvgTime,
218+ hsSuccessGets :: CountNAvgTime,
219+ hsTotalPuts :: CountNAvgTime,
220+ hsSuccessPuts :: CountNAvgTime,
221+ hsTotalPosts :: CountNAvgTime,
222+ hsSuccessPosts :: CountNAvgTime,
223+ hsTotalDeletes :: CountNAvgTime,
224+ hsSuccessDeletes :: CountNAvgTime
225+ }
226+ deriving stock (Show, Eq)
227+
228+instance FromJSON HttpStats where
229+ parseJSON = withObject "HttpStats" $ \v ->
230+ HttpStats
231+ <$> v
232+ .: "totalHEADs"
233+ <*> v
234+ .: "successHEADs"
235+ <*> v
236+ .: "totalGETs"
237+ <*> v
238+ .: "successGETs"
239+ <*> v
240+ .: "totalPUTs"
241+ <*> v
242+ .: "successPUTs"
243+ <*> v
244+ .: "totalPOSTs"
245+ <*> v
246+ .: "successPOSTs"
247+ <*> v
248+ .: "totalDELETEs"
249+ <*> v
250+ .: "successDELETEs"
251+
252+data SIData = SIData
253+ { sdStorage :: StorageInfo,
254+ sdConnStats :: ConnStats,
255+ sdHttpStats :: HttpStats,
256+ sdProps :: ServerProps
257+ }
258+ deriving stock (Show, Eq)
259+
260+instance FromJSON SIData where
261+ parseJSON = withObject "SIData" $ \v ->
262+ SIData
263+ <$> v
264+ .: "storage"
265+ <*> v
266+ .: "network"
267+ <*> v
268+ .: "http"
269+ <*> v
270+ .: "server"
271+
272+data ServerInfo = ServerInfo
273+ { siError :: Text,
274+ siAddr :: Text,
275+ siData :: SIData
276+ }
277+ deriving stock (Show, Eq)
278+
279+instance FromJSON ServerInfo where
280+ parseJSON = withObject "ServerInfo" $ \v ->
281+ ServerInfo
282+ <$> v
283+ .: "error"
284+ <*> v
285+ .: "addr"
286+ <*> v
287+ .: "data"
288+
289+data ServerVersion = ServerVersion
290+ { svVersion :: Text,
291+ svCommitId :: Text
292+ }
293+ deriving stock (Show, Eq)
294+
295+instance FromJSON ServerVersion where
296+ parseJSON = withObject "ServerVersion" $ \v ->
297+ ServerVersion
298+ <$> v
299+ .: "version"
300+ <*> v
301+ .: "commitID"
302+
303+data ServiceStatus = ServiceStatus
304+ { ssVersion :: ServerVersion,
305+ ssUptime :: NominalDiffTime
306+ }
307+ deriving stock (Show, Eq)
308+
309+instance FromJSON ServiceStatus where
310+ parseJSON = withObject "ServiceStatus" $ \v -> do
311+ serverVersion <- v .: "serverVersion"
312+ uptimeNs <- v .: "uptime"
313+ let uptime = uptimeNs / 1e9
314+ return $ ServiceStatus serverVersion uptime
315+
316+data ServiceAction
317+ = ServiceActionRestart
318+ | ServiceActionStop
319+ deriving stock (Show, Eq)
320+
321+instance ToJSON ServiceAction where
322+ toJSON a = object ["action" .= serviceActionToText a]
323+
324+serviceActionToText :: ServiceAction -> Text
325+serviceActionToText a = case a of
326+ ServiceActionRestart -> "restart"
327+ ServiceActionStop -> "stop"
328+
329+adminPath :: ByteString
330+adminPath = "/minio/admin"
331+
332+data HealStartResp = HealStartResp
333+ { hsrClientToken :: Text,
334+ hsrClientAddr :: Text,
335+ hsrStartTime :: UTCTime
336+ }
337+ deriving stock (Show, Eq)
338+
339+instance FromJSON HealStartResp where
340+ parseJSON = withObject "HealStartResp" $ \v ->
341+ HealStartResp
342+ <$> v
343+ .: "clientToken"
344+ <*> v
345+ .: "clientAddress"
346+ <*> v
347+ .: "startTime"
348+
349+data HealOpts = HealOpts
350+ { hoRecursive :: Bool,
351+ hoDryRun :: Bool
352+ }
353+ deriving stock (Show, Eq)
354+
355+instance ToJSON HealOpts where
356+ toJSON (HealOpts r d) =
357+ object ["recursive" .= r, "dryRun" .= d]
358+ toEncoding (HealOpts r d) =
359+ pairs ("recursive" .= r <> "dryRun" .= d)
360+
361+instance FromJSON HealOpts where
362+ parseJSON = withObject "HealOpts" $ \v ->
363+ HealOpts
364+ <$> v
365+ .: "recursive"
366+ <*> v
367+ .: "dryRun"
368+
369+data HealItemType
370+ = HealItemMetadata
371+ | HealItemBucket
372+ | HealItemBucketMetadata
373+ | HealItemObject
374+ deriving stock (Show, Eq)
375+
376+instance FromJSON HealItemType where
377+ parseJSON = withText "HealItemType" $ \v -> case v of
378+ "metadata" -> return HealItemMetadata
379+ "bucket" -> return HealItemBucket
380+ "object" -> return HealItemObject
381+ "bucket-metadata" -> return HealItemBucketMetadata
382+ _ -> typeMismatch "HealItemType" (A.String v)
383+
384+data NodeSummary = NodeSummary
385+ { nsName :: Text,
386+ nsErrSet :: Bool,
387+ nsErrMessage :: Text
388+ }
389+ deriving stock (Show, Eq)
390+
391+instance FromJSON NodeSummary where
392+ parseJSON = withObject "NodeSummary" $ \v ->
393+ NodeSummary
394+ <$> v
395+ .: "name"
396+ <*> v
397+ .: "errSet"
398+ <*> v
399+ .: "errMsg"
400+
401+data SetConfigResult = SetConfigResult
402+ { scrStatus :: Bool,
403+ scrNodeSummary :: [NodeSummary]
404+ }
405+ deriving stock (Show, Eq)
406+
407+instance FromJSON SetConfigResult where
408+ parseJSON = withObject "SetConfigResult" $ \v ->
409+ SetConfigResult
410+ <$> v
411+ .: "status"
412+ <*> v
413+ .: "nodeResults"
414+
415+data HealResultItem = HealResultItem
416+ { hriResultIdx :: Int,
417+ hriType :: HealItemType,
418+ hriBucket :: Bucket,
419+ hriObject :: Object,
420+ hriDetail :: Text,
421+ hriParityBlocks :: Maybe Int,
422+ hriDataBlocks :: Maybe Int,
423+ hriDiskCount :: Int,
424+ hriSetCount :: Int,
425+ hriObjectSize :: Int,
426+ hriBefore :: [DriveInfo],
427+ hriAfter :: [DriveInfo]
428+ }
429+ deriving stock (Show, Eq)
430+
431+instance FromJSON HealResultItem where
432+ parseJSON = withObject "HealResultItem" $ \v ->
433+ HealResultItem
434+ <$> v
435+ .: "resultId"
436+ <*> v
437+ .: "type"
438+ <*> v
439+ .: "bucket"
440+ <*> v
441+ .: "object"
442+ <*> v
443+ .: "detail"
444+ <*> v
445+ .:? "parityBlocks"
446+ <*> v
447+ .:? "dataBlocks"
448+ <*> v
449+ .: "diskCount"
450+ <*> v
451+ .: "setCount"
452+ <*> v
453+ .: "objectSize"
454+ <*> ( do
455+ before <- v .: "before"
456+ before .: "drives"
457+ )
458+ <*> ( do
459+ after <- v .: "after"
460+ after .: "drives"
461+ )
462+
463+data HealStatus = HealStatus
464+ { hsSummary :: Text,
465+ hsStartTime :: UTCTime,
466+ hsSettings :: HealOpts,
467+ hsNumDisks :: Int,
468+ hsFailureDetail :: Maybe Text,
469+ hsItems :: Maybe [HealResultItem]
470+ }
471+ deriving stock (Show, Eq)
472+
473+instance FromJSON HealStatus where
474+ parseJSON = withObject "HealStatus" $ \v ->
475+ HealStatus
476+ <$> v
477+ .: "Summary"
478+ <*> v
479+ .: "StartTime"
480+ <*> v
481+ .: "Settings"
482+ <*> v
483+ .: "NumDisks"
484+ <*> v
485+ .:? "Detail"
486+ <*> v
487+ .: "Items"
488+
489+healPath :: Maybe Bucket -> Maybe Text -> ByteString
490+healPath bucket prefix = do
491+ if isJust bucket
492+ then
493+ encodeUtf8 $
494+ "v1/heal/"
495+ <> fromMaybe "" bucket
496+ <> "/"
497+ <> fromMaybe "" prefix
498+ else encodeUtf8 ("v1/heal/" :: Text)
499+
500+-- | Get server version and uptime.
501+serviceStatus :: Minio ServiceStatus
502+serviceStatus = do
503+ rsp <-
504+ executeAdminRequest
505+ AdminReqInfo
506+ { ariMethod = HT.methodGet,
507+ ariPayload = PayloadBS B.empty,
508+ ariPayloadHash = Nothing,
509+ ariPath = "v1/service",
510+ ariHeaders = [],
511+ ariQueryParams = []
512+ }
513+
514+ let rspBS = NC.responseBody rsp
515+ case eitherDecode rspBS of
516+ Right ss -> return ss
517+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
518+
519+-- | Send service restart or stop action to MinIO server.
520+serviceSendAction :: ServiceAction -> Minio ()
521+serviceSendAction action = do
522+ let payload = PayloadBS $ LBS.toStrict $ A.encode action
523+ void $
524+ executeAdminRequest
525+ AdminReqInfo
526+ { ariMethod = HT.methodPost,
527+ ariPayload = payload,
528+ ariPayloadHash = Nothing,
529+ ariPath = "v1/service",
530+ ariHeaders = [],
531+ ariQueryParams = []
532+ }
533+
534+-- | Get the current config file from server.
535+getConfig :: Minio ByteString
536+getConfig = do
537+ rsp <-
538+ executeAdminRequest
539+ AdminReqInfo
540+ { ariMethod = HT.methodGet,
541+ ariPayload = PayloadBS B.empty,
542+ ariPayloadHash = Nothing,
543+ ariPath = "v1/config",
544+ ariHeaders = [],
545+ ariQueryParams = []
546+ }
547+ return $ LBS.toStrict $ NC.responseBody rsp
548+
549+-- | Set a new config to the server.
550+setConfig :: ByteString -> Minio SetConfigResult
551+setConfig config = do
552+ rsp <-
553+ executeAdminRequest
554+ AdminReqInfo
555+ { ariMethod = HT.methodPut,
556+ ariPayload = PayloadBS config,
557+ ariPayloadHash = Nothing,
558+ ariPath = "v1/config",
559+ ariHeaders = [],
560+ ariQueryParams = []
561+ }
562+
563+ let rspBS = NC.responseBody rsp
564+ case eitherDecode rspBS of
565+ Right scr -> return scr
566+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
567+
568+-- | Get the progress of currently running heal task, this API should be
569+-- invoked right after `startHeal`. `token` is obtained after `startHeal`
570+-- which should be used to get the heal status.
571+getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
572+getHealStatus bucket prefix token = do
573+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
574+ let qparams = HT.queryTextToQuery [("clientToken", Just token)]
575+ rsp <-
576+ executeAdminRequest
577+ AdminReqInfo
578+ { ariMethod = HT.methodPost,
579+ ariPayload = PayloadBS B.empty,
580+ ariPayloadHash = Nothing,
581+ ariPath = healPath bucket prefix,
582+ ariHeaders = [],
583+ ariQueryParams = qparams
584+ }
585+ let rspBS = NC.responseBody rsp
586+ case eitherDecode rspBS of
587+ Right hs -> return hs
588+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
589+
590+doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
591+doHeal bucket prefix opts forceStart = do
592+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
593+ let payload = PayloadBS $ LBS.toStrict $ A.encode opts
594+ let qparams =
595+ bool
596+ []
597+ (HT.queryTextToQuery [("forceStart", Just "true")])
598+ forceStart
599+
600+ rsp <-
601+ executeAdminRequest
602+ AdminReqInfo
603+ { ariMethod = HT.methodPost,
604+ ariPayload = payload,
605+ ariPayloadHash = Nothing,
606+ ariPath = healPath bucket prefix,
607+ ariHeaders = [],
608+ ariQueryParams = qparams
609+ }
610+
611+ let rspBS = NC.responseBody rsp
612+ case eitherDecode rspBS of
613+ Right hsr -> return hsr
614+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
615+
616+-- | Start a heal sequence that scans data under given (possible empty)
617+-- `bucket` and `prefix`. The `recursive` bool turns on recursive
618+-- traversal under the given path. `dryRun` does not mutate on-disk data,
619+-- but performs data validation. Two heal sequences on overlapping paths
620+-- may not be initiated. The progress of a heal should be followed using
621+-- the `HealStatus` API. The server accumulates results of the heal
622+-- traversal and waits for the client to receive and acknowledge
623+-- them using the status API
624+startHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
625+startHeal bucket prefix opts = doHeal bucket prefix opts False
626+
627+-- | Similar to start a heal sequence, but force start a new heal sequence
628+-- even if an active heal is under progress.
629+forceStartHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
630+forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
631+
632+-- | Fetches information for all cluster nodes, such as server
633+-- properties, storage information, network statistics, etc.
634+getServerInfo :: Minio [ServerInfo]
635+getServerInfo = do
636+ rsp <-
637+ executeAdminRequest
638+ AdminReqInfo
639+ { ariMethod = HT.methodGet,
640+ ariPayload = PayloadBS B.empty,
641+ ariPayloadHash = Nothing,
642+ ariPath = "v1/info",
643+ ariHeaders = [],
644+ ariQueryParams = []
645+ }
646+ let rspBS = NC.responseBody rsp
647+ case eitherDecode rspBS of
648+ Right si -> return si
649+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
650+
651+executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
652+executeAdminRequest ari = do
653+ req <- buildAdminRequest ari
654+ mgr <- asks mcConnManager
655+ httpLbs req mgr
656+
657+buildAdminRequest :: AdminReqInfo -> Minio NC.Request
658+buildAdminRequest areq = do
659+ ci <- asks mcConnInfo
660+ sha256Hash <-
661+ if connectIsSecure ci
662+ then -- if secure connection
663+ return "UNSIGNED-PAYLOAD"
664+ else -- otherwise compute sha256
665+ getPayloadSHA256Hash (ariPayload areq)
666+
667+ timeStamp <- liftIO getCurrentTime
668+
669+ mgr <- asks mcConnManager
670+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
671+
672+ let hostHeader = (hHost, getHostAddr ci)
673+ newAreq =
674+ areq
675+ { ariPayloadHash = Just sha256Hash,
676+ ariHeaders =
677+ hostHeader
678+ : sha256Header sha256Hash
679+ : ariHeaders areq
680+ }
681+ signReq = toRequest ci newAreq
682+ sp =
683+ SignParams
684+ (coerce $ cvAccessKey cv)
685+ (coerce $ cvSecretKey cv)
686+ (coerce $ cvSessionToken cv)
687+ ServiceS3
688+ timeStamp
689+ Nothing
690+ Nothing
691+ (ariPayloadHash newAreq)
692+ signHeaders = signV4 sp signReq
693+
694+ -- Update signReq with Authorization header containing v4 signature
695+ return
696+ signReq
697+ { NC.requestHeaders = ariHeaders newAreq ++ signHeaders
698+ }
699+ where
700+ toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
701+ toRequest ci aReq =
702+ NC.defaultRequest
703+ { NC.method = ariMethod aReq,
704+ NC.secure = connectIsSecure ci,
705+ NC.host = encodeUtf8 $ connectHost ci,
706+ NC.port = connectPort ci,
707+ NC.path = B.intercalate "/" [adminPath, ariPath aReq],
708+ NC.requestHeaders = ariHeaders aReq,
709+ NC.queryString = HT.renderQuery False $ ariQueryParams aReq,
710+ NC.requestBody = getRequestBody (ariPayload aReq)
711+ }
src/Network/Minio/CopyObject.hsnew file+106
@@ -0,0 +1,106 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.CopyObject where
18+
19+import qualified Data.List as List
20+import Lib.Prelude
21+import Network.Minio.Data
22+import Network.Minio.Errors
23+import Network.Minio.S3API
24+import Network.Minio.Utils
25+
26+-- | Copy an object using single or multipart copy strategy.
27+copyObjectInternal ::
28+ Bucket ->
29+ Object ->
30+ SourceInfo ->
31+ Minio ETag
32+copyObjectInternal b' o srcInfo = do
33+ let sBucket = srcBucket srcInfo
34+ sObject = srcObject srcInfo
35+
36+ -- get source object size with a head request
37+ oi <- headObject sBucket sObject []
38+ let srcSize = oiSize oi
39+
40+ -- check that byte offsets are valid if specified in cps
41+ let rangeMay = srcRange srcInfo
42+ range = maybe (0, srcSize) identity rangeMay
43+ startOffset = fst range
44+ endOffset = snd range
45+
46+ when
47+ ( isJust rangeMay
48+ && ( (startOffset < 0)
49+ || (endOffset < startOffset)
50+ || (endOffset >= srcSize)
51+ )
52+ )
53+ $ throwIO
54+ $ MErrVInvalidSrcObjByteRange range
55+
56+ -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
57+ -- 2. If startOffset /= 0 use multipart copy
58+ let destSize =
59+ (\(a, b) -> b - a + 1) $
60+ maybe (0, srcSize - 1) identity rangeMay
61+
62+ if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
63+ then multiPartCopyObject b' o srcInfo srcSize
64+ else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} []
65+
66+-- | Given the input byte range of the source object, compute the
67+-- splits for a multipart copy object procedure. Minimum part size
68+-- used is minPartSize.
69+selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
70+selectCopyRanges (st, end) =
71+ zip pns $
72+ zipWith (\x y -> (st + x, st + x + y - 1)) startOffsets partSizes
73+ where
74+ size = end - st + 1
75+ (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
76+
77+-- | Perform a multipart copy object action. Since we cannot verify
78+-- existing parts based on the source object, there is no resuming
79+-- copy action support.
80+multiPartCopyObject ::
81+ Bucket ->
82+ Object ->
83+ SourceInfo ->
84+ Int64 ->
85+ Minio ETag
86+multiPartCopyObject b o cps srcSize = do
87+ uid <- newMultipartUpload b o []
88+
89+ let byteRange = maybe (0, srcSize - 1) identity $ srcRange cps
90+ partRanges = selectCopyRanges byteRange
91+ partSources =
92+ map
93+ (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end)}))
94+ partRanges
95+ dstInfo = defaultDestinationInfo {dstBucket = b, dstObject = o}
96+
97+ copiedParts <-
98+ limitedMapConcurrently
99+ 10
100+ ( \(pn, cps') -> do
101+ (etag, _) <- copyObjectPart dstInfo cps' uid pn []
102+ return (pn, etag)
103+ )
104+ partSources
105+
106+ completeMultipartUpload b o uid copiedParts
src/Network/Minio/Credentials.hsnew file+77
@@ -0,0 +1,77 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.Credentials
18+ ( CredentialValue (..),
19+ credentialValueText,
20+ STSCredentialProvider (..),
21+ AccessKey (..),
22+ SecretKey (..),
23+ SessionToken (..),
24+ ExpiryTime (..),
25+ STSCredentialStore,
26+ initSTSCredential,
27+ getSTSCredential,
28+ Creds (..),
29+ getCredential,
30+ Endpoint,
31+
32+ -- * STS Assume Role
33+ defaultSTSAssumeRoleOptions,
34+ STSAssumeRole (..),
35+ STSAssumeRoleOptions (..),
36+ )
37+where
38+
39+import Data.Time (diffUTCTime, getCurrentTime)
40+import qualified Network.HTTP.Client as NC
41+import Network.Minio.Credentials.AssumeRole
42+import Network.Minio.Credentials.Types
43+import qualified UnliftIO.MVar as M
44+
45+data STSCredentialStore = STSCredentialStore
46+ { cachedCredentials :: M.MVar (CredentialValue, ExpiryTime),
47+ refreshAction :: Endpoint -> NC.Manager -> IO (CredentialValue, ExpiryTime)
48+ }
49+
50+initSTSCredential :: (STSCredentialProvider p) => p -> IO STSCredentialStore
51+initSTSCredential p = do
52+ let action = retrieveSTSCredentials p
53+ -- start with dummy credential, so that refresh happens for first request.
54+ now <- getCurrentTime
55+ mvar <- M.newMVar (CredentialValue mempty mempty mempty, coerce now)
56+ return $
57+ STSCredentialStore
58+ { cachedCredentials = mvar,
59+ refreshAction = action
60+ }
61+
62+getSTSCredential :: STSCredentialStore -> Endpoint -> NC.Manager -> IO (CredentialValue, Bool)
63+getSTSCredential store ep mgr = M.modifyMVar (cachedCredentials store) $ \cc@(v, expiry) -> do
64+ now <- getCurrentTime
65+ if diffUTCTime now (coerce expiry) > 0
66+ then do
67+ res <- refreshAction store ep mgr
68+ return (res, (fst res, True))
69+ else return (cc, (v, False))
70+
71+data Creds
72+ = CredsStatic CredentialValue
73+ | CredsSTS STSCredentialStore
74+
75+getCredential :: Creds -> Endpoint -> NC.Manager -> IO CredentialValue
76+getCredential (CredsStatic v) _ _ = return v
77+getCredential (CredsSTS s) ep mgr = fst <$> getSTSCredential s ep mgr
src/Network/Minio/Credentials/AssumeRole.hsnew file+266
@@ -0,0 +1,266 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.Credentials.AssumeRole where
18+
19+import qualified Data.ByteArray as BA
20+import qualified Data.ByteString.Lazy as LB
21+import qualified Data.Text as T
22+import qualified Data.Time as Time
23+import Data.Time.Units (Second)
24+import Lib.Prelude (UTCTime, throwIO)
25+import Network.HTTP.Client (RequestBody (RequestBodyBS))
26+import qualified Network.HTTP.Client as NC
27+import Network.HTTP.Types (hContentType, methodPost, renderSimpleQuery)
28+import Network.HTTP.Types.Header (hHost)
29+import Network.Minio.Credentials.Types
30+import Network.Minio.Data.Crypto (hashSHA256)
31+import Network.Minio.Errors (MErrV (..))
32+import Network.Minio.Sign.V4
33+import Network.Minio.Utils (getHostHeader, httpLbs)
34+import Network.Minio.XmlCommon
35+import Text.XML.Cursor hiding (bool)
36+
37+stsVersion :: ByteString
38+stsVersion = "2011-06-15"
39+
40+defaultDurationSeconds :: Second
41+defaultDurationSeconds = 3600
42+
43+-- | Assume Role API argument.
44+--
45+-- @since 1.7.0
46+data STSAssumeRole = STSAssumeRole
47+ { -- | Credentials to use in the AssumeRole STS API.
48+ sarCredentials :: CredentialValue,
49+ -- | Optional settings.
50+ sarOptions :: STSAssumeRoleOptions
51+ }
52+
53+-- | Options for STS Assume Role API.
54+data STSAssumeRoleOptions = STSAssumeRoleOptions
55+ { -- | STS endpoint to which the request will be made. For MinIO, this is the
56+ -- same as the server endpoint. For AWS, this has to be the Security Token
57+ -- Service endpoint. If using with 'setSTSCredential', this option can be
58+ -- left as 'Nothing' and the endpoint in 'ConnectInfo' will be used.
59+ saroEndpoint :: Maybe Text,
60+ -- | Desired validity for the generated credentials.
61+ saroDurationSeconds :: Maybe Second,
62+ -- | IAM policy to apply for the generated credentials.
63+ saroPolicyJSON :: Maybe ByteString,
64+ -- | Location is usually required for AWS.
65+ saroLocation :: Maybe Text,
66+ saroRoleARN :: Maybe Text,
67+ saroRoleSessionName :: Maybe Text
68+ }
69+
70+-- | Default STS Assume Role options - all options are Nothing, except for
71+-- duration which is set to 1 hour.
72+defaultSTSAssumeRoleOptions :: STSAssumeRoleOptions
73+defaultSTSAssumeRoleOptions =
74+ STSAssumeRoleOptions
75+ { saroEndpoint = Nothing,
76+ saroDurationSeconds = Just 3600,
77+ saroPolicyJSON = Nothing,
78+ saroLocation = Nothing,
79+ saroRoleARN = Nothing,
80+ saroRoleSessionName = Nothing
81+ }
82+
83+data AssumeRoleCredentials = AssumeRoleCredentials
84+ { arcCredentials :: CredentialValue,
85+ arcExpiration :: UTCTime
86+ }
87+ deriving stock (Show, Eq)
88+
89+data AssumeRoleResult = AssumeRoleResult
90+ { arrSourceIdentity :: Text,
91+ arrAssumedRoleArn :: Text,
92+ arrAssumedRoleId :: Text,
93+ arrRoleCredentials :: AssumeRoleCredentials
94+ }
95+ deriving stock (Show, Eq)
96+
97+-- | parseSTSAssumeRoleResult parses an XML response of the following form:
98+--
99+-- <AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
100+-- <AssumeRoleResult>
101+-- <SourceIdentity>Alice</SourceIdentity>
102+-- <AssumedRoleUser>
103+-- <Arn>arn:aws:sts::123456789012:assumed-role/demo/TestAR</Arn>
104+-- <AssumedRoleId>ARO123EXAMPLE123:TestAR</AssumedRoleId>
105+-- </AssumedRoleUser>
106+-- <Credentials>
107+-- <AccessKeyId>ASIAIOSFODNN7EXAMPLE</AccessKeyId>
108+-- <SecretAccessKey>wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY</SecretAccessKey>
109+-- <SessionToken>
110+-- AQoDYXdzEPT//////////wEXAMPLEtc764bNrC9SAPBSM22wDOk4x4HIZ8j4FZTwdQW
111+-- LWsKWHGBuFqwAeMicRXmxfpSPfIeoIYRqTflfKD8YUuwthAx7mSEI/qkPpKPi/kMcGd
112+-- QrmGdeehM4IC1NtBmUpp2wUE8phUZampKsburEDy0KPkyQDYwT7WZ0wq5VSXDvp75YU
113+-- 9HFvlRd8Tx6q6fE8YQcHNVXAkiY9q6d+xo0rKwT38xVqr7ZD0u0iPPkUL64lIZbqBAz
114+-- +scqKmlzm8FDrypNC9Yjc8fPOLn9FX9KSYvKTr4rvx3iSIlTJabIQwj2ICCR/oLxBA==
115+-- </SessionToken>
116+-- <Expiration>2019-11-09T13:34:41Z</Expiration>
117+-- </Credentials>
118+-- <PackedPolicySize>6</PackedPolicySize>
119+-- </AssumeRoleResult>
120+-- <ResponseMetadata>
121+-- <RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
122+-- </ResponseMetadata>
123+-- </AssumeRoleResponse>
124+parseSTSAssumeRoleResult :: (MonadIO m) => ByteString -> Text -> m AssumeRoleResult
125+parseSTSAssumeRoleResult xmldata namespace = do
126+ r <- parseRoot $ LB.fromStrict xmldata
127+ let s3Elem' = s3Elem namespace
128+ sourceIdentity =
129+ T.concat $
130+ r
131+ $/ s3Elem' "AssumeRoleResult"
132+ &/ s3Elem' "SourceIdentity"
133+ &/ content
134+ roleArn =
135+ T.concat $
136+ r
137+ $/ s3Elem' "AssumeRoleResult"
138+ &/ s3Elem' "AssumedRoleUser"
139+ &/ s3Elem' "Arn"
140+ &/ content
141+ roleId =
142+ T.concat $
143+ r
144+ $/ s3Elem' "AssumeRoleResult"
145+ &/ s3Elem' "AssumedRoleUser"
146+ &/ s3Elem' "AssumedRoleId"
147+ &/ content
148+
149+ convSB :: Text -> BA.ScrubbedBytes
150+ convSB = BA.convert . (encodeUtf8 :: Text -> ByteString)
151+
152+ credsInfo = do
153+ cr <-
154+ maybe (Left $ MErrVXmlParse "No Credentials Element found") Right $
155+ listToMaybe $
156+ r $/ s3Elem' "AssumeRoleResult" &/ s3Elem' "Credentials"
157+ let cur = fromNode $ node cr
158+ return
159+ ( CredentialValue
160+ { cvAccessKey =
161+ coerce $
162+ T.concat $
163+ cur $/ s3Elem' "AccessKeyId" &/ content,
164+ cvSecretKey =
165+ coerce $
166+ convSB $
167+ T.concat $
168+ cur
169+ $/ s3Elem' "SecretAccessKey"
170+ &/ content,
171+ cvSessionToken =
172+ Just $
173+ coerce $
174+ convSB $
175+ T.concat $
176+ cur
177+ $/ s3Elem' "SessionToken"
178+ &/ content
179+ },
180+ T.concat $ cur $/ s3Elem' "Expiration" &/ content
181+ )
182+ creds <- either throwIO pure credsInfo
183+ expiry <- parseS3XMLTime $ snd creds
184+ let roleCredentials =
185+ AssumeRoleCredentials
186+ { arcCredentials = fst creds,
187+ arcExpiration = expiry
188+ }
189+ return
190+ AssumeRoleResult
191+ { arrSourceIdentity = sourceIdentity,
192+ arrAssumedRoleArn = roleArn,
193+ arrAssumedRoleId = roleId,
194+ arrRoleCredentials = roleCredentials
195+ }
196+
197+instance STSCredentialProvider STSAssumeRole where
198+ getSTSEndpoint = saroEndpoint . sarOptions
199+ retrieveSTSCredentials sar (host', port', isSecure') mgr = do
200+ -- Assemble STS request
201+ let requiredParams =
202+ [ ("Action", "AssumeRole"),
203+ ("Version", stsVersion)
204+ ]
205+ opts = sarOptions sar
206+
207+ durSecs :: Int =
208+ fromIntegral $
209+ fromMaybe defaultDurationSeconds $
210+ saroDurationSeconds opts
211+ otherParams =
212+ [ ("RoleArn",) . encodeUtf8 <$> saroRoleARN opts,
213+ ("RoleSessionName",) . encodeUtf8 <$> saroRoleSessionName opts,
214+ Just ("DurationSeconds", show durSecs),
215+ ("Policy",) <$> saroPolicyJSON opts
216+ ]
217+ parameters = requiredParams ++ catMaybes otherParams
218+ (host, port, isSecure) =
219+ case getSTSEndpoint sar of
220+ Just ep ->
221+ let endPt = NC.parseRequest_ $ toString ep
222+ in (NC.host endPt, NC.port endPt, NC.secure endPt)
223+ Nothing -> (host', port', isSecure')
224+ reqBody = renderSimpleQuery False parameters
225+ req =
226+ NC.defaultRequest
227+ { NC.host = host,
228+ NC.port = port,
229+ NC.secure = isSecure,
230+ NC.method = methodPost,
231+ NC.requestHeaders =
232+ [ (hHost, getHostHeader (host, port)),
233+ (hContentType, "application/x-www-form-urlencoded")
234+ ],
235+ NC.requestBody = RequestBodyBS reqBody
236+ }
237+
238+ -- Sign the STS request.
239+ timeStamp <- liftIO Time.getCurrentTime
240+ let sp =
241+ SignParams
242+ { spAccessKey = coerce $ cvAccessKey $ sarCredentials sar,
243+ spSecretKey = coerce $ cvSecretKey $ sarCredentials sar,
244+ spSessionToken = coerce $ cvSessionToken $ sarCredentials sar,
245+ spService = ServiceSTS,
246+ spTimeStamp = timeStamp,
247+ spRegion = saroLocation opts,
248+ spExpirySecs = Nothing,
249+ spPayloadHash = Just $ hashSHA256 reqBody
250+ }
251+ signHeaders = signV4 sp req
252+ signedReq =
253+ req
254+ { NC.requestHeaders = NC.requestHeaders req ++ signHeaders
255+ }
256+
257+ -- Make the STS request
258+ resp <- httpLbs signedReq mgr
259+ result <-
260+ parseSTSAssumeRoleResult
261+ (toStrict $ NC.responseBody resp)
262+ "https://sts.amazonaws.com/doc/2011-06-15/"
263+ return
264+ ( arcCredentials $ arrRoleCredentials result,
265+ coerce $ arcExpiration $ arrRoleCredentials result
266+ )
src/Network/Minio/Credentials/Types.hsnew file+90
@@ -0,0 +1,90 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17+{-# LANGUAGE StrictData #-}
18+
19+module Network.Minio.Credentials.Types where
20+
21+import qualified Data.ByteArray as BA
22+import Lib.Prelude (UTCTime)
23+import qualified Network.HTTP.Client as NC
24+
25+-- | Access Key type.
26+newtype AccessKey = AccessKey {unAccessKey :: Text}
27+ deriving stock (Show)
28+ deriving newtype (Eq, IsString, Semigroup, Monoid)
29+
30+-- | Secret Key type - has a show instance that does not print the value.
31+newtype SecretKey = SecretKey {unSecretKey :: BA.ScrubbedBytes}
32+ deriving stock (Show)
33+ deriving newtype (Eq, IsString, Semigroup, Monoid)
34+
35+-- | Session Token type - has a show instance that does not print the value.
36+newtype SessionToken = SessionToken {unSessionToken :: BA.ScrubbedBytes}
37+ deriving stock (Show)
38+ deriving newtype (Eq, IsString, Semigroup, Monoid)
39+
40+-- | Object storage credential data type. It has support for the optional
41+-- [SessionToken](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html)
42+-- for using temporary credentials requested via STS.
43+--
44+-- The show instance for this type does not print the value of secrets for
45+-- security.
46+--
47+-- @since 1.7.0
48+data CredentialValue = CredentialValue
49+ { cvAccessKey :: AccessKey,
50+ cvSecretKey :: SecretKey,
51+ cvSessionToken :: Maybe SessionToken
52+ }
53+ deriving stock (Eq, Show)
54+
55+scrubbedToText :: BA.ScrubbedBytes -> Text
56+scrubbedToText =
57+ let b2t :: ByteString -> Text
58+ b2t = decodeUtf8
59+ s2b :: BA.ScrubbedBytes -> ByteString
60+ s2b = BA.convert
61+ in b2t . s2b
62+
63+-- | Convert a 'CredentialValue' to a text tuple. Use this to output the
64+-- credential to files or other programs.
65+credentialValueText :: CredentialValue -> (Text, Text, Maybe Text)
66+credentialValueText cv =
67+ ( coerce $ cvAccessKey cv,
68+ (scrubbedToText . coerce) $ cvSecretKey cv,
69+ scrubbedToText . coerce <$> cvSessionToken cv
70+ )
71+
72+-- | Endpoint represented by host, port and TLS enabled flag.
73+type Endpoint = (ByteString, Int, Bool)
74+
75+-- | Typeclass for STS credential providers.
76+--
77+-- @since 1.7.0
78+class STSCredentialProvider p where
79+ retrieveSTSCredentials ::
80+ p ->
81+ -- | STS Endpoint (host, port, isSecure)
82+ Endpoint ->
83+ NC.Manager ->
84+ IO (CredentialValue, ExpiryTime)
85+ getSTSEndpoint :: p -> Maybe Text
86+
87+-- | 'ExpiryTime' represents a time at which a credential expires.
88+newtype ExpiryTime = ExpiryTime {unExpiryTime :: UTCTime}
89+ deriving stock (Show)
90+ deriving newtype (Eq)
src/Network/Minio/Data.hs+1038−330
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,141 +13,276 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16+{-# LANGUAGE CPP #-}
17+{-# LANGUAGE GADTs #-}
18+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
19+{-# LANGUAGE StrictData #-}
20+{-# LANGUAGE TypeFamilies #-}
16
21
17−{-# LANGUAGE GeneralizedNewtypeDeriving, TypeFamilies #-}
18module Network.Minio.Data where
22module Network.Minio.Data where
19
23
20−import Control.Monad.Base
24+import qualified Conduit as C
21−import qualified Control.Monad.Catch as MC
25+import qualified Control.Concurrent.MVar as M
22−import Control.Monad.Trans.Control
26+import Control.Monad.Trans.Except (throwE)
23−import Control.Monad.Trans.Resource
27+import Control.Monad.Trans.Resource
24−
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
25import qualified Data.ByteString as B
36import qualified Data.ByteString as B
37+import qualified Data.ByteString.Lazy as LB
26−import Data.Default (Default(..))
38+import Data.Default.Class (def)
39+import qualified Data.HashMap.Strict as H
40+import qualified Data.Ini as Ini
27−import qualified Data.Map as Map
41+import qualified Data.List as List
28import qualified Data.Text as T
42import qualified Data.Text as T
43+import qualified Data.Text.Encoding as TE
29−import Data.Time (formatTime, defaultTimeLocale)
44+import Data.Time (defaultTimeLocale, formatTime)
45+import Lib.Prelude (UTCTime, throwIO)
46+import qualified Network.Connection as Conn
30−import Network.HTTP.Client (defaultManagerSettings)
47+import Network.HTTP.Client (defaultManagerSettings)
48+import qualified Network.HTTP.Client.TLS as TLS
31import qualified Network.HTTP.Conduit as NC
49import qualified Network.HTTP.Conduit as NC
32−import Network.HTTP.Types (Method, Header, Query)
50+import Network.HTTP.Types
51+ ( ByteRange,
52+ Header,
53+ Method,
54+ Query,
55+ hRange,
56+ )
33import qualified Network.HTTP.Types as HT
57import 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)
34−import Network.Minio.Errors
64+import Network.Minio.Errors
35−import Text.XML
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
36
78
37−import GHC.Show (Show(..))
79+-- | minimum size of parts used in multipart operations.
80+minPartSize :: Int64
81+minPartSize = 64 * oneMiB
38
82
39−import Lib.Prelude
83+oneMiB :: Int64
84+oneMiB = 1024 * 1024
40
85
86+-- | maximum number of parts that can be uploaded for a single object.
87+maxMultipartParts :: Int64
88+maxMultipartParts = 10000
41
89
42-- TODO: Add a type which provides typed constants for region. this
90-- TODO: Add a type which provides typed constants for region. this
43-- type should have a IsString instance to infer the appropriate
91-- type should have a IsString instance to infer the appropriate
44-- constant.
92-- constant.
93+
45-- | awsRegionMap - library constant
94-- | awsRegionMap - library constant
46−awsRegionMap :: Map.Map Text Text
95+awsRegionMap :: H.HashMap Text Text
47−awsRegionMap = Map.fromList [
96+awsRegionMap =
97+ H.fromList
48− ("us-east-1", "s3.amazonaws.com")
98+ [ ("us-east-1", "s3.us-east-1.amazonaws.com"),
49− , ("us-east-2", "s3-us-east-2.amazonaws.com")
99+ ("us-east-2", "s3.us-east-2.amazonaws.com"),
50− , ("us-west-1", "s3-us-west-1.amazonaws.com")
100+ ("us-west-1", "s3.us-west-1.amazonaws.com"),
51− , ("us-east-2", "s3-us-west-2.amazonaws.com")
101+ ("us-west-2", "s3.us-west-2.amazonaws.com"),
52− , ("ca-central-1", "s3-ca-central-1.amazonaws.com")
102+ ("ca-central-1", "s3.ca-central-1.amazonaws.com"),
53− , ("ap-south-1", "s3-ap-south-1.amazonaws.com")
103+ ("ap-south-1", "s3.ap-south-1.amazonaws.com"),
104+ ("ap-south-2", "s3.ap-south-2.amazonaws.com"),
54− , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
105+ ("ap-northeast-1", "s3.ap-northeast-1.amazonaws.com"),
55− , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
106+ ("ap-northeast-2", "s3.ap-northeast-2.amazonaws.com"),
107+ ("ap-northeast-3", "s3.ap-northeast-3.amazonaws.com"),
56− , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
108+ ("ap-southeast-1", "s3.ap-southeast-1.amazonaws.com"),
57− , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
109+ ("ap-southeast-2", "s3.ap-southeast-2.amazonaws.com"),
110+ ("ap-southeast-3", "s3.ap-southeast-3.amazonaws.com"),
58− , ("eu-west-1", "s3-eu-west-1.amazonaws.com")
111+ ("eu-west-1", "s3.eu-west-1.amazonaws.com"),
59− , ("eu-west-2", "s3-eu-west-2.amazonaws.com")
112+ ("eu-west-2", "s3.eu-west-2.amazonaws.com"),
113+ ("eu-west-3", "s3.eu-west-3.amazonaws.com"),
60− , ("eu-central-1", "s3-eu-central-1.amazonaws.com")
114+ ("eu-central-1", "s3.eu-central-1.amazonaws.com"),
115+ ("eu-central-2", "s3.eu-central-2.amazonaws.com"),
116+ ("eu-south-1", "s3.eu-south-1.amazonaws.com"),
61− , ("sa-east-1", "s3-sa-east-1.amazonaws.com")
117+ ("eu-south-2", "s3.eu-south-2.amazonaws.com"),
118+ ("af-south-1", "s3.af-south-1.amazonaws.com"),
119+ ("ap-east-1", "s3.ap-east-1.amazonaws.com"),
120+ ("cn-north-1", "s3.cn-north-1.amazonaws.com.cn"),
121+ ("cn-northwest-1", "s3.cn-northwest-1.amazonaws.com.cn"),
122+ ("eu-north-1", "s3.eu-north-1.amazonaws.com"),
123+ ("me-south-1", "s3.me-south-1.amazonaws.com"),
124+ ("me-central-1", "s3.me-central-1.amazonaws.com"),
125+ ("us-gov-east-1", "s3.us-gov-east-1.amazonaws.com"),
126+ ("us-gov-west-1", "s3.us-gov-west-1.amazonaws.com"),
127+ ("sa-east-1", "s3.sa-east-1.amazonaws.com")
62− ]
128+ ]
63−
129+
64−-- | Connection Info data type. To create a 'ConnectInfo' value, use one
130+-- | Connection Info data type. To create a 'ConnectInfo' value,
65−-- of the provided smart constructors or override fields of the
131+-- enable the @OverloadedStrings@ language extension and use the
66−-- Default instance.
132+-- `IsString` instance to provide a URL, for example:
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.:
85--
133--
86−-- > awsCI {
134+-- > let c :: ConnectInfo = "https://play.min.io"
87−-- > connectAccessKey = "my-access-key"
88−-- > , connectSecretKey = "my-secret-key"
89−-- > }
90−awsCI :: ConnectInfo
135+data ConnectInfo = ConnectInfo
91−awsCI = def {
92− connectHost = "s3.amazonaws.com"
136+ { connectHost :: Text,
93− , connectPort = 443
137+ connectPort :: Int,
94− , connectAccessKey = ""
138+ connectCreds :: Creds,
95− , connectSecretKey = ""
96− , connectIsSecure = True
139+ connectIsSecure :: Bool,
140+ connectRegion :: Region,
141+ connectAutoDiscoverRegion :: Bool,
142+ connectDisableTLSCertValidation :: Bool
97 }
143 }
98
144
99−-- | AWS ConnectInfo with a specified region. It can optionally
145+getEndpoint :: ConnectInfo -> Endpoint
100−-- disable the automatic discovery of a bucket's region via the
146+getEndpoint ci = (encodeUtf8 $ connectHost ci, connectPort ci, connectIsSecure ci)
101−-- Boolean argument.
147+
102−--
148+instance IsString ConnectInfo where
103−-- > awsWithRegionCI "us-west-1" False {
149+ fromString str =
104−-- > connectAccessKey = "my-access-key"
150+ let req = NC.parseRequest_ str
105−-- > , connectSecretKey = "my-secret-key"
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
106−-- > }
159+ }
160+
161+-- | A 'CredentialLoader' is an action that may return a 'CredentialValue'.
162+-- Loaders may be chained together using 'findFirst'.
107--
163--
108−-- This restricts all operations to the "us-west-1" region and does
164+-- @since 1.7.0
109−-- not perform any bucket location requests.
165+type CredentialLoader = IO (Maybe CredentialValue)
110−awsWithRegionCI :: Region -> Bool -> ConnectInfo
166+
111−awsWithRegionCI region autoDiscoverRegion =
167+-- | Combines the given list of loaders, by calling each one in
112− let host = maybe "s3.amazonaws.com" identity $
168+-- order until a 'CredentialValue' is returned.
113− Map.lookup region awsRegionMap
169+findFirst :: [CredentialLoader] -> IO (Maybe CredentialValue)
114− in awsCI {
170+findFirst [] = return Nothing
115− connectHost = host
171+findFirst (f : fs) = do
116− , connectRegion = region
172+ c <- f
117− , connectAutoDiscoverRegion = autoDiscoverRegion
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
118 }
227 }
119
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}
120
236
121−-- | <https://play.minio.io:9000 Minio Play Server>
237+-- | Set the S3 region parameter in the `ConnectInfo`
122−-- ConnectInfo. Credentials are already filled in.
238+setRegion :: Region -> ConnectInfo -> ConnectInfo
123−minioPlayCI :: ConnectInfo
239+setRegion r connInfo =
124−minioPlayCI = def {
240+ connInfo
125− connectHost = "play.minio.io"
241+ { connectRegion = r,
126− , connectPort = 9000
127− , connectAccessKey = "Q3AM3UQ867SPQQA43P2F"
128− , connectSecretKey = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
129− , connectIsSecure = True
130− , connectAutoDiscoverRegion = False
242+ connectAutoDiscoverRegion = False
131− }
243+ }
132
244
133−-- | ConnectInfo for Minio server. Takes hostname, port and a Boolean
245+-- | Check if the connection to object storage server is secure
134−-- to enable TLS.
246+-- (i.e. uses TLS)
135−--
247+isConnectInfoSecure :: ConnectInfo -> Bool
136−-- > minioCI "minio.example.com" 9000 True {
248+isConnectInfoSecure = connectIsSecure
137−-- > connectAccessKey = "my-access-key"
249+
138−-- > , connectSecretKey = "my-secret-key"
250+-- | Disable TLS certificate validation completely! This makes TLS
139−-- > }
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
140--
264--
141−-- This connects to a Minio server at the given hostname and port over
265+-- Credentials should be supplied before use.
142−-- HTTPS.
266+gcsCI :: ConnectInfo
143−minioCI :: Text -> Int -> Bool -> ConnectInfo
267+gcsCI =
144−minioCI host port isSecure = def {
268+ setRegion
145− connectHost = host
269+ "us"
146− , connectPort = port
270+ "https://storage.googleapis.com"
271+
272+-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
273+-- should be supplied before use.
274+awsCI :: ConnectInfo
275+awsCI = "https://s3.amazonaws.com"
276+
277+-- | <https://play.min.io MinIO Play Server>
278+-- ConnectInfo. Credentials are already filled in.
279+minioPlayCI :: ConnectInfo
280+minioPlayCI =
281+ let playCreds = CredentialValue "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" Nothing
282+ in setCreds playCreds $
283+ setRegion
147− , connectRegion = "us-east-1"
284+ "us-east-1"
148− , connectIsSecure = isSecure
285+ "https://play.min.io"
149− , connectAutoDiscoverRegion = False
150− }
151
286
152-- |
287-- |
153-- Represents a bucket in the object store
288-- Represents a bucket in the object store
@@ -157,25 +292,156 @@ type Bucket = Text
157-- Represents an object name
292-- Represents an object name
158type Object = Text
293type Object = Text
159
294
160−-- |
161−-- Represents a region
295+-- | Represents a region
162−-- TODO: This could be a Sum Type with all defined regions for AWS.
163type Region = Text
296type Region = Text
164
297
165−-- | A type alias to represent an Entity-Tag returned by S3-compatible
298+-- | A type alias to represent an Entity-Tag returned by S3-compatible APIs.
166−-- APIs.
167type ETag = Text
299type ETag = Text
168
300
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+
169-- |
411-- |
170-- BucketInfo returned for list buckets call
412-- BucketInfo returned for list buckets call
171−data BucketInfo = BucketInfo {
413+data BucketInfo = BucketInfo
172− biName :: Bucket
414+ { biName :: Bucket,
173− , biCreationDate :: UTCTime
415+ biCreationDate :: UTCTime
416+ }
174− } deriving (Show, Eq)
417+ deriving stock (Show, Eq)
175
418
176-- | A type alias to represent a part-number for multipart upload
419-- | A type alias to represent a part-number for multipart upload
177type PartNumber = Int16
420type PartNumber = Int16
178
421
422+-- | Select part sizes - the logic is that the minimum part-size will
423+-- be 64MiB.
424+selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
425+selectPartSizes size =
426+ uncurry (List.zip3 [1 ..]) $
427+ List.unzip $
428+ loop 0 size
429+ where
430+ ceil :: Double -> Int64
431+ ceil = ceiling
432+ partSize =
433+ max
434+ minPartSize
435+ ( ceil $
436+ fromIntegral size
437+ / fromIntegral maxMultipartParts
438+ )
439+ m = partSize
440+ loop st sz
441+ | st > sz = []
442+ | st + m >= sz = [(st, sz - st)]
443+ | otherwise = (st, m) : loop (st + m) sz
444+
179-- | A type alias to represent an upload-id for multipart upload
445-- | A type alias to represent an upload-id for multipart upload
180type UploadId = Text
446type UploadId = Text
181
447
@@ -184,163 +450,264 @@ type PartTuple = (PartNumber, ETag)
184
450
185-- | Represents result from a listing of object parts of an ongoing
451-- | Represents result from a listing of object parts of an ongoing
186-- multipart upload.
452-- multipart upload.
187−data ListPartsResult = ListPartsResult {
453+data ListPartsResult = ListPartsResult
188− lprHasMore :: Bool
454+ { lprHasMore :: Bool,
189− , lprNextPart :: Maybe Int
455+ lprNextPart :: Maybe Int,
190− , lprParts :: [ObjectPartInfo]
456+ lprParts :: [ObjectPartInfo]
457+ }
191− } deriving (Show, Eq)
458+ deriving stock (Show, Eq)
192−
193
459
194-- | Represents information about an object part in an ongoing
460-- | Represents information about an object part in an ongoing
195-- multipart upload.
461-- multipart upload.
196−data ObjectPartInfo = ObjectPartInfo {
462+data ObjectPartInfo = ObjectPartInfo
197− opiNumber :: PartNumber
463+ { opiNumber :: PartNumber,
198− , opiETag :: ETag
464+ opiETag :: ETag,
199− , opiSize :: Int64
465+ opiSize :: Int64,
200− , opiModTime :: UTCTime
466+ opiModTime :: UTCTime
467+ }
201− } deriving (Show, Eq)
468+ deriving stock (Show, Eq)
202
469
203-- | Represents result from a listing of incomplete uploads to a
470-- | Represents result from a listing of incomplete uploads to a
204-- bucket.
471-- bucket.
205−data ListUploadsResult = ListUploadsResult {
472+data ListUploadsResult = ListUploadsResult
206− lurHasMore :: Bool
473+ { lurHasMore :: Bool,
207− , lurNextKey :: Maybe Text
474+ lurNextKey :: Maybe Text,
208− , lurNextUpload :: Maybe Text
475+ lurNextUpload :: Maybe Text,
209− , lurUploads :: [(Object, UploadId, UTCTime)]
476+ lurUploads :: [(Object, UploadId, UTCTime)],
210− , lurCPrefixes :: [Text]
477+ lurCPrefixes :: [Text]
478+ }
211− } deriving (Show, Eq)
479+ deriving stock (Show, Eq)
212
480
213-- | Represents information about a multipart upload.
481-- | Represents information about a multipart upload.
214−data UploadInfo = UploadInfo {
482+data UploadInfo = UploadInfo
215− uiKey :: Object
483+ { uiKey :: Object,
216− , uiUploadId :: UploadId
484+ uiUploadId :: UploadId,
217− , uiInitTime :: UTCTime
485+ uiInitTime :: UTCTime,
218− , uiSize :: Int64
486+ uiSize :: Int64
487+ }
219− } deriving (Show, Eq)
488+ deriving stock (Show, Eq)
220
489
221-- | Represents result from a listing of objects in a bucket.
490-- | Represents result from a listing of objects in a bucket.
222−data ListObjectsResult = ListObjectsResult {
491+data ListObjectsResult = ListObjectsResult
223− lorHasMore :: Bool
492+ { lorHasMore :: Bool,
224− , lorNextToken :: Maybe Text
493+ lorNextToken :: Maybe Text,
225− , lorObjects :: [ObjectInfo]
494+ lorObjects :: [ObjectInfo],
226− , lorCPrefixes :: [Text]
495+ lorCPrefixes :: [Text]
496+ }
227− } deriving (Show, Eq)
497+ deriving stock (Show, Eq)
228
498
229-- | Represents result from a listing of objects version 1 in a bucket.
499-- | Represents result from a listing of objects version 1 in a bucket.
230−data ListObjectsV1Result = ListObjectsV1Result {
500+data ListObjectsV1Result = ListObjectsV1Result
231− lorHasMore' :: Bool
501+ { lorHasMore' :: Bool,
232− , lorNextMarker :: Maybe Text
502+ lorNextMarker :: Maybe Text,
233− , lorObjects' :: [ObjectInfo]
503+ lorObjects' :: [ObjectInfo],
234− , lorCPrefixes' :: [Text]
504+ lorCPrefixes' :: [Text]
505+ }
235− } deriving (Show, Eq)
506+ deriving stock (Show, Eq)
236
507
237-- | Represents information about an object.
508-- | Represents information about an object.
238−data ObjectInfo = ObjectInfo {
509+data ObjectInfo = ObjectInfo
510+ { -- | Object key
239− oiObject :: Object
511+ oiObject :: Object,
512+ -- | Modification time of the object
240− , oiModTime :: UTCTime
513+ oiModTime :: UTCTime,
514+ -- | ETag of the object
241− , oiETag :: ETag
515+ oiETag :: ETag,
516+ -- | Size of the object in bytes
242− , oiSize :: Int64
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+ }
243− } deriving (Show, Eq)
530+ deriving stock (Show, Eq)
244−
531+
245−data CopyPartSource = CopyPartSource {
532+-- | Represents source object in server-side copy object
246− -- | formatted like "\/sourceBucket\/sourceObject"
533+data SourceInfo = SourceInfo
247− cpSource :: Text
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,
248− -- | (0, 9) means first ten bytes of the source object
586+ -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
249− , cpSourceRange :: Maybe (Int64, Int64)
587+ gooRange :: Maybe ByteRange,
250− , cpSourceIfMatch :: Maybe Text
588+ -- | Set matching ETag condition, GetObject which matches the following
251− , cpSourceIfNoneMatch :: Maybe Text
589+ -- ETag.
252− , cpSourceIfUnmodifiedSince :: Maybe UTCTime
590+ gooIfMatch :: Maybe ETag,
253− , cpSourceIfModifiedSince :: Maybe UTCTime
591+ -- | Set matching ETag none condition, GetObject which does not match
254− } deriving (Show, Eq)
592+ -- the following ETag.
255−
593+ gooIfNoneMatch :: Maybe ETag,
256−instance Default CopyPartSource where
594+ -- | Set object unmodified condition, GetObject unmodified since given time.
257− def = CopyPartSource "" def def def def def
595+ gooIfUnmodifiedSince :: Maybe UTCTime,
258−
596+ -- | Set object modified condition, GetObject modified since given time.
259−cpsToHeaders :: CopyPartSource -> [HT.Header]
597+ gooIfModifiedSince :: Maybe UTCTime,
260−cpsToHeaders cps = ("x-amz-copy-source", encodeUtf8 $ cpSource cps) :
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
261− rangeHdr ++ zip names values
610+ ++ zip names values
262− where
611+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
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
612 where
283− splits = T.splitOn "/" $ cpSource cps
613+ names =
614+ [ "If-Match",
615+ "If-None-Match",
616+ "If-Unmodified-Since",
617+ "If-Modified-Since"
618+ ]
619+ values =
620+ mapMaybe
621+ (fmap encodeUtf8 . (goo &))
622+ [ gooIfMatch,
623+ gooIfNoneMatch,
624+ fmap formatRFC1123 . gooIfUnmodifiedSince,
625+ fmap formatRFC1123 . gooIfModifiedSince
626+ ]
627+ rangeHdr =
628+ maybe [] (\a -> [(hRange, HT.renderByteRanges [a])]) $
629+ gooRange goo
630+
631+-- | Data type returned by 'getObject' representing the object being
632+-- retrieved. Use the @gor*@ functions to access its contents.
633+data GetObjectResponse = GetObjectResponse
634+ { -- | ObjectInfo of the object being retrieved.
635+ gorObjectInfo :: ObjectInfo,
636+ -- | A conduit of the bytes of the object.
637+ gorObjectStream :: C.ConduitM () ByteString Minio ()
638+ }
284
639
285-- | A data-type for events that can occur in the object storage
640-- | A data-type for events that can occur in the object storage
286-- server. Reference:
641-- server. Reference:
287-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
642-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
643+data Event
288−data Event = ObjectCreated
644+ = ObjectCreated
289− | ObjectCreatedPut
645+ | ObjectCreatedPut
290− | ObjectCreatedPost
646+ | ObjectCreatedPost
291− | ObjectCreatedCopy
647+ | ObjectCreatedCopy
292− | ObjectCreatedMultipartUpload
648+ | ObjectCreatedMultipartUpload
293− | ObjectRemoved
649+ | ObjectRemoved
294− | ObjectRemovedDelete
650+ | ObjectRemovedDelete
295− | ObjectRemovedDeleteMarkerCreated
651+ | ObjectRemovedDeleteMarkerCreated
296− | ReducedRedundancyLostObject
652+ | ReducedRedundancyLostObject
297− deriving (Eq)
653+ deriving stock (Eq, Show)
298−
654+
299−instance Show Event where
655+instance ToText Event where
300− show ObjectCreated = "s3:ObjectCreated:*"
656+ toText ObjectCreated = "s3:ObjectCreated:*"
301− show ObjectCreatedPut = "s3:ObjectCreated:Put"
657+ toText ObjectCreatedPut = "s3:ObjectCreated:Put"
302− show ObjectCreatedPost = "s3:ObjectCreated:Post"
658+ toText ObjectCreatedPost = "s3:ObjectCreated:Post"
303− show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
659+ toText ObjectCreatedCopy = "s3:ObjectCreated:Copy"
304− show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
660+ toText ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
305− show ObjectRemoved = "s3:ObjectRemoved:*"
661+ toText ObjectRemoved = "s3:ObjectRemoved:*"
306− show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
662+ toText ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
307− show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
663+ toText ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
308− show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
664+ toText ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
309
665
310textToEvent :: Text -> Maybe Event
666textToEvent :: Text -> Maybe Event
311textToEvent t = case t of
667textToEvent t = case t of
312− "s3:ObjectCreated:*" -> Just ObjectCreated
668+ "s3:ObjectCreated:*" -> Just ObjectCreated
313− "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
669+ "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
314− "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
670+ "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
315− "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
671+ "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
316− "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
672+ "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
317− "s3:ObjectRemoved:*" -> Just ObjectRemoved
673+ "s3:ObjectRemoved:*" -> Just ObjectRemoved
318− "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
674+ "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
319 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
675 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
320− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
676+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
321− _ -> Nothing
677+ _ -> Nothing
322−
323
678
679+-- | Filter data type - part of notification configuration
324−data Filter = Filter
680+newtype Filter = Filter
325 { fFilter :: FilterKey
681 { fFilter :: FilterKey
682+ }
326− } deriving (Show, Eq)
683+ deriving stock (Show, Eq)
327
684
328−instance Default Filter where
685+-- | defaultFilter is empty, used to create a notification
329− def = Filter def
686+-- configuration.
687+defaultFilter :: Filter
688+defaultFilter = Filter defaultFilterKey
330
689
690+-- | FilterKey contains FilterRules, and is part of a Filter.
331−data FilterKey = FilterKey
691+newtype FilterKey = FilterKey
332 { fkKey :: FilterRules
692 { fkKey :: FilterRules
693+ }
333− } deriving (Show, Eq)
694+ deriving stock (Show, Eq)
334
695
335−instance Default FilterKey where
696+-- | defaultFilterKey is empty, used to create notification
336− def = FilterKey def
697+-- configuration.
698+defaultFilterKey :: FilterKey
699+defaultFilterKey = FilterKey defaultFilterRules
337
700
701+-- | FilterRules represents a collection of `FilterRule`s.
338−data FilterRules = FilterRules
702+newtype FilterRules = FilterRules
339 { frFilterRules :: [FilterRule]
703 { frFilterRules :: [FilterRule]
704+ }
340− } deriving (Show, Eq)
705+ deriving stock (Show, Eq)
341
706
342−instance Default FilterRules where
707+-- | defaultFilterRules is empty, used to create notification
708+-- configuration.
709+defaultFilterRules :: FilterRules
343− def = FilterRules []
710+defaultFilterRules = FilterRules []
344
711
345-- | A filter rule that can act based on the suffix or prefix of an
712-- | A filter rule that can act based on the suffix or prefix of an
346-- object. As an example, let's create two filter rules:
713-- object. As an example, let's create two filter rules:
@@ -348,25 +715,28 @@ instance Default FilterRules where
348-- > let suffixRule = FilterRule "suffix" ".jpg"
715-- > let suffixRule = FilterRule "suffix" ".jpg"
349-- > let prefixRule = FilterRule "prefix" "images/"
716-- > let prefixRule = FilterRule "prefix" "images/"
350--
717--
351−-- The `suffixRule` restricts the notification to be triggered only
718+-- The @suffixRule@ restricts the notification to be triggered only
352−-- for objects having a suffix of ".jpg", and the `prefixRule`
719+-- for objects having a suffix of ".jpg", and the @prefixRule@
353-- restricts it to objects having a prefix of "images/".
720-- restricts it to objects having a prefix of "images/".
354data FilterRule = FilterRule
721data FilterRule = FilterRule
355− { frName :: Text
722+ { frName :: Text,
356− , frValue :: Text
723+ frValue :: Text
724+ }
357− } deriving (Show, Eq)
725+ deriving stock (Show, Eq)
358
726
727+-- | Arn is an alias of Text
359type Arn = Text
728type Arn = Text
360
729
361-- | A data-type representing the configuration for a particular
730-- | A data-type representing the configuration for a particular
362-- notification system. It could represent a Queue, Topic or Lambda
731-- notification system. It could represent a Queue, Topic or Lambda
363-- Function configuration.
732-- Function configuration.
364data NotificationConfig = NotificationConfig
733data NotificationConfig = NotificationConfig
365− { ncId :: Text
734+ { ncId :: Text,
366− , ncArn :: Arn
735+ ncArn :: Arn,
367− , ncEvents :: [Event]
736+ ncEvents :: [Event],
368− , ncFilter :: Filter
737+ ncFilter :: Filter
738+ }
369− } deriving (Show, Eq)
739+ deriving stock (Show, Eq)
370
740
371-- | A data-type to represent bucket notification configuration. It is
741-- | A data-type to represent bucket notification configuration. It is
372-- a collection of queue, topic or lambda function configurations. The
742-- a collection of queue, topic or lambda function configurations. The
@@ -374,110 +744,448 @@ data NotificationConfig = NotificationConfig
374-- described at
744-- described at
375-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
745-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
376data Notification = Notification
746data Notification = Notification
377− { nQueueConfigurations :: [NotificationConfig]
747+ { nQueueConfigurations :: [NotificationConfig],
378− , nTopicConfigurations :: [NotificationConfig]
748+ nTopicConfigurations :: [NotificationConfig],
379− , nCloudFunctionConfigurations :: [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+ }
380− } deriving (Eq, Show)
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"
381
932
382−instance Default Notification where
933+-- | Allow quoted record delimiters inside a row using this property.
383− def = Notification [] [] []
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+--------------------------------------------------------------------------
384
1023
385-- | Represents different kinds of payload that are used with S3 API
1024-- | Represents different kinds of payload that are used with S3 API
386-- requests.
1025-- requests.
1026+data Payload
387−data Payload = PayloadBS ByteString
1027+ = PayloadBS ByteString
388− | PayloadH Handle
1028+ | PayloadH Handle Int64 Int64 -- file handle, offset and length
389− Int64 -- offset
1029+ | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
390− Int64 -- size
1030+
391−
1031+defaultPayload :: Payload
392−instance Default Payload where
393− def = PayloadBS ""
1032+defaultPayload = PayloadBS ""
394−
1033+
395−data RequestInfo = RequestInfo {
1034+data AdminReqInfo = AdminReqInfo
396− riMethod :: Method
1035+ { ariMethod :: Method,
397− , riBucket :: Maybe Bucket
1036+ ariPayloadHash :: Maybe ByteString,
398− , riObject :: Maybe Object
1037+ ariPayload :: Payload,
399− , riQueryParams :: Query
1038+ ariPath :: ByteString,
400− , riHeaders :: [Header]
1039+ ariHeaders :: [Header],
401− , riPayload :: Payload
1040+ ariQueryParams :: Query
402− , riPayloadHash :: Maybe ByteString
403− , riRegion :: Maybe Region
404− , riNeedsLocation :: Bool
405 }
1041 }
406
1042
407−instance Default RequestInfo where
1043+data S3ReqInfo = S3ReqInfo
408− def = RequestInfo HT.methodGet def def def def def Nothing def True
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+ }
409
1055
410−getPathFromRI :: RequestInfo -> ByteString
1056+defaultS3ReqInfo :: S3ReqInfo
411−getPathFromRI ri =
1057+defaultS3ReqInfo =
412− let
1058+ S3ReqInfo
413− b = riBucket ri
1059+ HT.methodGet
414− o = riObject ri
1060+ Nothing
415− segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
1061+ Nothing
416− in
1062+ []
417− B.concat ["/", B.intercalate "/" segments]
1063+ []
1064+ defaultPayload
1065+ Nothing
1066+ Nothing
1067+ True
1068+ Nothing
418
1069
419−-- | Time to expire for a presigned URL. It interpreted as a number of
1070+getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
420−-- seconds. The maximum duration that can be specified is 7 days.
1071+getS3Path b o =
421−type UrlExpiry = Int
1072+ let segments = map encodeUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
1073+ in B.concat ["/", B.intercalate "/" segments]
422
1074
423−type RegionMap = Map.Map Bucket Region
1075+type RegionMap = H.HashMap Bucket Region
424
1076
1077+-- | The Minio Monad - all computations accessing object storage
1078+-- happens in it.
425−newtype Minio a = Minio {
1079+newtype Minio a = Minio
426− unMinio :: ReaderT MinioConn (StateT RegionMap (ResourceT IO)) a
1080+ { unMinio :: ReaderT MinioConn (ResourceT IO) a
427 }
1081 }
428− deriving (
1082+ deriving newtype
429− Functor
1083+ ( Functor,
430− , Applicative
1084+ Applicative,
431− , Monad
1085+ Monad,
432− , MonadIO
1086+ MonadIO,
433− , MonadReader MinioConn
1087+ MonadReader MinioConn,
434− , MonadState RegionMap
435− , MonadThrow
436− , MonadCatch
437− , MonadBase IO
438− , MonadResource
1088+ MonadResource,
1089+ MonadUnliftIO
439 )
1090 )
440
1091
441−instance MonadBaseControl IO Minio where
442− type StM Minio a = (a, RegionMap)
443− liftBaseWith f = Minio $ liftBaseWith $ \q -> f (q . unMinio)
444− restoreM = Minio . restoreM
445−
446−-- | MinioConn holds connection info and a connection pool
1092+-- | MinioConn holds connection info and a connection pool to allow
1093+-- for efficient resource re-use.
447−data MinioConn = MinioConn {
1094+data MinioConn = MinioConn
448− mcConnInfo :: ConnectInfo
1095+ { mcConnInfo :: ConnectInfo,
449− , mcConnManager :: NC.Manager
1096+ mcConnManager :: NC.Manager,
1097+ mcRegionMap :: MVar RegionMap
450 }
1098 }
451
1099
1100+class HasSvcNamespace env where
1101+ getSvcNamespace :: env -> Text
1102+
1103+instance HasSvcNamespace MinioConn where
1104+ getSvcNamespace env =
1105+ let host = connectHost $ mcConnInfo env
1106+ in ( if host == "storage.googleapis.com"
1107+ then "http://doc.s3.amazonaws.com/2006-03-01"
1108+ else "http://s3.amazonaws.com/doc/2006-03-01/"
1109+ )
1110+
452-- | Takes connection information and returns a connection object to
1111-- | Takes connection information and returns a connection object to
453−-- be passed to 'runMinio'
1112+-- be passed to 'runMinio'. The returned value can be kept in the
1113+-- application environment and passed to `runMinioWith` whenever
1114+-- object storage is accessed.
454connect :: ConnectInfo -> IO MinioConn
1115connect :: ConnectInfo -> IO MinioConn
455connect ci = do
1116connect 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
456− let settings = bool defaultManagerSettings NC.tlsManagerSettings $
1125+ | connectIsSecure ci = NC.tlsManagerSettings
457− connectIsSecure ci
1126+ | otherwise = defaultManagerSettings
458 mgr <- NC.newManager settings
1127 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
459− return $ MinioConn ci mgr
1141+ return $ MinioConn ci mgr rMapMVar
460
1142
461-- | Run the Minio action and return the result or an error.
1143-- | Run the Minio action and return the result or an error.
462runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
1144runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
463runMinio ci m = do
1145runMinio ci m = do
464− conn <- liftIO $ connect ci
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 =
465− runResourceT . flip evalStateT Map.empty . flip runReaderT conn . unMinio $
1153+ flip runReaderT conn . unMinio $
466− fmap Right m `MC.catches`
1154+ fmap Right m
467− [ MC.Handler handlerServiceErr
1155+ `U.catches` [ U.Handler handlerServiceErr,
468− , MC.Handler handlerHE
1156+ U.Handler handlerHE,
469− , MC.Handler handlerFE
1157+ U.Handler handlerFE,
470− , MC.Handler handlerValidation
1158+ U.Handler handlerValidation
471− ]
1159+ ]
472 where
1160 where
473 handlerServiceErr = return . Left . MErrService
1161 handlerServiceErr = return . Left . MErrService
474 handlerHE = return . Left . MErrHTTP
1162 handlerHE = return . Left . MErrHTTP
475 handlerFE = return . Left . MErrIO
1163 handlerFE = return . Left . MErrIO
476 handlerValidation = return . Left . MErrValidation
1164 handlerValidation = return . Left . MErrValidation
477
1165
478−s3Name :: Text -> Name
1166+-- | Similar to 'runMinio'. Allows applications to allocate/release
479−s3Name s = Name s (Just "http://s3.amazonaws.com/doc/2006-03-01/") Nothing
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
480
1172
481-- | Format as per RFC 1123.
1173-- | Format as per RFC 1123.
482formatRFC1123 :: UTCTime -> T.Text
1174formatRFC1123 :: UTCTime -> T.Text
483formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1175formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
1176+
1177+lookupRegionCache :: Bucket -> Minio (Maybe Region)
1178+lookupRegionCache b = do
1179+ rMVar <- asks mcRegionMap
1180+ rMap <- UM.readMVar rMVar
1181+ return $ H.lookup b rMap
1182+
1183+addToRegionCache :: Bucket -> Region -> Minio ()
1184+addToRegionCache b region = do
1185+ rMVar <- asks mcRegionMap
1186+ UM.modifyMVar_ rMVar $ return . H.insert b region
1187+
1188+deleteFromRegionCache :: Bucket -> Minio ()
1189+deleteFromRegionCache b = do
1190+ rMVar <- asks mcRegionMap
1191+ UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/Data/ByteString.hs+20−19
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,23 +13,21 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16−
17{-# LANGUAGE FlexibleInstances #-}
16{-# LANGUAGE FlexibleInstances #-}
17+
18module Network.Minio.Data.ByteString
18module Network.Minio.Data.ByteString
19− (
20− stripBS
19+ ( stripBS,
21− , UriEncodable(..)
20+ UriEncodable (..),
21+ )
22− ) where
22+where
23
23
24import qualified Data.ByteString as B
24import qualified Data.ByteString as B
25import qualified Data.ByteString.Builder as BB
25import qualified Data.ByteString.Builder as BB
26import qualified Data.ByteString.Char8 as BC8
26import qualified Data.ByteString.Char8 as BC8
27import qualified Data.ByteString.Lazy as LB
27import qualified Data.ByteString.Lazy as LB
28−import Data.Char (isSpace, toUpper, isAsciiUpper, isAsciiLower, isDigit)
28+import Data.Char (isAsciiLower, isAsciiUpper, isDigit, isSpace, toUpper)
29import qualified Data.Text as T
29import qualified Data.Text as T
30−import Numeric (showHex)
30+import Numeric (showHex)
31−
32−import Lib.Prelude
33
31
34stripBS :: ByteString -> ByteString
32stripBS :: ByteString -> ByteString
35stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
33stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
@@ -39,8 +37,10 @@ class UriEncodable s where
39
37
40instance UriEncodable [Char] where
38instance UriEncodable [Char] where
41 uriEncode encodeSlash payload =
39 uriEncode encodeSlash payload =
40+ LB.toStrict $
42− LB.toStrict $ BB.toLazyByteString $ mconcat $
41+ BB.toLazyByteString $
42+ mconcat $
43− map (`uriEncodeChar` encodeSlash) payload
43+ map (`uriEncodeChar` encodeSlash) payload
44
44
45instance UriEncodable ByteString where
45instance UriEncodable ByteString where
46 -- assumes that uriEncode is passed ASCII encoded strings.
46 -- assumes that uriEncode is passed ASCII encoded strings.
@@ -59,16 +59,17 @@ uriEncodeChar '/' True = BB.byteString "%2F"
59uriEncodeChar '/' False = BB.char7 '/'
59uriEncodeChar '/' False = BB.char7 '/'
60uriEncodeChar ch _
60uriEncodeChar ch _
61 | isAsciiUpper ch
61 | isAsciiUpper ch
62− || isAsciiLower ch
62+ || isAsciiLower ch
63− || isDigit ch
63+ || isDigit ch
64− || (ch == '_')
64+ || (ch == '_')
65− || (ch == '-')
65+ || (ch == '-')
66− || (ch == '.')
66+ || (ch == '.')
67+ || (ch == '~') =
67− || (ch == '~') = BB.char7 ch
68+ BB.char7 ch
68 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
69 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
69 where
70 where
70 f :: Word8 -> BB.Builder
71 f :: Word8 -> BB.Builder
71 f n = BB.char7 '%' <> BB.string7 hexStr
72 f n = BB.char7 '%' <> BB.string7 hexStr
72 where
73 where
73 hexStr = map toUpper $ showHex q $ showHex r ""
74 hexStr = map toUpper $ showHex q $ showHex r ""
74− (q, r) = divMod (fromIntegral n) (16::Word8)
75+ (q, r) = divMod n (16 :: Word8)
src/Network/Minio/Data/Crypto.hs+39−28
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,50 +15,54 @@
15--
15--
16
16
17module Network.Minio.Data.Crypto
17module Network.Minio.Data.Crypto
18− (
19− hashSHA256
18+ ( hashSHA256,
20− , hashSHA256FromSource
19+ hashSHA256FromSource,
20+ hashMD5,
21+ hashMD5ToBase64,
22+ hashMD5FromSource,
23+ hmacSHA256,
24+ hmacSHA256RawBS,
25+ digestToBS,
26+ digestToBase16,
27+ encodeToBase64,
28+ )
29+where
21
30
22− , hashMD5
31+import Crypto.Hash
23− , hashMD5FromSource
32+ ( Digest,
24−
33+ MD5 (..),
25− , hmacSHA256
34+ SHA256 (..),
26− , hmacSHA256RawBS
35+ hashWith,
27− , digestToBS
36+ )
28− , digestToBase16
29− ) where
30−
31−import Crypto.Hash (SHA256(..), MD5(..), hashWith, Digest)
32−import Crypto.Hash.Conduit (sinkHash)
37+import Crypto.Hash.Conduit (sinkHash)
33−import Crypto.MAC.HMAC (hmac, HMAC)
38+import Crypto.MAC.HMAC (HMAC, hmac)
34−import Data.ByteArray (ByteArrayAccess, convert)
39+import Data.ByteArray (ByteArrayAccess, convert)
35−import Data.ByteArray.Encoding (convertToBase, Base(Base16))
40+import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
36import qualified Data.Conduit as C
41import qualified Data.Conduit as C
37
42
38−import Lib.Prelude
39−
40hashSHA256 :: ByteString -> ByteString
43hashSHA256 :: ByteString -> ByteString
41hashSHA256 = digestToBase16 . hashWith SHA256
44hashSHA256 = digestToBase16 . hashWith SHA256
42
45
43−hashSHA256FromSource :: Monad m => C.Producer m ByteString -> m ByteString
46+hashSHA256FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
44hashSHA256FromSource src = do
47hashSHA256FromSource src = do
45− digest <- src C.$$ sinkSHA256Hash
48+ digest <- C.connect src sinkSHA256Hash
46 return $ digestToBase16 digest
49 return $ digestToBase16 digest
47 where
50 where
48 -- To help with type inference
51 -- To help with type inference
49− sinkSHA256Hash :: Monad m => C.Consumer ByteString m (Digest SHA256)
52+ sinkSHA256Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest SHA256)
50 sinkSHA256Hash = sinkHash
53 sinkSHA256Hash = sinkHash
51
54
55+-- Returns MD5 hash hex encoded.
52hashMD5 :: ByteString -> ByteString
56hashMD5 :: ByteString -> ByteString
53hashMD5 = digestToBase16 . hashWith MD5
57hashMD5 = digestToBase16 . hashWith MD5
54
58
55−hashMD5FromSource :: Monad m => C.Producer m ByteString -> m ByteString
59+hashMD5FromSource :: (Monad m) => C.ConduitM () ByteString m () -> m ByteString
56hashMD5FromSource src = do
60hashMD5FromSource src = do
57− digest <- src C.$$ sinkMD5Hash
61+ digest <- C.connect src sinkMD5Hash
58 return $ digestToBase16 digest
62 return $ digestToBase16 digest
59 where
63 where
60 -- To help with type inference
64 -- To help with type inference
61− sinkMD5Hash :: Monad m => C.Consumer ByteString m (Digest MD5)
65+ sinkMD5Hash :: (Monad m) => C.ConduitM ByteString Void m (Digest MD5)
62 sinkMD5Hash = sinkHash
66 sinkMD5Hash = sinkHash
63
67
64hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
68hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
@@ -67,8 +71,15 @@ hmacSHA256 message key = hmac key message
67hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
71hmacSHA256RawBS :: ByteString -> ByteString -> ByteString
68hmacSHA256RawBS message key = convert $ hmacSHA256 message key
72hmacSHA256RawBS message key = convert $ hmacSHA256 message key
69
73
70−digestToBS :: ByteArrayAccess a => a -> ByteString
74+digestToBS :: (ByteArrayAccess a) => a -> ByteString
71digestToBS = convert
75digestToBS = convert
72
76
73−digestToBase16 :: ByteArrayAccess a => a -> ByteString
77+digestToBase16 :: (ByteArrayAccess a) => a -> ByteString
74digestToBase16 = convertToBase Base16
78digestToBase16 = convertToBase Base16
79+
80+-- Returns MD5 hash base 64 encoded.
81+hashMD5ToBase64 :: (ByteArrayAccess a) => a -> ByteString
82+hashMD5ToBase64 = convertToBase Base64 . hashWith MD5
83+
84+encodeToBase64 :: (ByteArrayAccess a) => a -> ByteString
85+encodeToBase64 = convertToBase Base64
src/Network/Minio/Data/Time.hs+18−14
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,20 +15,24 @@
15--
15--
16
16
17module Network.Minio.Data.Time
17module Network.Minio.Data.Time
18− (
19− awsTimeFormat
18+ ( awsTimeFormat,
20− , awsTimeFormatBS
19+ awsTimeFormatBS,
21− , awsDateFormat
20+ awsDateFormat,
22− , awsDateFormatBS
21+ awsDateFormatBS,
23− , awsParseTime
22+ awsParseTime,
24− , iso8601TimeFormat
23+ iso8601TimeFormat,
24+ UrlExpiry,
25+ )
25− ) where
26+where
26−
27+
27−
28−import Data.ByteString.Char8 (pack)
28+import Data.ByteString.Char8 (pack)
29import qualified Data.Time as Time
29import qualified Data.Time as Time
30+import Data.Time.Format.ISO8601 (iso8601Show)
31+import Lib.Prelude
30
32
31−import Lib.Prelude
33+-- | Time to expire for a presigned URL. It interpreted as a number of
34+-- seconds. The maximum duration that can be specified is 7 days.
35+type UrlExpiry = Int
32
36
33awsTimeFormat :: UTCTime -> [Char]
37awsTimeFormat :: UTCTime -> [Char]
34awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
38awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
@@ -46,4 +50,4 @@ awsParseTime :: [Char] -> Maybe UTCTime
46awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
50awsParseTime = Time.parseTimeM False Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
47
51
48iso8601TimeFormat :: UTCTime -> [Char]
52iso8601TimeFormat :: UTCTime -> [Char]
49−iso8601TimeFormat = Time.formatTime Time.defaultTimeLocale (Time.iso8601DateFormat $ Just "%T%QZ")
53+iso8601TimeFormat = iso8601Show
src/Network/Minio/Errors.hs+59−45
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,69 +14,83 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17−module Network.Minio.Errors where
17+module Network.Minio.Errors
18+ ( MErrV (..),
19+ ServiceErr (..),
20+ MinioErr (..),
21+ toServiceErr,
22+ )
23+where
18
24
19−import Control.Exception
25+import Control.Exception (IOException)
20import qualified Network.HTTP.Conduit as NC
26import qualified Network.HTTP.Conduit as NC
21
27
22−import Lib.Prelude
23−
24−
25---------------------------------
28---------------------------------
26-- Errors
29-- Errors
27---------------------------------
30---------------------------------
31+
28-- | Various validation errors
32-- | Various validation errors
33+data MErrV
29−data MErrV = MErrVSinglePUTSizeExceeded Int64
34+ = MErrVSinglePUTSizeExceeded Int64
30− | MErrVPutSizeExceeded Int64
35+ | MErrVPutSizeExceeded Int64
31− | MErrVETagHeaderNotFound
36+ | MErrVETagHeaderNotFound
32− | MErrVInvalidObjectInfoResponse
37+ | MErrVInvalidObjectInfoResponse
33− | MErrVInvalidSrcObjSpec Text
38+ | MErrVInvalidSrcObjSpec Text
34− | MErrVInvalidSrcObjByteRange (Int64, Int64)
39+ | MErrVInvalidSrcObjByteRange (Int64, Int64)
35− | MErrVCopyObjSingleNoRangeAccepted
40+ | MErrVCopyObjSingleNoRangeAccepted
36− | MErrVRegionNotSupported Text
41+ | MErrVRegionNotSupported Text
37− | MErrVXmlParse Text
42+ | MErrVXmlParse Text
38− | MErrVInvalidBucketName Text
43+ | MErrVInvalidBucketName Text
39− | MErrVInvalidObjectName Text
44+ | MErrVInvalidObjectName Text
40− | MErrVInvalidUrlExpiry Int
45+ | MErrVInvalidUrlExpiry Int
46+ | MErrVJsonParse Text
47+ | MErrVInvalidHealPath
48+ | MErrVMissingCredentials
49+ | MErrVInvalidEncryptionKeyLength
50+ | MErrVStreamingBodyUnexpectedEOF
51+ | MErrVUnexpectedPayload
52+ | MErrVSTSEndpointNotFound
41− deriving (Show, Eq)
53+ deriving stock (Show, Eq)
42
54
43instance Exception MErrV
55instance Exception MErrV
44
56
45-- | Errors returned by S3 compatible service
57-- | Errors returned by S3 compatible service
58+data ServiceErr
46−data ServiceErr = BucketAlreadyExists
59+ = BucketAlreadyExists
47− | BucketAlreadyOwnedByYou
60+ | BucketAlreadyOwnedByYou
48− | NoSuchBucket
61+ | NoSuchBucket
49− | InvalidBucketName
62+ | InvalidBucketName
50− | NoSuchKey
63+ | NoSuchKey
64+ | SelectErr Text Text
51− | ServiceErr Text Text
65+ | ServiceErr Text Text
52− deriving (Show, Eq)
66+ deriving stock (Show, Eq)
53
67
54instance Exception ServiceErr
68instance Exception ServiceErr
55
69
56toServiceErr :: Text -> Text -> ServiceErr
70toServiceErr :: Text -> Text -> ServiceErr
57−toServiceErr "NoSuchKey" _ = NoSuchKey
71+toServiceErr "NoSuchKey" _ = NoSuchKey
58−toServiceErr "NoSuchBucket" _ = NoSuchBucket
72+toServiceErr "NoSuchBucket" _ = NoSuchBucket
59−toServiceErr "InvalidBucketName" _ = InvalidBucketName
73+toServiceErr "InvalidBucketName" _ = InvalidBucketName
60toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
74toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
61−toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
75+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
62−toServiceErr code message = ServiceErr code message
76+toServiceErr code message = ServiceErr code message
63−
64
77
65-- | Errors thrown by the library
78-- | Errors thrown by the library
79+data MinioErr
66−data MinioErr = MErrHTTP NC.HttpException
80+ = MErrHTTP NC.HttpException
67− | MErrIO IOException
81+ | MErrIO IOException
68− | MErrService ServiceErr
82+ | MErrService ServiceErr
69− | MErrValidation MErrV
83+ | MErrValidation MErrV
70− deriving (Show)
84+ deriving stock (Show)
71
85
72instance Eq MinioErr where
86instance Eq MinioErr where
73− MErrHTTP _ == MErrHTTP _ = True
87+ MErrHTTP _ == MErrHTTP _ = True
74− MErrHTTP _ == _ = False
88+ MErrHTTP _ == _ = False
75− MErrIO _ == MErrIO _ = True
89+ MErrIO _ == MErrIO _ = True
76− MErrIO _ == _ = False
90+ MErrIO _ == _ = False
77− MErrService a == MErrService b = a == b
91+ MErrService a == MErrService b = a == b
78− MErrService _ == _ = False
92+ MErrService _ == _ = False
79− MErrValidation a == MErrValidation b = a == b
93+ MErrValidation a == MErrValidation b = a == b
80− MErrValidation _ == _ = False
94+ MErrValidation _ == _ = False
81
95
82instance Exception MinioErr
96instance Exception MinioErr
src/Network/Minio/JsonParser.hsnew file+49
@@ -0,0 +1,49 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.JsonParser
18+ ( parseErrResponseJSON,
19+ )
20+where
21+
22+import Data.Aeson
23+ ( FromJSON,
24+ eitherDecode,
25+ parseJSON,
26+ withObject,
27+ (.:),
28+ )
29+import qualified Data.Text as T
30+import Lib.Prelude
31+import Network.Minio.Errors
32+
33+data AdminErrJSON = AdminErrJSON
34+ { aeCode :: Text,
35+ aeMessage :: Text
36+ }
37+ deriving stock (Eq, Show)
38+
39+instance FromJSON AdminErrJSON where
40+ parseJSON = withObject "AdminErrJSON" $ \v ->
41+ AdminErrJSON
42+ <$> v .: "Code"
43+ <*> v .: "Message"
44+
45+parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
46+parseErrResponseJSON jsondata =
47+ case eitherDecode jsondata of
48+ Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
49+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs+120−38
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -19,80 +19,162 @@ module Network.Minio.ListOps where
19import qualified Data.Conduit as C
19import qualified Data.Conduit as C
20import qualified Data.Conduit.Combinators as CC
20import qualified Data.Conduit.Combinators as CC
21import qualified Data.Conduit.List as CL
21import 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+ )
22
56
23−import Lib.Prelude
57+-- | Represents a list output item - either an object or an object
58+-- prefix (i.e. a directory).
59+data ListItem
60+ = ListItemObject ObjectInfo
61+ | ListItemPrefix Text
62+ deriving stock (Show, Eq)
24
63
25−import Network.Minio.Data
64+-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
26−import Network.Minio.S3API
65+-- similar to a file system tree traversal.
27−
66+--
28−-- | List objects in a bucket matching the given prefix. If recurse is
67+-- If @prefix@ is not 'Nothing', only items with the given prefix are
29−-- set to True objects matching prefix are recursively listed.
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).
30−listObjects :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
76+listObjects :: Bucket -> Maybe Text -> Bool -> C.ConduitM () ListItem Minio ()
31listObjects bucket prefix recurse = loop Nothing
77listObjects bucket prefix recurse = loop Nothing
32 where
78 where
33− loop :: Maybe Text -> C.Producer Minio ObjectInfo
79+ loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
34 loop nextToken = do
80 loop nextToken = do
35− let
36− delimiter = bool (Just "/") Nothing recurse
81+ let delimiter = bool (Just "/") Nothing recurse
37
82
38 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
83 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
39− CL.sourceList $ lorObjects res
84+ CL.sourceList $ map ListItemObject $ lorObjects res
85+ unless recurse $
86+ CL.sourceList $
87+ map ListItemPrefix $
88+ lorCPrefixes res
40 when (lorHasMore res) $
89 when (lorHasMore res) $
41 loop (lorNextToken res)
90 loop (lorNextToken res)
42
91
43−-- | List objects in a bucket matching the given prefix. If recurse is
92+-- | Lists objects - similar to @listObjects@, however uses the older
44−-- set to True objects matching prefix are recursively listed.
93+-- V1 AWS S3 API. Prefer @listObjects@ to this.
45−listObjectsV1 :: Bucket -> Maybe Text -> Bool -> C.Producer Minio ObjectInfo
94+listObjectsV1 ::
95+ Bucket ->
96+ Maybe Text ->
97+ Bool ->
98+ C.ConduitM () ListItem Minio ()
46listObjectsV1 bucket prefix recurse = loop Nothing
99listObjectsV1 bucket prefix recurse = loop Nothing
47 where
100 where
48− loop :: Maybe Text -> C.Producer Minio ObjectInfo
101+ loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
49 loop nextMarker = do
102 loop nextMarker = do
50− let
51− delimiter = bool (Just "/") Nothing recurse
103+ let delimiter = bool (Just "/") Nothing recurse
52
104
53 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
105 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
54− CL.sourceList $ lorObjects' res
106+ CL.sourceList $ map ListItemObject $ lorObjects' res
107+ unless recurse $
108+ CL.sourceList $
109+ map ListItemPrefix $
110+ lorCPrefixes' res
55 when (lorHasMore' res) $
111 when (lorHasMore' res) $
56 loop (lorNextMarker res)
112 loop (lorNextMarker res)
57
113
58-- | List incomplete uploads in a bucket matching the given prefix. If
114-- | List incomplete uploads in a bucket matching the given prefix. If
59-- recurse is set to True incomplete uploads for the given prefix are
115-- recurse is set to True incomplete uploads for the given prefix are
60-- recursively listed.
116-- recursively listed.
61−listIncompleteUploads :: Bucket -> Maybe Text -> Bool
117+listIncompleteUploads ::
118+ Bucket ->
119+ Maybe Text ->
120+ Bool ->
62− -> C.Producer Minio UploadInfo
121+ C.ConduitM () UploadInfo Minio ()
63listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
122listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
64 where
123 where
65− loop :: Maybe Text -> Maybe Text -> C.Producer Minio UploadInfo
124+ loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
66 loop nextKeyMarker nextUploadIdMarker = do
125 loop nextKeyMarker nextUploadIdMarker = do
67− let
68− delimiter = bool (Just "/") Nothing recurse
126+ let delimiter = bool (Just "/") Nothing recurse
69
127
128+ res <-
129+ lift $
70− res <- lift $ listIncompleteUploads' bucket prefix delimiter
130+ listIncompleteUploads'
131+ bucket
132+ prefix
133+ delimiter
134+ nextKeyMarker
71− nextKeyMarker nextUploadIdMarker Nothing
135+ nextUploadIdMarker
136+ Nothing
72
137
138+ aggrSizes <- lift $
73− aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do
139+ forM (lurUploads res) $ \(uKey, uId, _) -> do
140+ partInfos <-
141+ C.runConduit $
74− partInfos <- listIncompleteParts bucket uKey uId C.$$ CC.sinkList
142+ listIncompleteParts bucket uKey uId
143+ C..| CC.sinkList
75− return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
144+ return $ foldl' (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
76
145
77 CL.sourceList $
146 CL.sourceList $
147+ zipWith
148+ ( curry
78− map (\((uKey, uId, uInitTime), size) ->
149+ ( \((uKey, uId, uInitTime), size) ->
79− UploadInfo uKey uId uInitTime size
150+ UploadInfo uKey uId uInitTime size
151+ )
152+ )
80− ) $ zip (lurUploads res) aggrSizes
153+ (lurUploads res)
154+ aggrSizes
81
155
82 when (lurHasMore res) $
156 when (lurHasMore res) $
83 loop (lurNextKey res) (lurNextUpload res)
157 loop (lurNextKey res) (lurNextUpload res)
84
158
85−
86-- | List object parts of an ongoing multipart upload for given
159-- | List object parts of an ongoing multipart upload for given
87-- bucket, object and uploadId.
160-- bucket, object and uploadId.
88−listIncompleteParts :: Bucket -> Object -> UploadId
161+listIncompleteParts ::
162+ Bucket ->
163+ Object ->
164+ UploadId ->
89− -> C.Producer Minio ObjectPartInfo
165+ C.ConduitM () ObjectPartInfo Minio ()
90listIncompleteParts bucket object uploadId = loop Nothing
166listIncompleteParts bucket object uploadId = loop Nothing
91 where
167 where
92− loop :: Maybe Text -> C.Producer Minio ObjectPartInfo
168+ loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
93 loop nextPartMarker = do
169 loop nextPartMarker = do
170+ res <-
171+ lift $
94− res <- lift $ listIncompleteParts' bucket object uploadId Nothing
172+ listIncompleteParts'
173+ bucket
174+ object
175+ uploadId
176+ Nothing
95− nextPartMarker
177+ nextPartMarker
96 CL.sourceList $ lprParts res
178 CL.sourceList $ lprParts res
97 when (lprHasMore res) $
179 when (lprHasMore res) $
98 loop (show <$> lprNextPart res)
180 loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs+221−147
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,45 +13,51 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16+{-# LANGUAGE CPP #-}
16
17
17module Network.Minio.PresignedOperations
18module Network.Minio.PresignedOperations
18− ( UrlExpiry
19+ ( UrlExpiry,
19− , makePresignedUrl
20+ makePresignedUrl,
20− , presignedPutObjectUrl
21+ presignedPutObjectUrl,
21− , presignedGetObjectUrl
22+ presignedGetObjectUrl,
22− , presignedHeadObjectUrl
23+ presignedHeadObjectUrl,
23−
24− , PostPolicyCondition(..)
24+ PostPolicyCondition (..),
25− , ppCondBucket
25+ ppCondBucket,
26− , ppCondContentLengthRange
26+ ppCondContentLengthRange,
27− , ppCondContentType
27+ ppCondContentType,
28− , ppCondKey
28+ ppCondKey,
29− , ppCondKeyStartsWith
29+ ppCondKeyStartsWith,
30− , ppCondSuccessActionStatus
30+ ppCondSuccessActionStatus,
31−
32− , PostPolicy(..)
31+ PostPolicy (..),
33− , PostPolicyError(..)
32+ PostPolicyError (..),
34− , newPostPolicy
33+ newPostPolicy,
35− , showPostPolicy
34+ showPostPolicy,
36− , presignedPostPolicy
35+ presignedPostPolicy,
36+ )
37− ) where
37+where
38−
38+
39−import Data.Aeson ((.=))
39+import Data.Aeson ((.=))
40−import qualified Data.Aeson as Json
40+import qualified Data.Aeson as Json
41−import Data.ByteString.Builder (byteString, toLazyByteString)
41+import Data.ByteString.Builder (byteString, toLazyByteString)
42−import Data.Default (def)
43−import qualified Data.Map.Strict as Map
42+import qualified Data.HashMap.Strict as H
44−import qualified Data.Text as T
43+import qualified Data.Text as T
45−import qualified Data.Time as Time
44+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
45+import Lib.Prelude
50−
46+import qualified Network.HTTP.Client as NClient
51−import Network.Minio.Data
47+import qualified Network.HTTP.Types as HT
48+import Network.Minio.API (buildRequest)
49+import Network.Minio.Credentials
50+import Network.Minio.Data
52−import Network.Minio.Data.Time
51+import Network.Minio.Data.Time
53−import Network.Minio.Errors
52+import Network.Minio.Errors
54−import Network.Minio.Sign.V4
53+import Network.Minio.Sign.V4
54+import Network.URI (uriToString)
55+
56+{- ORMOLU_DISABLE -}
57+#if MIN_VERSION_aeson(2,0,0)
58+import qualified Data.Aeson.Key as A
59+#endif
60+{- ORMOLU_ENABLE -}
55
61
56-- | Generate a presigned URL. This function allows for advanced usage
62-- | Generate a presigned URL. This function allows for advanced usage
57-- - for simple cases prefer the `presigned*Url` functions.
63-- - for simple cases prefer the `presigned*Url` functions.
@@ -61,35 +67,36 @@ import Network.Minio.Sign.V4
61--
67--
62-- All extra query parameters or headers are signed, and therefore are
68-- All extra query parameters or headers are signed, and therefore are
63-- required to be sent when the generated URL is actually used.
69-- required to be sent when the generated URL is actually used.
64−makePresignedUrl :: UrlExpiry -> HT.Method -> Maybe Bucket -> Maybe Object
70+makePresignedUrl ::
71+ UrlExpiry ->
72+ HT.Method ->
73+ Maybe Bucket ->
74+ Maybe Object ->
75+ Maybe Region ->
76+ HT.Query ->
65− -> Maybe Region -> HT.Query -> HT.RequestHeaders
77+ HT.RequestHeaders ->
66− -> Minio ByteString
78+ Minio ByteString
67makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
79makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
68− when (expiry > 7*24*3600 || expiry < 0) $
80+ when (expiry > 7 * 24 * 3600 || expiry < 0) $
81+ throwIO $
69− throwM $ MErrVInvalidUrlExpiry expiry
82+ MErrVInvalidUrlExpiry expiry
70−
83+
71− ci <- asks mcConnInfo
84+ let s3ri =
72−
85+ defaultS3ReqInfo
73− let
86+ { riPresignExpirySecs = Just expiry,
74− host = formatBS "{}:{}" (connectHost ci, connectPort ci)
75− hostHeader = (hHost, host)
76− ri = def { riMethod = method
87+ riMethod = method,
77− , riBucket = bucket
88+ riBucket = bucket,
78− , riObject = object
89+ riObject = object,
90+ riRegion = region,
79− , riQueryParams = extraQuery
91+ riQueryParams = extraQuery,
80− , riHeaders = hostHeader : extraHeaders
92+ riHeaders = extraHeaders
81− , riRegion = Just $ maybe (connectRegion ci) identity region
93+ }
82− }
94+
83−
95+ req <- buildRequest s3ri
84− signPairs <- liftIO $ signV4 ci ri (Just expiry)
96+ let uri = NClient.getUri req
85−
97+ uriString = uriToString identity uri ""
86− let
98+
87− qpToAdd = (fmap . fmap) Just signPairs
99+ return $ encodeUtf8 uriString
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
93
100
94-- | Generate a URL with authentication signature to PUT (upload) an
101-- | Generate a URL with authentication signature to PUT (upload) an
95-- object. Any extra headers if passed, are signed, and so they are
102-- object. Any extra headers if passed, are signed, and so they are
@@ -98,11 +105,22 @@ makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
98--
105--
99-- For a list of possible headers to pass, please refer to the PUT
106-- For a list of possible headers to pass, please refer to the PUT
100-- object REST API AWS S3 documentation.
107-- object REST API AWS S3 documentation.
101−presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders
108+presignedPutObjectUrl ::
109+ Bucket ->
110+ Object ->
111+ UrlExpiry ->
112+ HT.RequestHeaders ->
102− -> Minio ByteString
113+ Minio ByteString
103−presignedPutObjectUrl bucket object expirySeconds extraHeaders =
114+presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
115+ region <- asks (Just . connectRegion . mcConnInfo)
104− makePresignedUrl expirySeconds HT.methodPut
116+ makePresignedUrl
117+ expirySeconds
118+ HT.methodPut
119+ (Just bucket)
120+ (Just object)
121+ region
122+ []
105− (Just bucket) (Just object) Nothing [] extraHeaders
123+ extraHeaders
106
124
107-- | Generate a URL with authentication signature to GET (download) an
125-- | Generate a URL with authentication signature to GET (download) an
108-- object. All extra query parameters and headers passed here will be
126-- object. All extra query parameters and headers passed here will be
@@ -113,11 +131,23 @@ presignedPutObjectUrl bucket object expirySeconds extraHeaders =
113--
131--
114-- For a list of possible request parameters and headers, please refer
132-- For a list of possible request parameters and headers, please refer
115-- to the GET object REST API AWS S3 documentation.
133-- to the GET object REST API AWS S3 documentation.
116−presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query
134+presignedGetObjectUrl ::
135+ Bucket ->
136+ Object ->
137+ UrlExpiry ->
138+ HT.Query ->
117− -> HT.RequestHeaders -> Minio ByteString
139+ HT.RequestHeaders ->
140+ Minio ByteString
118−presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders =
141+presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
142+ region <- asks (Just . connectRegion . mcConnInfo)
119− makePresignedUrl expirySeconds HT.methodGet
143+ makePresignedUrl
120− (Just bucket) (Just object) Nothing extraQuery extraHeaders
144+ expirySeconds
145+ HT.methodGet
146+ (Just bucket)
147+ (Just object)
148+ region
149+ extraQuery
150+ extraHeaders
121
151
122-- | Generate a URL with authentication signature to make a HEAD
152-- | Generate a URL with authentication signature to make a HEAD
123-- request on an object. This is used to fetch metadata about an
153-- request on an object. This is used to fetch metadata about an
@@ -126,49 +156,74 @@ presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders =
126--
156--
127-- For a list of possible headers to pass, please refer to the HEAD
157-- For a list of possible headers to pass, please refer to the HEAD
128-- object REST API AWS S3 documentation.
158-- object REST API AWS S3 documentation.
129−presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry
159+presignedHeadObjectUrl ::
160+ Bucket ->
161+ Object ->
162+ UrlExpiry ->
130− -> HT.RequestHeaders -> Minio ByteString
163+ HT.RequestHeaders ->
164+ Minio ByteString
131−presignedHeadObjectUrl bucket object expirySeconds extraHeaders =
165+presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
166+ region <- asks (Just . connectRegion . mcConnInfo)
132− makePresignedUrl expirySeconds HT.methodHead
167+ makePresignedUrl
168+ expirySeconds
169+ HT.methodHead
170+ (Just bucket)
171+ (Just object)
172+ region
173+ []
133− (Just bucket) (Just object) Nothing [] extraHeaders
174+ extraHeaders
134
175
135-- | Represents individual conditions in a Post Policy document.
176-- | Represents individual conditions in a Post Policy document.
136−data PostPolicyCondition = PPCStartsWith Text Text
177+data PostPolicyCondition
178+ = PPCStartsWith Text Text
137− | PPCEquals Text Text
179+ | PPCEquals Text Text
138− | PPCRange Text Int64 Int64
180+ | PPCRange Text Int64 Int64
139− deriving (Show, Eq)
181+ deriving stock (Show, Eq)
140
182
183+{- ORMOLU_DISABLE -}
141instance Json.ToJSON PostPolicyCondition where
184instance Json.ToJSON PostPolicyCondition where
142 toJSON (PPCStartsWith k v) = Json.toJSON ["starts-with", k, v]
185 toJSON (PPCStartsWith k v) = Json.toJSON ["starts-with", k, v]
186+#if MIN_VERSION_aeson(2,0,0)
187+ toJSON (PPCEquals k v) = Json.object [(A.fromText k) .= v]
188+#else
143 toJSON (PPCEquals k v) = Json.object [k .= v]
189 toJSON (PPCEquals k v) = Json.object [k .= v]
190+#endif
144 toJSON (PPCRange k minVal maxVal) =
191 toJSON (PPCRange k minVal maxVal) =
145 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
192 Json.toJSON [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
146
193
147 toEncoding (PPCStartsWith k v) = Json.foldable ["starts-with", k, v]
194 toEncoding (PPCStartsWith k v) = Json.foldable ["starts-with", k, v]
195+#if MIN_VERSION_aeson(2,0,0)
196+ toEncoding (PPCEquals k v) = Json.pairs ((A.fromText k) .= v)
197+#else
148 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
198 toEncoding (PPCEquals k v) = Json.pairs (k .= v)
199+#endif
149 toEncoding (PPCRange k minVal maxVal) =
200 toEncoding (PPCRange k minVal maxVal) =
150 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
201 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
202+{- ORMOLU_ENABLE -}
151
203
152-- | A PostPolicy is required to perform uploads via browser forms.
204-- | A PostPolicy is required to perform uploads via browser forms.
153−data PostPolicy = PostPolicy {
205+data PostPolicy = PostPolicy
154− expiration :: UTCTime
206+ { expiration :: UTCTime,
155− , conditions :: [PostPolicyCondition]
207+ conditions :: [PostPolicyCondition]
208+ }
156− } deriving (Show, Eq)
209+ deriving stock (Show, Eq)
157
210
158instance Json.ToJSON PostPolicy where
211instance Json.ToJSON PostPolicy where
159 toJSON (PostPolicy e c) =
212 toJSON (PostPolicy e c) =
213+ Json.object
160− Json.object $ [ "expiration" .= iso8601TimeFormat e
214+ [ "expiration" .= iso8601TimeFormat e,
161− , "conditions" .= c
215+ "conditions" .= c
162− ]
216+ ]
163 toEncoding (PostPolicy e c) =
217 toEncoding (PostPolicy e c) =
164 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
218 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
165
219
166-- | Possible validation errors when creating a PostPolicy.
220-- | Possible validation errors when creating a PostPolicy.
167−data PostPolicyError = PPEKeyNotSpecified
221+data PostPolicyError
222+ = PPEKeyNotSpecified
168− | PPEBucketNotSpecified
223+ | PPEBucketNotSpecified
169− | PPEConditionKeyEmpty
224+ | PPEConditionKeyEmpty
170− | PPERangeInvalid
225+ | PPERangeInvalid
171− deriving (Eq, Show)
226+ deriving stock (Show, Eq)
172
227
173-- | Set the bucket name that the upload should use.
228-- | Set the bucket name that the upload should use.
174ppCondBucket :: Bucket -> PostPolicyCondition
229ppCondBucket :: Bucket -> PostPolicyCondition
@@ -176,8 +231,10 @@ ppCondBucket = PPCEquals "bucket"
176
231
177-- | Set the content length range constraint with minimum and maximum
232-- | Set the content length range constraint with minimum and maximum
178-- byte count values.
233-- byte count values.
179−ppCondContentLengthRange :: Int64 -> Int64
234+ppCondContentLengthRange ::
235+ Int64 ->
236+ Int64 ->
180− -> PostPolicyCondition
237+ PostPolicyCondition
181ppCondContentLengthRange = PPCRange "content-length-range"
238ppCondContentLengthRange = PPCRange "content-length-range"
182
239
183-- | Set the content-type header for the upload.
240-- | Set the content-type header for the upload.
@@ -200,82 +257,99 @@ ppCondSuccessActionStatus n =
200
257
201-- | This function creates a PostPolicy after validating its
258-- | This function creates a PostPolicy after validating its
202-- arguments.
259-- arguments.
260+newPostPolicy ::
261+ UTCTime ->
203−newPostPolicy :: UTCTime -> [PostPolicyCondition]
262+ [PostPolicyCondition] ->
204− -> Either PostPolicyError PostPolicy
263+ Either PostPolicyError PostPolicy
205newPostPolicy expirationTime conds
264newPostPolicy expirationTime conds
206 -- object name condition must be present
265 -- object name condition must be present
207 | not $ any (keyEquals "key") conds =
266 | not $ any (keyEquals "key") conds =
208 Left PPEKeyNotSpecified
267 Left PPEKeyNotSpecified
209−
210 -- bucket name condition must be present
268 -- bucket name condition must be present
211 | not $ any (keyEquals "bucket") conds =
269 | not $ any (keyEquals "bucket") conds =
212 Left PPEBucketNotSpecified
270 Left PPEBucketNotSpecified
213−
214 -- a condition with an empty key is invalid
271 -- a condition with an empty key is invalid
215 | any (keyEquals "") conds || any isEmptyRangeKey conds =
272 | any (keyEquals "") conds || any isEmptyRangeKey conds =
216 Left PPEConditionKeyEmpty
273 Left PPEConditionKeyEmpty
217−
218 -- invalid range check
274 -- invalid range check
219 | any isInvalidRange conds =
275 | any isInvalidRange conds =
220 Left PPERangeInvalid
276 Left PPERangeInvalid
221−
222 -- all good!
277 -- all good!
223 | otherwise =
278 | otherwise =
224 return $ PostPolicy expirationTime conds
279 return $ PostPolicy expirationTime conds
225−
226 where
280 where
227 keyEquals k' (PPCStartsWith k _) = k == k'
281 keyEquals k' (PPCStartsWith k _) = k == k'
228− keyEquals k' (PPCEquals k _) = k == k'
282+ keyEquals k' (PPCEquals k _) = k == k'
229− keyEquals _ _ = False
283+ keyEquals _ _ = False
230−
231 isEmptyRangeKey (PPCRange k _ _) = k == ""
284 isEmptyRangeKey (PPCRange k _ _) = k == ""
232− isEmptyRangeKey _ = False
285+ isEmptyRangeKey _ = False
233−
234 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
286 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
235− isInvalidRange _ = False
287+ isInvalidRange _ = False
236
288
237-- | Convert Post Policy to a string (e.g. for printing).
289-- | Convert Post Policy to a string (e.g. for printing).
238showPostPolicy :: PostPolicy -> ByteString
290showPostPolicy :: PostPolicy -> ByteString
239−showPostPolicy = toS . Json.encode
291+showPostPolicy = toStrictBS . Json.encode
240
292
241-- | Generate a presigned URL and POST policy to upload files via a
293-- | Generate a presigned URL and POST policy to upload files via a
242-- browser. On success, this function returns a URL and POST
294-- browser. On success, this function returns a URL and POST
243-- form-data.
295-- form-data.
244−presignedPostPolicy :: PostPolicy
296+presignedPostPolicy ::
297+ PostPolicy ->
245− -> Minio (ByteString, Map.Map Text ByteString)
298+ Minio (ByteString, H.HashMap Text ByteString)
246presignedPostPolicy p = do
299presignedPostPolicy p = do
247 ci <- asks mcConnInfo
300 ci <- asks mcConnInfo
248− signTime <- liftIO $ Time.getCurrentTime
301+ signTime <- liftIO Time.getCurrentTime
249−
302+ mgr <- asks mcConnManager
250− let
303+ cv <- liftIO $ getCredential (connectCreds ci) (getEndpoint ci) mgr
304+
251− extraConditions =
305+ let extraConditions signParams =
252− [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime)
306+ [ PPCEquals "x-amz-date" (toText $ awsTimeFormat signTime),
253− , PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256"
307+ PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
308+ PPCEquals
254− , PPCEquals "x-amz-credential"
309+ "x-amz-credential"
255− (T.intercalate "/" [connectAccessKey ci,
310+ ( T.intercalate
256− decodeUtf8 $ mkScope signTime region])
311+ "/"
312+ [ coerce $ cvAccessKey cv,
313+ decodeUtf8 $ credentialScope signParams
314+ ]
315+ )
257− ]
316+ ]
258− ppWithCreds = p {
317+ ppWithCreds signParams =
318+ p
259− conditions = conditions p ++ extraConditions
319+ { conditions = conditions p ++ extraConditions signParams
260− }
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
261− signData = signV4PostPolicy (showPostPolicy ppWithCreds)
331+ signData = signV4PostPolicy (showPostPolicy $ ppWithCreds sp) sp
262− signTime ci
263−
264− -- compute form-data
332+ -- compute form-data
265− mkPair (PPCStartsWith k v) = Just (k, v)
333+ mkPair (PPCStartsWith k v) = Just (k, v)
266− mkPair (PPCEquals k v) = Just (k, v)
334+ mkPair (PPCEquals k v) = Just (k, v)
267− mkPair _ = Nothing
335+ mkPair _ = Nothing
268− formFromPolicy = Map.map toS $ Map.fromList $ catMaybes $
336+ formFromPolicy =
269− mkPair <$> conditions ppWithCreds
337+ H.map encodeUtf8 $
338+ H.fromList $
339+ mapMaybe
340+ mkPair
341+ (conditions $ ppWithCreds sp)
270− formData = formFromPolicy `Map.union` signData
342+ formData = formFromPolicy `H.union` signData
271−
272− -- compute POST upload URL
343+ -- compute POST upload URL
273− bucket = Map.findWithDefault "" "bucket" formData
344+ bucket = H.lookupDefault "" "bucket" formData
274− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
345+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
275− host = formatBS "{}:{}" (connectHost ci, connectPort ci)
346+ url =
276− region = connectRegion ci
347+ toStrictBS $
277−
278− url = toS $ toLazyByteString $ scheme <> byteString host <>
348+ toLazyByteString $
349+ scheme
350+ <> byteString (getHostAddr ci)
351+ <> byteString "/"
279− byteString "/" <> byteString (toS bucket) <> byteString "/"
352+ <> byteString bucket
353+ <> byteString "/"
280
354
281 return (url, formData)
355 return (url, formData)
src/Network/Minio/PutObject.hs+102−160
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,43 +15,24 @@
15--
15--
16
16
17module Network.Minio.PutObject
17module Network.Minio.PutObject
18− (
19− putObjectInternal
18+ ( putObjectInternal,
20− , ObjectData(..)
19+ ObjectData (..),
21− , selectPartSizes
20+ selectPartSizes,
22− , copyObjectInternal
21+ )
23− , selectCopyRanges
24− , minPartSize
25− ) where
22+where
26−
23+
27−
24+import Conduit (takeC)
25+import qualified Conduit as C
28−import qualified Data.Conduit as C
26+import qualified Data.ByteString.Lazy as LBS
29import qualified Data.Conduit.Binary as CB
27import qualified Data.Conduit.Binary as CB
30import qualified Data.Conduit.Combinators as CC
28import qualified Data.Conduit.Combinators as CC
31import qualified Data.Conduit.List as CL
29import qualified Data.Conduit.List as CL
32import qualified Data.List as List
30import qualified Data.List as List
33−
34−import Lib.Prelude
31+import Lib.Prelude
35−
36−import Network.Minio.Data
32+import Network.Minio.Data
37−import Network.Minio.Errors
33+import Network.Minio.Errors
38−import Network.Minio.S3API
34+import Network.Minio.S3API
39−import Network.Minio.Utils
35+import Network.Minio.Utils
40−
41−
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
55
36
56-- | A data-type to represent the source data for an object. A
37-- | A data-type to represent the source data for an object. A
57-- file-path or a producer-conduit may be provided.
38-- file-path or a producer-conduit may be provided.
@@ -62,70 +43,90 @@ maxMultipartParts = 10000
62--
43--
63-- For streams also, a size may be provided. This is useful to limit
44-- For streams also, a size may be provided. This is useful to limit
64-- the input - if it is not provided, upload will continue until the
45-- the input - if it is not provided, upload will continue until the
65−-- stream ends or the object reaches `maxObjectsize` size.
46+-- stream ends or the object reaches `maxObjectSize` size.
66−data ObjectData m =
47+data ObjectData m
67− ODFile FilePath (Maybe Int64) -- ^ Takes filepath and optional size.
48+ = -- | Takes filepath and optional
68− | ODStream (C.Producer m ByteString) (Maybe Int64) -- ^ Pass size in bytes as maybe if known.
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)
69
57
70-- | Put an object from ObjectData. This high-level API handles
58-- | Put an object from ObjectData. This high-level API handles
71-- objects of all sizes, and even if the object size is unknown.
59-- objects of all sizes, and even if the object size is unknown.
72−putObjectInternal :: Bucket -> Object -> ObjectData Minio -> Minio ETag
60+putObjectInternal ::
61+ Bucket ->
62+ Object ->
63+ PutObjectOptions ->
64+ ObjectData Minio ->
65+ Minio ETag
73−putObjectInternal b o (ODStream src sizeMay) = sequentialMultipartUpload b o sizeMay src
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
74−putObjectInternal b o (ODFile fp sizeMay) = do
78+putObjectInternal b o opts (ODFile fp sizeMay) = do
75 hResE <- withNewHandle fp $ \h ->
79 hResE <- withNewHandle fp $ \h ->
76− liftM2 (,) (isHandleSeekable h) (getFileSize h)
80+ liftA2 (,) (isHandleSeekable h) (getFileSize h)
77
81
78− (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return
82+ (isSeekable, handleSizeMay) <-
83+ either
84+ (const $ return (False, Nothing))
85+ return
79− hResE
86+ hResE
80
87
81 -- prefer given size to queried size.
88 -- prefer given size to queried size.
82 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
89 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
83
90
84 case finalSizeMay of
91 case finalSizeMay of
85− -- unable to get size, so assume non-seekable file and max-object size
92+ -- unable to get size, so assume non-seekable file
86− Nothing -> sequentialMultipartUpload b o (Just maxObjectSize) $
93+ Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
87− CB.sourceFile fp
88−
89 -- got file size, so check for single/multipart upload
94 -- got file size, so check for single/multipart upload
90 Just size ->
95 Just size ->
96+ if
91− if | size <= 64 * oneMiB -> either throwM return =<<
97+ | size <= 64 * oneMiB ->
98+ either throwIO return
92− withNewHandle fp (\h -> putObjectSingle b o [] h 0 size)
99+ =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
93− | size > maxObjectSize -> throwM $ MErrVPutSizeExceeded size
100+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
94− | isSeekable -> parallelMultipartUpload b o fp size
101+ | isSeekable -> parallelMultipartUpload b o opts fp size
102+ | otherwise ->
95− | otherwise -> sequentialMultipartUpload b o (Just size) $
103+ sequentialMultipartUpload b o opts (Just size) $
96− CB.sourceFile fp
104+ CB.sourceFile fp
97−
105+
98−-- | Select part sizes - the logic is that the minimum part-size will
106+parallelMultipartUpload ::
99−-- be 64MiB.
107+ Bucket ->
100−selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
108+ Object ->
101−selectPartSizes size = uncurry (List.zip3 [1..]) $
109+ PutObjectOptions ->
102− List.unzip $ loop 0 size
110+ FilePath ->
103− where
111+ Int64 ->
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
112+ Minio ETag
117−parallelMultipartUpload b o filePath size = do
113+parallelMultipartUpload b o opts filePath size = do
118 -- get a new upload id.
114 -- get a new upload id.
119− uploadId <- newMultipartUpload b o []
115+ uploadId <- newMultipartUpload b o (pooToHeaders opts)
120
116
121 let partSizeInfo = selectPartSizes size
117 let partSizeInfo = selectPartSizes size
122
118
119+ let threads = fromMaybe 10 $ pooNumThreads opts
120+
123− -- perform upload with 10 threads
121+ -- perform upload with 'threads' threads
122+ uploadedPartsE <-
124− uploadedPartsE <- limitedMapConcurrently 10
123+ limitedMapConcurrently
124+ (fromIntegral threads)
125− (uploadPart uploadId) partSizeInfo
125+ (uploadPart uploadId)
126+ partSizeInfo
126
127
127 -- if there were any errors, rethrow exception.
128 -- if there were any errors, rethrow exception.
128− mapM_ throwM $ lefts uploadedPartsE
129+ mapM_ throwIO $ lefts uploadedPartsE
129
130
130 -- if we get here, all parts were successfully uploaded.
131 -- if we get here, all parts were successfully uploaded.
131 completeMultipartUpload b o uploadId $ rights uploadedPartsE
132 completeMultipartUpload b o uploadId $ rights uploadedPartsE
@@ -136,96 +137,37 @@ parallelMultipartUpload b o filePath size = do
136 putObjectPart b o uploadId partNum [] payload
137 putObjectPart b o uploadId partNum [] payload
137
138
138-- | Upload multipart object from conduit source sequentially
139-- | Upload multipart object from conduit source sequentially
139−sequentialMultipartUpload :: Bucket -> Object -> Maybe Int64
140+sequentialMultipartUpload ::
141+ Bucket ->
142+ Object ->
143+ PutObjectOptions ->
144+ Maybe Int64 ->
145+ C.ConduitM () ByteString Minio () ->
140− -> C.Producer Minio ByteString -> Minio ETag
146+ Minio ETag
141−sequentialMultipartUpload b o sizeMay src = do
147+sequentialMultipartUpload b o opts sizeMay src = do
142 -- get a new upload id.
148 -- get a new upload id.
143− uploadId <- newMultipartUpload b o []
149+ uploadId <- newMultipartUpload b o (pooToHeaders opts)
144
150
145 -- upload parts in loop
151 -- upload parts in loop
146 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
152 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
147 (pnums, _, sizes) = List.unzip3 partSizes
153 (pnums, _, sizes) = List.unzip3 partSizes
148− uploadedParts <- src
154+ uploadedParts <-
155+ C.runConduit $
156+ src
149− C..| chunkBSConduit sizes
157+ C..| chunkBSConduit (map fromIntegral sizes)
150− C..| CL.map PayloadBS
158+ C..| CL.map PayloadBS
151− C..| uploadPart' uploadId pnums
159+ C..| uploadPart' uploadId pnums
152− C.$$ CC.sinkList
160+ C..| CC.sinkList
153
161
154 -- complete multipart upload
162 -- complete multipart upload
155 completeMultipartUpload b o uploadId uploadedParts
163 completeMultipartUpload b o uploadId uploadedParts
156−
157 where
164 where
158 uploadPart' _ [] = return ()
165 uploadPart' _ [] = return ()
159− uploadPart' uid (pn:pns) = do
166+ uploadPart' uid (pn : pns) = do
160 payloadMay <- C.await
167 payloadMay <- C.await
161 case payloadMay of
168 case payloadMay of
162 Nothing -> return ()
169 Nothing -> return ()
170+ Just payload -> do
163− Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload
171+ pinfo <- lift $ putObjectPart b o uid pn [] payload
164− C.yield pinfo
172+ C.yield pinfo
165− uploadPart' uid pns
173+ 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+544−281
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,409 +14,672 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17+-- |
18+-- Module: Network.Minio.S3API
19+-- Copyright: (c) 2017-2023 MinIO Dev Team
20+-- License: Apache 2.0
21+-- Maintainer: MinIO Dev Team <dev@min.io>
22+--
23+-- Lower-level API for S3 compatible object stores. Start with @Network.Minio@
24+-- and use this only if needed.
17module Network.Minio.S3API
25module Network.Minio.S3API
18− (
19− Region
26+ ( Region,
20− , getLocation
27+ getLocation,
21−
28+
22− -- * Listing buckets
29+ -- * Listing buckets
30+
23− --------------------
31+ --------------------
24− , getService
32+ getService,
25−
33+
26− -- * Listing objects
34+ -- * Listing objects
35+
27− --------------------
36+ --------------------
28− , ListObjectsResult(..)
37+ ListObjectsResult (..),
29− , ListObjectsV1Result(..)
38+ ListObjectsV1Result (..),
30− , listObjects'
39+ listObjects',
31− , listObjectsV1'
40+ listObjectsV1',
32−
41+
33− -- * Retrieving buckets
42+ -- * Retrieving buckets
34− , headBucket
43+ headBucket,
35−
44+
36− -- * Retrieving objects
45+ -- * Retrieving objects
46+
37− -----------------------
47+ -----------------------
38− , getObject'
48+ getObject',
39− , headObject
49+ headObject,
40−
50+
41− -- * Creating buckets and objects
51+ -- * Creating buckets and objects
52+
42− ---------------------------------
53+ ---------------------------------
43− , putBucket
54+ putBucket,
44− , ETag
55+ ETag,
56+ maxSinglePutObjectSizeBytes,
45− , putObjectSingle
57+ putObjectSingle',
58+ putObjectSingle,
46− , copyObjectSingle
59+ copyObjectSingle,
47−
60+
48− -- * Multipart Upload APIs
61+ -- * Multipart Upload APIs
62+
49− --------------------------
63+ --------------------------
50− , UploadId
64+ UploadId,
51− , PartTuple
65+ PartTuple,
52− , Payload(..)
66+ Payload (..),
53− , PartNumber
67+ PartNumber,
54− , CopyPartSource(..)
55− , newMultipartUpload
68+ newMultipartUpload,
56− , putObjectPart
69+ putObjectPart,
57− , copyObjectPart
70+ copyObjectPart,
58− , completeMultipartUpload
71+ completeMultipartUpload,
59− , abortMultipartUpload
72+ abortMultipartUpload,
60− , ListUploadsResult(..)
73+ ListUploadsResult (..),
61− , listIncompleteUploads'
74+ listIncompleteUploads',
62− , ListPartsResult(..)
75+ ListPartsResult (..),
63− , listIncompleteParts'
76+ listIncompleteParts',
64−
77+
65− -- * Deletion APIs
78+ -- * Deletion APIs
79+
66− --------------------------
80+ --------------------------
67− , deleteBucket
81+ deleteBucket,
68− , deleteObject
82+ deleteObject,
69−
83+
70− -- * Presigned Operations
84+ -- * Presigned Operations
85+
71− -----------------------------
86+ -----------------------------
72− , module Network.Minio.PresignedOperations
87+ module Network.Minio.PresignedOperations,
73−
88+
89+ -- ** Bucket Policies
90+ getBucketPolicy,
91+ setBucketPolicy,
92+
74− -- * Bucket Notifications
93+ -- * Bucket Notifications
94+
75− -------------------------
95+ -------------------------
76− , Notification(..)
96+ Notification (..),
77− , NotificationConfig(..)
97+ NotificationConfig (..),
78− , Arn
98+ Arn,
79− , Event(..)
99+ Event (..),
80− , Filter(..)
100+ Filter (..),
81− , FilterKey(..)
101+ FilterKey (..),
82− , FilterRules(..)
102+ FilterRules (..),
83− , FilterRule(..)
103+ FilterRule (..),
84− , getBucketNotification
104+ getBucketNotification,
85− , putBucketNotification
105+ putBucketNotification,
86− , removeAllBucketNotification
106+ removeAllBucketNotification,
107+ )
87− ) where
108+where
88−
109+
89−import Control.Monad.Catch (catches, Handler(..))
110+import qualified Data.ByteString as BS
90−import qualified Data.Conduit as C
111+import qualified Data.Text as T
91−import Data.Default (def)
112+import Lib.Prelude
92import qualified Network.HTTP.Conduit as NC
113import qualified Network.HTTP.Conduit as NC
93import qualified Network.HTTP.Types as HT
114import qualified Network.HTTP.Types as HT
94−import Network.HTTP.Types.Status (status404)
115+import Network.HTTP.Types.Status (status404)
95−
96−import Lib.Prelude hiding (catches)
97−
98−import Network.Minio.API
116+import Network.Minio.API
117+import Network.Minio.APICommon
99−import Network.Minio.Data
118+import Network.Minio.Data
100−import Network.Minio.Errors
119+import Network.Minio.Errors
120+import Network.Minio.PresignedOperations
101−import Network.Minio.Utils
121+import Network.Minio.Utils
102−import Network.Minio.XmlGenerator
122+import Network.Minio.XmlGenerator
103−import Network.Minio.XmlParser
123+import Network.Minio.XmlParser
104−import Network.Minio.PresignedOperations
124+import UnliftIO (Handler (Handler))
105−
106
125
107-- | Fetch all buckets from the service.
126-- | Fetch all buckets from the service.
108getService :: Minio [BucketInfo]
127getService :: Minio [BucketInfo]
109getService = do
128getService = do
129+ resp <-
110− resp <- executeRequest $ def {
130+ executeRequest $
131+ defaultS3ReqInfo
111− riNeedsLocation = False
132+ { riNeedsLocation = False
112− }
133+ }
113 parseListBuckets $ NC.responseBody resp
134 parseListBuckets $ NC.responseBody resp
114
135
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+
115−-- | GET an object from the service and return the response headers
150+-- | GET an object from the service and return parsed ObjectInfo and a
116−-- and a conduit source for the object content
151+-- conduit source for the object content
117−getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
152+getObject' ::
118− -> Minio ([HT.Header], C.ResumableSource Minio ByteString)
153+ Bucket ->
154+ Object ->
155+ HT.Query ->
156+ [HT.Header] ->
157+ Minio GetObjectResponse
119getObject' bucket object queryParams headers = do
158getObject' bucket object queryParams headers = do
120 resp <- mkStreamRequest reqInfo
159 resp <- mkStreamRequest reqInfo
160+ let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
161+ objInfo <-
162+ maybe
163+ (throwIO MErrVInvalidObjectInfoResponse)
164+ return
165+ objInfoMaybe
166+ return $
167+ GetObjectResponse
168+ { gorObjectInfo = objInfo,
121− return (NC.responseHeaders resp, NC.responseBody resp)
169+ gorObjectStream = NC.responseBody resp
170+ }
122 where
171 where
172+ reqInfo =
173+ defaultS3ReqInfo
123− reqInfo = def { riBucket = Just bucket
174+ { riBucket = Just bucket,
124− , riObject = Just object
175+ riObject = Just object,
125− , riQueryParams = queryParams
176+ riQueryParams = queryParams,
126− , riHeaders = headers
177+ riHeaders =
178+ headers
179+ -- This header is required for safety as otherwise http-client,
180+ -- sends Accept-Encoding: gzip, and the server may actually gzip
181+ -- body. In that case Content-Length header will be missing.
182+ <> [("Accept-Encoding", "identity")]
127− }
183+ }
128
184
129-- | Creates a bucket via a PUT bucket call.
185-- | Creates a bucket via a PUT bucket call.
130putBucket :: Bucket -> Region -> Minio ()
186putBucket :: Bucket -> Region -> Minio ()
131−putBucket bucket location = void $
187+putBucket bucket location = do
132− executeRequest $
188+ ns <- asks getSvcNamespace
189+ void $
190+ executeRequest $
191+ defaultS3ReqInfo
133− def { riMethod = HT.methodPut
192+ { riMethod = HT.methodPut,
134− , riBucket = Just bucket
193+ riBucket = Just bucket,
135− , riPayload = PayloadBS $ mkCreateBucketConfig location
194+ riPayload = PayloadBS $ mkCreateBucketConfig ns location,
136− , riNeedsLocation = False
195+ riNeedsLocation = False
137 }
196 }
138
197
139-- | Single PUT object size.
198-- | Single PUT object size.
140maxSinglePutObjectSizeBytes :: Int64
199maxSinglePutObjectSizeBytes :: Int64
141maxSinglePutObjectSizeBytes = 5 * 1024 * 1024 * 1024
200maxSinglePutObjectSizeBytes = 5 * 1024 * 1024 * 1024
142
201
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+
143-- | PUT an object into the service. This function performs a single
232-- | PUT an object into the service. This function performs a single
144-- PUT object call, and so can only transfer objects upto 5GiB.
233-- PUT object call, and so can only transfer objects upto 5GiB.
145−putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
234+putObjectSingle ::
235+ Bucket ->
236+ Object ->
237+ [HT.Header] ->
238+ Handle ->
239+ Int64 ->
240+ Int64 ->
146− -> Int64 -> Minio ETag
241+ Minio ETag
147putObjectSingle bucket object headers h offset size = do
242putObjectSingle bucket object headers h offset size = do
148 -- check length is within single PUT object size.
243 -- check length is within single PUT object size.
149 when (size > maxSinglePutObjectSizeBytes) $
244 when (size > maxSinglePutObjectSizeBytes) $
245+ throwIO $
150− throwM $ MErrVSinglePUTSizeExceeded size
246+ MErrVSinglePUTSizeExceeded size
151
247
152 -- content-length header is automatically set by library.
248 -- content-length header is automatically set by library.
249+ let payload = mkStreamingPayload $ PayloadH h offset size
250+ resp <-
153− resp <- executeRequest $
251+ executeRequest $
252+ defaultS3ReqInfo
154− def { riMethod = HT.methodPut
253+ { riMethod = HT.methodPut,
155− , riBucket = Just bucket
254+ riBucket = Just bucket,
156− , riObject = Just object
255+ riObject = Just object,
157− , riHeaders = headers
256+ riHeaders = headers,
158− , riPayload = PayloadH h offset size
257+ riPayload = payload
159− }
258+ }
160
259
161 let rheaders = NC.responseHeaders resp
260 let rheaders = NC.responseHeaders resp
162 etag = getETagHeader rheaders
261 etag = getETagHeader rheaders
163 maybe
262 maybe
164− (throwM MErrVETagHeaderNotFound)
263+ (throwIO MErrVETagHeaderNotFound)
165− return etag
264+ return
265+ etag
166
266
167-- | List objects in a bucket matching prefix up to delimiter,
267-- | List objects in a bucket matching prefix up to delimiter,
168-- starting from nextMarker.
268-- starting from nextMarker.
169−listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
269+listObjectsV1' ::
270+ Bucket ->
271+ Maybe Text ->
272+ Maybe Text ->
273+ Maybe Text ->
274+ Maybe Int ->
170− -> Minio ListObjectsV1Result
275+ Minio ListObjectsV1Result
171listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
276listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
277+ resp <-
278+ executeRequest $
279+ defaultS3ReqInfo
172− resp <- executeRequest $ def { riMethod = HT.methodGet
280+ { riMethod = HT.methodGet,
173− , riBucket = Just bucket
281+ riBucket = Just bucket,
174− , riQueryParams = mkOptionalParams params
282+ riQueryParams = mkOptionalParams params
175− }
283+ }
176 parseListObjectsV1Response $ NC.responseBody resp
284 parseListObjectsV1Response $ NC.responseBody resp
177 where
285 where
178− params = [
286+ params =
179− ("marker", nextMarker)
287+ [ ("marker", nextMarker),
180− , ("prefix", prefix)
288+ ("prefix", prefix),
181− , ("delimiter", delimiter)
289+ ("delimiter", delimiter),
182− , ("max-keys", show <$> maxKeys)
290+ ("max-keys", show <$> maxKeys)
183 ]
291 ]
184
292
185-- | List objects in a bucket matching prefix up to delimiter,
293-- | List objects in a bucket matching prefix up to delimiter,
186-- starting from nextToken.
294-- starting from nextToken.
187−listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
295+listObjects' ::
296+ Bucket ->
297+ Maybe Text ->
298+ Maybe Text ->
299+ Maybe Text ->
300+ Maybe Int ->
188− -> Minio ListObjectsResult
301+ Minio ListObjectsResult
189listObjects' bucket prefix nextToken delimiter maxKeys = do
302listObjects' bucket prefix nextToken delimiter maxKeys = do
303+ resp <-
304+ executeRequest $
305+ defaultS3ReqInfo
190− resp <- executeRequest $ def { riMethod = HT.methodGet
306+ { riMethod = HT.methodGet,
191− , riBucket = Just bucket
307+ riBucket = Just bucket,
192− , riQueryParams = mkOptionalParams params
308+ riQueryParams = mkOptionalParams params
193− }
309+ }
194 parseListObjectsResponse $ NC.responseBody resp
310 parseListObjectsResponse $ NC.responseBody resp
195 where
311 where
196− params = [
312+ params =
197− ("list-type", Just "2")
313+ [ ("list-type", Just "2"),
198− , ("continuation_token", nextToken)
314+ ("continuation_token", nextToken),
199− , ("prefix", prefix)
315+ ("prefix", prefix),
200− , ("delimiter", delimiter)
316+ ("delimiter", delimiter),
201− , ("max-keys", show <$> maxKeys)
317+ ("max-keys", show <$> maxKeys)
202 ]
318 ]
203
319
204-- | DELETE a bucket from the service.
320-- | DELETE a bucket from the service.
205deleteBucket :: Bucket -> Minio ()
321deleteBucket :: Bucket -> Minio ()
206−deleteBucket bucket = void $
322+deleteBucket bucket =
323+ void $
207− executeRequest $
324+ executeRequest $
325+ defaultS3ReqInfo
208− def { riMethod = HT.methodDelete
326+ { riMethod = HT.methodDelete,
209− , riBucket = Just bucket
327+ riBucket = Just bucket
210 }
328 }
211
329
212-- | DELETE an object from the service.
330-- | DELETE an object from the service.
213deleteObject :: Bucket -> Object -> Minio ()
331deleteObject :: Bucket -> Object -> Minio ()
214−deleteObject bucket object = void $
332+deleteObject bucket object =
333+ void $
215− executeRequest $
334+ executeRequest $
335+ defaultS3ReqInfo
216− def { riMethod = HT.methodDelete
336+ { riMethod = HT.methodDelete,
217− , riBucket = Just bucket
337+ riBucket = Just bucket,
218− , riObject = Just object
338+ riObject = Just object
219 }
339 }
220
340
221-- | Create a new multipart upload.
341-- | Create a new multipart upload.
222newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
342newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
223newMultipartUpload bucket object headers = do
343newMultipartUpload bucket object headers = do
344+ resp <-
345+ executeRequest $
346+ defaultS3ReqInfo
224− resp <- executeRequest $ def { riMethod = HT.methodPost
347+ { riMethod = HT.methodPost,
225− , riBucket = Just bucket
348+ riBucket = Just bucket,
226− , riObject = Just object
349+ riObject = Just object,
227− , riQueryParams = [("uploads", Nothing)]
350+ riQueryParams = [("uploads", Nothing)],
228− , riHeaders = headers
351+ riHeaders = headers
229− }
352+ }
230 parseNewMultipartUpload $ NC.responseBody resp
353 parseNewMultipartUpload $ NC.responseBody resp
231
354
232-- | PUT a part of an object as part of a multipart upload.
355-- | PUT a part of an object as part of a multipart upload.
233−putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
356+putObjectPart ::
357+ Bucket ->
358+ Object ->
359+ UploadId ->
360+ PartNumber ->
361+ [HT.Header] ->
362+ Payload ->
234− -> Payload -> Minio PartTuple
363+ Minio PartTuple
235putObjectPart bucket object uploadId partNumber headers payload = do
364putObjectPart bucket object uploadId partNumber headers payload = do
365+ -- transform payload to conduit to enable streaming signature
366+ let payload' = mkStreamingPayload payload
367+ resp <-
236− resp <- executeRequest $
368+ executeRequest $
369+ defaultS3ReqInfo
237− def { riMethod = HT.methodPut
370+ { riMethod = HT.methodPut,
238− , riBucket = Just bucket
371+ riBucket = Just bucket,
239− , riObject = Just object
372+ riObject = Just object,
240− , riQueryParams = mkOptionalParams params
373+ riQueryParams = mkOptionalParams params,
241− , riHeaders = headers
374+ riHeaders = headers,
242− , riPayload = payload
375+ riPayload = payload'
243− }
376+ }
244 let rheaders = NC.responseHeaders resp
377 let rheaders = NC.responseHeaders resp
245 etag = getETagHeader rheaders
378 etag = getETagHeader rheaders
246 maybe
379 maybe
247− (throwM MErrVETagHeaderNotFound)
380+ (throwIO MErrVETagHeaderNotFound)
248− (return . (partNumber, )) etag
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
249 where
402 where
250− params = [
403+ names =
251− ("uploadId", Just uploadId)
404+ [ "x-amz-copy-source-if-match",
252− , ("partNumber", Just $ show partNumber)
405+ "x-amz-copy-source-if-none-match",
406+ "x-amz-copy-source-if-unmodified-since",
407+ "x-amz-copy-source-if-modified-since"
253 ]
408 ]
409+ values =
410+ mapMaybe
411+ (fmap encodeUtf8 . (srcInfo &))
412+ [ srcIfMatch,
413+ srcIfNoneMatch,
414+ fmap formatRFC1123 . srcIfUnmodifiedSince,
415+ fmap formatRFC1123 . srcIfModifiedSince
416+ ]
417+ rangeHdr =
418+ maybe [] ((\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) . toByteRange) (srcRange srcInfo)
419+ toByteRange :: (Int64, Int64) -> HT.ByteRange
420+ toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
254
421
255-- | Performs server-side copy of an object or part of an object as an
422-- | Performs server-side copy of an object or part of an object as an
256-- upload part of an ongoing multi-part upload.
423-- upload part of an ongoing multi-part upload.
257−copyObjectPart :: Bucket -> Object -> CopyPartSource -> UploadId
424+copyObjectPart ::
425+ DestinationInfo ->
426+ SourceInfo ->
427+ UploadId ->
428+ PartNumber ->
429+ [HT.Header] ->
258− -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
430+ Minio (ETag, UTCTime)
259−copyObjectPart bucket object cps uploadId partNumber headers = do
431+copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
432+ resp <-
260− resp <- executeRequest $
433+ executeRequest $
434+ defaultS3ReqInfo
261− def { riMethod = HT.methodPut
435+ { riMethod = HT.methodPut,
262− , riBucket = Just bucket
436+ riBucket = Just $ dstBucket dstInfo,
263− , riObject = Just object
437+ riObject = Just $ dstObject dstInfo,
264− , riQueryParams = mkOptionalParams params
438+ riQueryParams = mkOptionalParams params,
265− , riHeaders = headers ++ cpsToHeaders cps
439+ riHeaders = headers ++ srcInfoToHeaders srcInfo
266− }
440+ }
267
441
268 parseCopyObjectResponse $ NC.responseBody resp
442 parseCopyObjectResponse $ NC.responseBody resp
269 where
443 where
270− params = [
444+ params =
271− ("uploadId", Just uploadId)
445+ [ ("uploadId", Just uploadId),
272− , ("partNumber", Just $ show partNumber)
446+ ("partNumber", Just $ show partNumber)
273 ]
447 ]
274
448
275-- | Performs server-side copy of an object that is upto 5GiB in
449-- | Performs server-side copy of an object that is upto 5GiB in
276-- size. If the object is greater than 5GiB, this function throws the
450-- size. If the object is greater than 5GiB, this function throws the
277-- error returned by the server.
451-- error returned by the server.
278−copyObjectSingle :: Bucket -> Object -> CopyPartSource -> [HT.Header]
452+copyObjectSingle ::
453+ Bucket ->
454+ Object ->
455+ SourceInfo ->
456+ [HT.Header] ->
279− -> Minio (ETag, UTCTime)
457+ Minio (ETag, UTCTime)
280−copyObjectSingle bucket object cps headers = do
458+copyObjectSingle bucket object srcInfo headers = do
281− -- validate that cpSourceRange is Nothing for this API.
459+ -- validate that srcRange is Nothing for this API.
282− when (isJust $ cpSourceRange cps) $
460+ when (isJust $ srcRange srcInfo) $
283− throwM MErrVCopyObjSingleNoRangeAccepted
461+ throwIO MErrVCopyObjSingleNoRangeAccepted
462+ resp <-
284− resp <- executeRequest $
463+ executeRequest $
464+ defaultS3ReqInfo
285− def { riMethod = HT.methodPut
465+ { riMethod = HT.methodPut,
286− , riBucket = Just bucket
466+ riBucket = Just bucket,
287− , riObject = Just object
467+ riObject = Just object,
288− , riHeaders = headers ++ cpsToHeaders cps
468+ riHeaders = headers ++ srcInfoToHeaders srcInfo
289− }
469+ }
290 parseCopyObjectResponse $ NC.responseBody resp
470 parseCopyObjectResponse $ NC.responseBody resp
291
471
292-- | Complete a multipart upload.
472-- | Complete a multipart upload.
293−completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
473+completeMultipartUpload ::
474+ Bucket ->
475+ Object ->
476+ UploadId ->
477+ [PartTuple] ->
294− -> Minio ETag
478+ Minio ETag
295completeMultipartUpload bucket object uploadId partTuple = do
479completeMultipartUpload bucket object uploadId partTuple = do
480+ resp <-
296− resp <- executeRequest $
481+ executeRequest $
482+ defaultS3ReqInfo
297− def { riMethod = HT.methodPost
483+ { riMethod = HT.methodPost,
298− , riBucket = Just bucket
484+ riBucket = Just bucket,
299− , riObject = Just object
485+ riObject = Just object,
300− , riQueryParams = mkOptionalParams params
486+ riQueryParams = mkOptionalParams params,
301− , riPayload = PayloadBS $
487+ riPayload =
488+ PayloadBS $
302− mkCompleteMultipartUploadRequest partTuple
489+ mkCompleteMultipartUploadRequest partTuple
303− }
490+ }
304 parseCompleteMultipartUploadResponse $ NC.responseBody resp
491 parseCompleteMultipartUploadResponse $ NC.responseBody resp
305 where
492 where
306 params = [("uploadId", Just uploadId)]
493 params = [("uploadId", Just uploadId)]
307
494
308-- | Abort a multipart upload.
495-- | Abort a multipart upload.
309abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
496abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
310−abortMultipartUpload bucket object uploadId = void $
497+abortMultipartUpload bucket object uploadId =
498+ void $
499+ executeRequest $
500+ defaultS3ReqInfo
311− executeRequest $ def { riMethod = HT.methodDelete
501+ { riMethod = HT.methodDelete,
312− , riBucket = Just bucket
502+ riBucket = Just bucket,
313− , riObject = Just object
503+ riObject = Just object,
314− , riQueryParams = mkOptionalParams params
504+ riQueryParams = mkOptionalParams params
315− }
505+ }
316 where
506 where
317 params = [("uploadId", Just uploadId)]
507 params = [("uploadId", Just uploadId)]
318
508
319-- | List incomplete multipart uploads.
509-- | List incomplete multipart uploads.
320−listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
510+listIncompleteUploads' ::
511+ Bucket ->
512+ Maybe Text ->
513+ Maybe Text ->
514+ Maybe Text ->
515+ Maybe Text ->
516+ Maybe Int ->
321− -> Maybe Text -> Maybe Int -> Minio ListUploadsResult
517+ Minio ListUploadsResult
322listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
518listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
519+ resp <-
520+ executeRequest $
521+ defaultS3ReqInfo
323− resp <- executeRequest $ def { riMethod = HT.methodGet
522+ { riMethod = HT.methodGet,
324− , riBucket = Just bucket
523+ riBucket = Just bucket,
325− , riQueryParams = params
524+ riQueryParams = params
326− }
525+ }
327 parseListUploadsResponse $ NC.responseBody resp
526 parseListUploadsResponse $ NC.responseBody resp
328 where
527 where
329 -- build query params
528 -- build query params
529+ params =
530+ ("uploads", Nothing)
330− params = ("uploads", Nothing) : mkOptionalParams
531+ : mkOptionalParams
331− [ ("prefix", prefix)
532+ [ ("prefix", prefix),
332− , ("delimiter", delimiter)
533+ ("delimiter", delimiter),
333− , ("key-marker", keyMarker)
534+ ("key-marker", keyMarker),
334− , ("upload-id-marker", uploadIdMarker)
535+ ("upload-id-marker", uploadIdMarker),
335− , ("max-uploads", show <$> maxKeys)
536+ ("max-uploads", show <$> maxKeys)
336− ]
537+ ]
337−
338
538
339-- | List parts of an ongoing multipart upload.
539-- | List parts of an ongoing multipart upload.
340−listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
540+listIncompleteParts' ::
541+ Bucket ->
542+ Object ->
543+ UploadId ->
544+ Maybe Text ->
545+ Maybe Text ->
341− -> Maybe Text -> Minio ListPartsResult
546+ Minio ListPartsResult
342listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
547listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
548+ resp <-
549+ executeRequest $
550+ defaultS3ReqInfo
343− resp <- executeRequest $ def { riMethod = HT.methodGet
551+ { riMethod = HT.methodGet,
344− , riBucket = Just bucket
552+ riBucket = Just bucket,
345− , riObject = Just object
553+ riObject = Just object,
346− , riQueryParams = mkOptionalParams params
554+ riQueryParams = mkOptionalParams params
347− }
555+ }
348 parseListPartsResponse $ NC.responseBody resp
556 parseListPartsResponse $ NC.responseBody resp
349 where
557 where
350 -- build optional query params
558 -- build optional query params
351− params = [
559+ params =
352− ("uploadId", Just uploadId)
560+ [ ("uploadId", Just uploadId),
353− , ("part-number-marker", partNumMarker)
561+ ("part-number-marker", partNumMarker),
354− , ("max-parts", maxParts)
562+ ("max-parts", maxParts)
355 ]
563 ]
356
564
357-- | Get metadata of an object.
565-- | Get metadata of an object.
358−headObject :: Bucket -> Object -> Minio ObjectInfo
566+headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
359−headObject bucket object = do
567+headObject bucket object reqHeaders = do
568+ resp <-
569+ executeRequest $
570+ defaultS3ReqInfo
360− resp <- executeRequest $ def { riMethod = HT.methodHead
571+ { riMethod = HT.methodHead,
361− , riBucket = Just bucket
572+ riBucket = Just bucket,
362− , riObject = Just object
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")]
363− }
580+ }
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 $
581+ maybe (throwIO MErrVInvalidObjectInfoResponse) return $
372− ObjectInfo <$> Just object <*> modTime <*> etag <*> size
582+ parseGetObjectHeaders object $
373−
583+ NC.responseHeaders resp
374−
375
584
376-- | Query the object store if a given bucket exists.
585-- | Query the object store if a given bucket exists.
377headBucket :: Bucket -> Minio Bool
586headBucket :: Bucket -> Minio Bool
378−headBucket bucket = headBucketEx `catches`
587+headBucket bucket =
588+ headBucketEx
379− [ Handler handleNoSuchBucket
589+ `catches` [ Handler handleNoSuchBucket,
380− , Handler handleStatus404
590+ Handler handleStatus404
381− ]
591+ ]
382−
383 where
592 where
384 handleNoSuchBucket :: ServiceErr -> Minio Bool
593 handleNoSuchBucket :: ServiceErr -> Minio Bool
594+ handleNoSuchBucket e
385− handleNoSuchBucket e | e == NoSuchBucket = return False
595+ | e == NoSuchBucket = return False
386− | otherwise = throwM e
596+ | otherwise = throwIO e
387−
388 handleStatus404 :: NC.HttpException -> Minio Bool
597 handleStatus404 :: NC.HttpException -> Minio Bool
389 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
598 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
390 if NC.responseStatus res == status404
599 if NC.responseStatus res == status404
391− then return False
600+ then return False
392− else throwM e
601+ else throwIO e
393− handleStatus404 e = throwM e
602+ handleStatus404 e = throwIO e
394−
395 headBucketEx = do
603 headBucketEx = do
604+ resp <-
605+ executeRequest $
606+ defaultS3ReqInfo
396− resp <- executeRequest $ def { riMethod = HT.methodHead
607+ { riMethod = HT.methodHead,
397− , riBucket = Just bucket
608+ riBucket = Just bucket
398− }
609+ }
399 return $ NC.responseStatus resp == HT.ok200
610 return $ NC.responseStatus resp == HT.ok200
400
611
401-- | Set the notification configuration on a bucket.
612-- | Set the notification configuration on a bucket.
402putBucketNotification :: Bucket -> Notification -> Minio ()
613putBucketNotification :: Bucket -> Notification -> Minio ()
403−putBucketNotification bucket ncfg =
614+putBucketNotification bucket ncfg = do
615+ ns <- asks getSvcNamespace
616+ void $
617+ executeRequest $
618+ defaultS3ReqInfo
404− void $ executeRequest $ def { riMethod = HT.methodPut
619+ { riMethod = HT.methodPut,
405− , riBucket = Just bucket
620+ riBucket = Just bucket,
406− , riQueryParams = [("notification", Nothing)]
621+ riQueryParams = [("notification", Nothing)],
407− , riPayload = PayloadBS $
622+ riPayload =
623+ PayloadBS $
408− mkPutNotificationRequest ncfg
624+ mkPutNotificationRequest ns ncfg
409− }
625+ }
410
626
411-- | Retrieve the notification configuration on a bucket.
627-- | Retrieve the notification configuration on a bucket.
412getBucketNotification :: Bucket -> Minio Notification
628getBucketNotification :: Bucket -> Minio Notification
413getBucketNotification bucket = do
629getBucketNotification bucket = do
630+ resp <-
631+ executeRequest $
632+ defaultS3ReqInfo
414− resp <- executeRequest $ def { riMethod = HT.methodGet
633+ { riMethod = HT.methodGet,
415− , riBucket = Just bucket
634+ riBucket = Just bucket,
416− , riQueryParams = [("notification", Nothing)]
635+ riQueryParams = [("notification", Nothing)]
417− }
636+ }
418 parseNotification $ NC.responseBody resp
637 parseNotification $ NC.responseBody resp
419
638
420-- | Remove all notifications configured on a bucket.
639-- | Remove all notifications configured on a bucket.
421removeAllBucketNotification :: Bucket -> Minio ()
640removeAllBucketNotification :: Bucket -> Minio ()
422−removeAllBucketNotification = flip putBucketNotification def
641+removeAllBucketNotification = flip putBucketNotification defaultNotification
642+
643+-- | Fetch the policy if any on a bucket.
644+getBucketPolicy :: Bucket -> Minio Text
645+getBucketPolicy bucket = do
646+ resp <-
647+ executeRequest $
648+ defaultS3ReqInfo
649+ { riMethod = HT.methodGet,
650+ riBucket = Just bucket,
651+ riQueryParams = [("policy", Nothing)]
652+ }
653+ return $ decodeUtf8Lenient $ toStrictBS $ NC.responseBody resp
654+
655+-- | Set a new policy on a bucket.
656+-- As a special condition if the policy is empty
657+-- then we treat it as policy DELETE operation.
658+setBucketPolicy :: Bucket -> Text -> Minio ()
659+setBucketPolicy bucket policy = do
660+ if T.null policy
661+ then deleteBucketPolicy bucket
662+ else putBucketPolicy bucket policy
663+
664+-- | Save a new policy on a bucket.
665+putBucketPolicy :: Bucket -> Text -> Minio ()
666+putBucketPolicy bucket policy = do
667+ void $
668+ executeRequest $
669+ defaultS3ReqInfo
670+ { riMethod = HT.methodPut,
671+ riBucket = Just bucket,
672+ riQueryParams = [("policy", Nothing)],
673+ riPayload = PayloadBS $ encodeUtf8 policy
674+ }
675+
676+-- | Delete any policy set on a bucket.
677+deleteBucketPolicy :: Bucket -> Minio ()
678+deleteBucketPolicy bucket = do
679+ void $
680+ executeRequest $
681+ defaultS3ReqInfo
682+ { riMethod = HT.methodDelete,
683+ riBucket = Just bucket,
684+ riQueryParams = [("policy", Nothing)]
685+ }
src/Network/Minio/SelectAPI.hsnew file+294
@@ -0,0 +1,294 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.SelectAPI
18+ ( -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
19+ -- format objects in AWS S3 and in MinIO using SQL Select
20+ -- statements. This allows significant reduction of data transfer
21+ -- from object storage for computation-intensive tasks, as relevant
22+ -- data is filtered close to the storage.
23+ selectObjectContent,
24+ SelectRequest,
25+ selectRequest,
26+
27+ -- *** Input Serialization
28+ InputSerialization,
29+ defaultCsvInput,
30+ linesJsonInput,
31+ documentJsonInput,
32+ defaultParquetInput,
33+ setInputCSVProps,
34+ CompressionType (..),
35+ setInputCompressionType,
36+
37+ -- *** CSV Format details
38+
39+ -- | CSV format options such as delimiters and quote characters are
40+ -- specified using using the functions below. Options are combined
41+ -- monoidally.
42+ CSVProp,
43+ recordDelimiter,
44+ fieldDelimiter,
45+ quoteCharacter,
46+ quoteEscapeCharacter,
47+ commentCharacter,
48+ allowQuotedRecordDelimiter,
49+ FileHeaderInfo (..),
50+ fileHeaderInfo,
51+ QuoteFields (..),
52+ quoteFields,
53+
54+ -- *** Output Serialization
55+ OutputSerialization,
56+ defaultCsvOutput,
57+ defaultJsonOutput,
58+ outputCSVFromProps,
59+ outputJSONFromRecordDelimiter,
60+
61+ -- *** Progress messages
62+ setRequestProgressEnabled,
63+
64+ -- *** Interpreting Select output
65+
66+ -- | The conduit returned by `selectObjectContent` returns values of
67+ -- the `EventMessage` data type. This returns the query output
68+ -- messages formatted according to the chosen output serialization,
69+ -- interleaved with progress messages (if enabled by
70+ -- `setRequestProgressEnabled`), and at the end a statistics
71+ -- message.
72+ --
73+ -- If the application is interested in only the payload, then
74+ -- `getPayloadBytes` can be used. For example to simply print the
75+ -- payload to stdout:
76+ --
77+ -- > resultConduit <- selectObjectContent bucket object mySelectRequest
78+ -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
79+ --
80+ -- Note that runConduit, the connect operator (.|) and stdoutC are
81+ -- all from the "conduit" package.
82+ getPayloadBytes,
83+ EventMessage (..),
84+ Progress (..),
85+ Stats,
86+ )
87+where
88+
89+import Conduit ((.|))
90+import qualified Conduit as C
91+import qualified Data.Binary as Bin
92+import qualified Data.ByteString as B
93+import qualified Data.ByteString.Lazy as LB
94+import Data.Digest.CRC32 (crc32, crc32Update)
95+import Lib.Prelude
96+import qualified Network.HTTP.Conduit as NC
97+import qualified Network.HTTP.Types as HT
98+import Network.Minio.API
99+import Network.Minio.Data
100+import Network.Minio.Errors
101+import Network.Minio.Utils
102+import Network.Minio.XmlGenerator
103+import Network.Minio.XmlParser
104+import UnliftIO (MonadUnliftIO)
105+
106+data EventStreamException
107+ = ESEPreludeCRCFailed
108+ | ESEMessageCRCFailed
109+ | ESEUnexpectedEndOfStream
110+ | ESEDecodeFail [Char]
111+ | ESEInvalidHeaderType
112+ | ESEInvalidHeaderValueType
113+ | ESEInvalidMessageType
114+ deriving stock (Eq, Show)
115+
116+instance Exception EventStreamException
117+
118+-- chunkSize in bytes is 32KiB
119+chunkSize :: Int
120+chunkSize = 32 * 1024
121+
122+parseBinary :: (Bin.Binary a) => ByteString -> IO a
123+parseBinary b = do
124+ case Bin.decodeOrFail $ LB.fromStrict b of
125+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
126+ Right (_, _, r) -> return r
127+
128+bytesToHeaderName :: Text -> IO MsgHeaderName
129+bytesToHeaderName t = case t of
130+ ":message-type" -> return MessageType
131+ ":event-type" -> return EventType
132+ ":content-type" -> return ContentType
133+ ":error-code" -> return ErrorCode
134+ ":error-message" -> return ErrorMessage
135+ _ -> throwIO ESEInvalidHeaderType
136+
137+parseHeaders ::
138+ (MonadUnliftIO m) =>
139+ Word32 ->
140+ C.ConduitM ByteString a m [MessageHeader]
141+parseHeaders 0 = return []
142+parseHeaders hdrLen = do
143+ bs1 <- readNBytes 1
144+ n :: Word8 <- liftIO $ parseBinary bs1
145+
146+ headerKeyBytes <- readNBytes $ fromIntegral n
147+ let headerKey = decodeUtf8Lenient headerKeyBytes
148+ headerName <- liftIO $ bytesToHeaderName headerKey
149+
150+ bs2 <- readNBytes 1
151+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
152+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
153+
154+ bs3 <- readNBytes 2
155+ vLen :: Word16 <- liftIO $ parseBinary bs3
156+ headerValueBytes <- readNBytes $ fromIntegral vLen
157+ let headerValue = decodeUtf8Lenient headerValueBytes
158+ m = (headerName, headerValue)
159+ k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
160+
161+ ms <- parseHeaders (hdrLen - k)
162+ return (m : ms)
163+
164+-- readNBytes returns N bytes read from the string and throws an
165+-- exception if N bytes are not present on the stream.
166+readNBytes :: (MonadUnliftIO m) => Int -> C.ConduitM ByteString a m ByteString
167+readNBytes n = do
168+ b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
169+ if B.length b /= n
170+ then throwIO ESEUnexpectedEndOfStream
171+ else return b
172+
173+crcCheck ::
174+ (MonadUnliftIO m) =>
175+ C.ConduitM ByteString ByteString m ()
176+crcCheck = do
177+ b <- readNBytes 12
178+ n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
179+ preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
180+ when (crc32 (B.take 8 b) /= preludeCRC) $
181+ throwIO ESEPreludeCRCFailed
182+
183+ -- we do not yield the checksum
184+ C.yield $ B.take 8 b
185+
186+ -- 12 bytes have been read off the current message. Now read the
187+ -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
188+ let startCrc = crc32 b
189+ finalCrc <- accumulateYield (fromIntegral n - 16) startCrc
190+
191+ bs <- readNBytes 4
192+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
193+
194+ when (finalCrc /= expectedCrc) $
195+ throwIO ESEMessageCRCFailed
196+
197+ -- we unconditionally recurse - downstream figures out when to
198+ -- quit reading the stream
199+ crcCheck
200+ where
201+ accumulateYield n checkSum = do
202+ let toRead = min n chunkSize
203+ b <- readNBytes toRead
204+ let c' = crc32Update checkSum b
205+ n' = n - B.length b
206+ C.yield b
207+ if n' > 0
208+ then accumulateYield n' c'
209+ else return c'
210+
211+handleMessage :: (MonadUnliftIO m) => C.ConduitT ByteString EventMessage m ()
212+handleMessage = do
213+ b1 <- readNBytes 4
214+ msgLen :: Word32 <- liftIO $ parseBinary b1
215+
216+ b2 <- readNBytes 4
217+ hdrLen :: Word32 <- liftIO $ parseBinary b2
218+
219+ hs <- parseHeaders hdrLen
220+
221+ let payloadLen = msgLen - hdrLen - 16
222+ getHdrVal h = fmap snd . find ((h ==) . fst)
223+ eventHdrValue = getHdrVal EventType hs
224+ msgHdrValue = getHdrVal MessageType hs
225+ errCode = getHdrVal ErrorCode hs
226+ errMsg = getHdrVal ErrorMessage hs
227+
228+ case msgHdrValue of
229+ Just "event" -> do
230+ case eventHdrValue of
231+ Just "Records" -> passThrough $ fromIntegral payloadLen
232+ Just "Cont" -> return ()
233+ Just "Progress" -> do
234+ bs <- readNBytes $ fromIntegral payloadLen
235+ progress <- parseSelectProgress bs
236+ C.yield $ ProgressEventMessage progress
237+ Just "Stats" -> do
238+ bs <- readNBytes $ fromIntegral payloadLen
239+ stats <- parseSelectProgress bs
240+ C.yield $ StatsEventMessage stats
241+ Just "End" -> return ()
242+ _ -> throwIO ESEInvalidMessageType
243+ when (eventHdrValue /= Just "End") handleMessage
244+ Just "error" -> do
245+ let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
246+ maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
247+ _ -> throwIO ESEInvalidMessageType
248+ where
249+ passThrough 0 = return ()
250+ passThrough n = do
251+ let c = min n chunkSize
252+ b <- readNBytes c
253+ C.yield $ RecordPayloadEventMessage b
254+ passThrough $ n - B.length b
255+
256+selectProtoConduit ::
257+ (MonadUnliftIO m) =>
258+ C.ConduitT ByteString EventMessage m ()
259+selectProtoConduit = crcCheck .| handleMessage
260+
261+-- | selectObjectContent calls the SelectRequest on the given
262+-- object. It returns a Conduit of event messages that can be consumed
263+-- by the client.
264+selectObjectContent ::
265+ Bucket ->
266+ Object ->
267+ SelectRequest ->
268+ Minio (C.ConduitT () EventMessage Minio ())
269+selectObjectContent b o r = do
270+ let reqInfo =
271+ defaultS3ReqInfo
272+ { riMethod = HT.methodPost,
273+ riBucket = Just b,
274+ riObject = Just o,
275+ riPayload = PayloadBS $ mkSelectRequest r,
276+ riNeedsLocation = False,
277+ riQueryParams = [("select", Nothing), ("select-type", Just "2")]
278+ }
279+ -- print $ mkSelectRequest r
280+ resp <- mkStreamRequest reqInfo
281+ return $ NC.responseBody resp .| selectProtoConduit
282+
283+-- | A helper conduit that returns only the record payload bytes.
284+getPayloadBytes :: (MonadIO m) => C.ConduitT EventMessage ByteString m ()
285+getPayloadBytes = do
286+ evM <- C.await
287+ case evM of
288+ Just v -> do
289+ case v of
290+ RecordPayloadEventMessage b -> C.yield b
291+ RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
292+ _ -> return ()
293+ getPayloadBytes
294+ Nothing -> return ()
src/Network/Minio/Sign/V4.hs+446−199
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,219 +13,313 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16+{-# LANGUAGE BangPatterns #-}
16
17
17module Network.Minio.Sign.V4
18module Network.Minio.Sign.V4
18− (
19+ ( SignParams (..),
20+ signV4QueryParams,
19− signV4
21+ signV4,
20− , signV4AtTime
21− , signV4PostPolicy
22+ signV4PostPolicy,
22− , mkScope
23+ signV4Stream,
23− , getHeadersToSign
24+ Service (..),
24− , mkCanonicalRequest
25+ credentialScope,
25− , mkStringToSign
26+ )
26− , mkSigningKey
27− , computeSignature
28− , SignV4Data(..)
29− , debugPrintSignV4Data
30− ) where
27+where
31
28
29+import qualified Conduit as C
30+import qualified Data.ByteArray as BA
32import qualified Data.ByteString as B
31import qualified Data.ByteString as B
32+import qualified Data.ByteString.Base64 as Base64
33import qualified Data.ByteString.Char8 as B8
33import qualified Data.ByteString.Char8 as B8
34+import qualified Data.ByteString.Lazy as LB
34−import Data.CaseInsensitive (mk)
35+import Data.CaseInsensitive (mk)
35import qualified Data.CaseInsensitive as CI
36import qualified Data.CaseInsensitive as CI
36−import qualified Data.Set as Set
37+import qualified Data.HashMap.Strict as Map
38+import qualified Data.HashSet as Set
39+import Data.List (partition)
40+import qualified Data.List.NonEmpty as NE
37import qualified Data.Time as Time
41import qualified Data.Time as Time
38−import qualified Data.ByteString.Base64 as Base64
39−import qualified Data.Map.Strict as Map
40−import Network.HTTP.Types (Header)
41−import qualified Network.HTTP.Types.Header as H
42−
43−import Lib.Prelude
42+import Lib.Prelude
44−import Network.Minio.Data
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)
45−import Network.Minio.Data.ByteString
47+import Network.Minio.Data.ByteString
46−import Network.Minio.Data.Crypto
48+import Network.Minio.Data.Crypto
47−import Network.Minio.Data.Time
49+import Network.Minio.Data.Time
50+import Network.Minio.Errors
51+import Text.Printf (printf)
48
52
49-- these headers are not included in the string to sign when signing a
53-- these headers are not included in the string to sign when signing a
50-- request
54-- request
51−ignoredHeaders :: Set ByteString
55+ignoredHeaders :: Set.HashSet ByteString
52−ignoredHeaders = Set.fromList $ map CI.foldedCase
56+ignoredHeaders =
57+ Set.fromList $
58+ map
59+ CI.foldedCase
53− [ H.hAuthorization
60+ [ H.hAuthorization,
54− , H.hContentType
61+ H.hContentType,
55− , H.hContentLength
56− , H.hUserAgent
62+ H.hUserAgent
57− ]
63+ ]
58−
64+
59−data SignV4Data = SignV4Data {
65+data Service = ServiceS3 | ServiceSTS
60− sv4SignTime :: UTCTime
66+ deriving stock (Eq, Show)
61− , sv4Scope :: ByteString
67+
62− , sv4CanonicalRequest :: ByteString
68+toByteString :: Service -> ByteString
63− , sv4HeadersToSign :: [(ByteString, ByteString)]
69+toByteString ServiceS3 = "s3"
64− , sv4Output :: [(ByteString, ByteString)]
70+toByteString ServiceSTS = "sts"
65− , sv4StringToSign :: ByteString
71+
66− , sv4SigningKey :: ByteString
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+ }
67− } deriving (Show)
82+ deriving stock (Show)
68−
83+
69−debugPrintSignV4Data :: SignV4Data -> IO ()
84+mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
70−debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
85+mkAuthHeader accessKey scope signedHeaderKeys sign =
71− B8.putStrLn "SignV4Data:"
86+ let authValue =
72− B8.putStr "Timestamp: " >> print t
87+ B.concat
73− B8.putStr "Scope: " >> B8.putStrLn s
88+ [ "AWS4-HMAC-SHA256 Credential=",
74− B8.putStrLn "Canonical Request:"
89+ encodeUtf8 accessKey,
75− B8.putStrLn cr
90+ "/",
76− B8.putStr "Headers to Sign: " >> print h2s
91+ scope,
77− B8.putStr "Output: " >> print o
92+ ", SignedHeaders=",
78− B8.putStr "StringToSign: " >> B8.putStrLn sts
93+ signedHeaderKeys,
79− B8.putStr "SigningKey: " >> printBytes sk
94+ ", Signature=",
80− B8.putStrLn "END of SignV4Data ========="
95+ sign
81− where
96+ ]
82− printBytes b = do
97+ in (H.hAuthorization, authValue)
83− mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b
98+
84− B8.putStrLn ""
99+data IsStreaming = IsStreamingLength Int64 | NotStreaming
85−
100+ deriving stock (Eq, Show)
101+
102+amzSecurityToken :: ByteString
103+amzSecurityToken = "X-Amz-Security-Token"
104+
86−-- | Given MinioClient and request details, including request method,
105+-- | Given SignParams and request details, including request method,
87-- request path, headers, query params and payload hash, generates an
106-- request path, headers, query params and payload hash, generates an
88-- updated set of headers, including the x-amz-date header and the
107-- updated set of headers, including the x-amz-date header and the
89-- Authorization header, which includes the signature.
108-- Authorization header, which includes the signature.
90−signV4 :: ConnectInfo -> RequestInfo -> Maybe Int
109+--
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
110+-- For normal requests (i.e. without an expiry time), the output is
100−-- time), the output is the list of headers to add to authenticate the
111+-- the list of headers to add to authenticate the request.
101−-- request.
102--
112--
103-- If `expiry` is not Nothing, it is assumed that a presigned request
113-- If `expiry` is not Nothing, it is assumed that a presigned request
104-- is being created. The expiry is interpreted as an integer number of
114-- is being created. The expiry is interpreted as an integer number of
105-- seconds. The output will be the list of query-parameters to add to
115-- seconds. The output will be the list of query-parameters to add to
106-- the request.
116-- the request.
107−signV4AtTime :: UTCTime -> ConnectInfo -> RequestInfo -> Maybe Int
117+signV4QueryParams :: SignParams -> NC.Request -> SimpleQuery
108− -> SignV4Data
118+signV4QueryParams !sp !req =
109−signV4AtTime ts ci ri expiry =
119+ let scope = credentialScope sp
110− let
120+ expiry = spExpirySecs sp
111− region = maybe (connectRegion ci) identity $ riRegion ri
121+
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
122+ headersToSign = getHeadersToSign $ NC.requestHeaders req
123− signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
123+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
124−
125− -- query-parameters to be added before signing for presigned URLs
124+ -- query-parameters to be added before signing for presigned URLs
126− -- (i.e. when `isJust expiry`)
125+ -- (i.e. when `isJust expiry`)
126+ authQP =
127− authQP = [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
127+ [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
128− , ("X-Amz-Credential", B.concat [accessKey, "/", scope])
128+ ("X-Amz-Credential", B.concat [encodeUtf8 $ spAccessKey sp, "/", scope]),
129− , datePair
129+ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
130− , ("X-Amz-Expires", maybe "" show expiry)
130+ ("X-Amz-Expires", maybe "" showBS expiry),
131− , ("X-Amz-SignedHeaders", signedHeaderKeys)
131+ ("X-Amz-SignedHeaders", signedHeaderKeys)
132− ]
132+ ]
133+ ++ maybeToList ((amzSecurityToken,) . BA.convert <$> spSessionToken sp)
133− finalQP = riQueryParams ri ++
134+ finalQP =
135+ parseQuery (NC.queryString req)
134− if isJust expiry
136+ ++ if isJust expiry
135− then (fmap . fmap) Just authQP
137+ then (fmap . fmap) Just authQP
136− else []
138+ else []
137−
138− -- 1. compute canonical request
139+ -- 1. compute canonical request
140+ canonicalRequest =
139− canonicalRequest = mkCanonicalRequest (ri {riQueryParams = finalQP})
141+ mkCanonicalRequest
142+ False
143+ sp
144+ (NC.setQueryString finalQP req)
140− headersToSign
145+ headersToSign
141−
146+
142− -- 2. compute string to sign
147+ -- 2. compute string to sign
143− stringToSign = mkStringToSign ts scope canonicalRequest
148+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
144−
145− -- 3.1 compute signing key
149+ -- 3.1 compute signing key
146− signingKey = mkSigningKey ts region secretKey
150+ signingKey = getSigningKey sp
147−
148− -- 3.2 compute signature
151+ -- 3.2 compute signature
149− signature = computeSignature stringToSign signingKey
152+ 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="
153+ in ("X-Amz-Signature", signature) : authQP
160− , signature
161− ]
162− authHeader = (H.hAuthorization, authValue)
163
154
164− -- finally compute output pairs
155+-- | Given SignParams and request details, including request method, request
165− output = if isJust expiry
156+-- path, headers, query params and payload hash, generates an updated set of
166− then ("X-Amz-Signature", signature) : authQP
157+-- headers, including the x-amz-date header and the Authorization header, which
167− else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
158+-- includes the signature.
168− datePair]
159+--
160+-- The output is the list of headers to be added to authenticate the request.
161+signV4 :: SignParams -> NC.Request -> [Header]
162+signV4 !sp !req =
163+ let scope = credentialScope sp
164+
165+ -- extra headers to be added for signing purposes.
166+ extraHeaders =
167+ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp)
168+ : ( -- payload hash is only used for S3 (not STS)
169+ [ ( "x-amz-content-sha256",
170+ fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
171+ )
172+ | spService sp == ServiceS3
173+ ]
174+ )
175+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
169
176
170− in
177+ -- 1. compute canonical request
171− SignV4Data ts scope canonicalRequest headersToSign output
178+ reqHeaders = NC.requestHeaders req ++ extraHeaders
172− stringToSign signingKey
179+ (canonicalRequest, signedHeaderKeys) =
180+ getCanonicalRequestAndSignedHeaders
181+ NotStreaming
182+ sp
183+ req
184+ reqHeaders
173
185
186+ -- 2. compute string to sign
187+ stringToSign = mkStringToSign (spTimeStamp sp) scope canonicalRequest
188+ -- 3.1 compute signing key
189+ signingKey = getSigningKey sp
190+ -- 3.2 compute signature
191+ signature = computeSignature stringToSign signingKey
192+ -- 4. compute auth header
193+ authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
194+ in authHeader : extraHeaders
174
195
175−mkScope :: UTCTime -> Region -> ByteString
196+credentialScope :: SignParams -> ByteString
197+credentialScope sp =
198+ let region = fromMaybe "" $ spRegion sp
176−mkScope ts region = B.intercalate "/"
199+ in B.intercalate
200+ "/"
177− [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts
201+ [ encodeUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" $ spTimeStamp sp,
178− , toS region
202+ encodeUtf8 region,
179− , "s3"
203+ toByteString $ spService sp,
180− , "aws4_request"
204+ "aws4_request"
181− ]
205+ ]
182
206
207+-- Folds header name, trims whitespace in header values, skips ignored headers
208+-- and sorts headers.
183getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
209getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
184getHeadersToSign !h =
210getHeadersToSign !h =
185− filter (flip Set.notMember ignoredHeaders . fst) $
211+ filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
186− map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
212+ map (bimap CI.foldedCase stripBS) h
187−
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 ->
188−mkCanonicalRequest :: RequestInfo -> [(ByteString, ByteString)]
274+ [(ByteString, ByteString)] ->
189− -> ByteString
275+ ByteString
190−mkCanonicalRequest !ri !headersForSign =
276+mkCanonicalRequest !isStreaming !sp !req !headersForSign =
191− let
277+ let httpMethod = NC.method req
278+ canonicalUri = uriEncode False $ NC.path req
192− canonicalQueryString = B.intercalate "&" $
279+ canonicalQueryString =
280+ B.intercalate "&" $
193− map (\(x, y) -> B.concat [x, "=", y]) $
281+ map (\(x, y) -> B.concat [x, "=", y]) $
194− sort $ map (\(x, y) ->
282+ sortBy (\a b -> compare (fst a) (fst b)) $
283+ map
195− (uriEncode True x, maybe "" (uriEncode True) y)) $
284+ ( bimap (uriEncode True) (maybe "" (uriEncode True))
196− riQueryParams ri
285+ )
197−
286+ (parseQuery $ NC.queryString req)
198− sortedHeaders = sort headersForSign
287+ sortedHeaders = sort headersForSign
199−
200− canonicalHeaders = B.concat $
288+ canonicalHeaders =
289+ B.concat $
201− map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
290+ map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
202−
203− signedHeaders = B.intercalate ";" $ map fst sortedHeaders
291+ signedHeaders = B.intercalate ";" $ map fst sortedHeaders
204−
292+ payloadHashStr =
205− in
293+ if isStreaming
294+ then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
295+ else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
206− B.intercalate "\n"
296+ in B.intercalate
207− [ riMethod ri
297+ "\n"
208− , uriEncode False $ getPathFromRI ri
298+ [ httpMethod,
209− , canonicalQueryString
299+ canonicalUri,
210− , canonicalHeaders
300+ canonicalQueryString,
211− , signedHeaders
301+ canonicalHeaders,
212− , maybe "UNSIGNED-PAYLOAD" identity $ riPayloadHash ri
302+ signedHeaders,
303+ payloadHashStr
213− ]
304+ ]
214
305
215mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
306mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
216−mkStringToSign ts !scope !canonicalRequest = B.intercalate "\n"
307+mkStringToSign ts !scope !canonicalRequest =
308+ B.intercalate
309+ "\n"
217− [ "AWS4-HMAC-SHA256"
310+ [ "AWS4-HMAC-SHA256",
218− , awsTimeFormatBS ts
311+ awsTimeFormatBS ts,
219− , scope
312+ scope,
220− , hashSHA256 canonicalRequest
313+ hashSHA256 canonicalRequest
221− ]
314+ ]
222−
315+
223−mkSigningKey :: UTCTime -> Region -> ByteString -> ByteString
316+getSigningKey :: SignParams -> ByteString
317+getSigningKey sp =
224−mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"
318+ hmacSHA256RawBS "aws4_request"
225− . hmacSHA256RawBS "s3"
319+ . hmacSHA256RawBS (toByteString $ spService sp)
226− . hmacSHA256RawBS (toS region)
320+ . hmacSHA256RawBS (encodeUtf8 $ fromMaybe "" $ spRegion sp)
227− . hmacSHA256RawBS (awsDateFormatBS ts)
321+ . hmacSHA256RawBS (awsDateFormatBS $ spTimeStamp sp)
228− $ B.concat ["AWS4", secretKey]
322+ $ B.concat ["AWS4", BA.convert $ spSecretKey sp]
229
323
230computeSignature :: ByteString -> ByteString -> ByteString
324computeSignature :: ByteString -> ByteString -> ByteString
231computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
325computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -233,15 +327,168 @@ computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
233-- | Takes a validated Post Policy JSON bytestring, the signing time,
327-- | Takes a validated Post Policy JSON bytestring, the signing time,
234-- and ConnInfo and returns form-data for the POST upload containing
328-- and ConnInfo and returns form-data for the POST upload containing
235-- just the signature and the encoded post-policy.
329-- just the signature and the encoded post-policy.
236−signV4PostPolicy :: ByteString -> UTCTime -> ConnectInfo
330+signV4PostPolicy ::
331+ ByteString ->
332+ SignParams ->
237− -> Map.Map Text ByteString
333+ Map.HashMap Text ByteString
238−signV4PostPolicy !postPolicyJSON !signTime !ci =
334+signV4PostPolicy !postPolicyJSON !sp =
239− let
240− stringToSign = Base64.encode postPolicyJSON
335+ let stringToSign = Base64.encode postPolicyJSON
241− region = connectRegion ci
336+ signingKey = getSigningKey sp
242− signingKey = mkSigningKey signTime region $ toS $ connectSecretKey ci
243− signature = computeSignature stringToSign signingKey
337+ signature = computeSignature stringToSign signingKey
244− in
338+ in Map.fromList $
245− Map.fromList [ ("x-amz-signature", signature)
339+ [ ("x-amz-signature", signature),
246− , ("policy", stringToSign)
340+ ("policy", stringToSign)
247− ]
341+ ]
342+ ++ maybeToList ((decodeUtf8 amzSecurityToken,) . BA.convert <$> spSessionToken sp)
343+
344+chunkSizeConstant :: Int
345+chunkSizeConstant = 64 * 1024
346+
347+-- base16Len computes the number of bytes required to represent @n (> 0)@ in
348+-- hexadecimal.
349+base16Len :: (Integral a) => a -> Int
350+base16Len n
351+ | n == 0 = 0
352+ | otherwise = 1 + base16Len (n `div` 16)
353+
354+signedStreamLength :: Int64 -> Int64
355+signedStreamLength dataLen =
356+ let chunkSzInt = fromIntegral chunkSizeConstant
357+ (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
358+ -- Structure of a chunk:
359+ -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
360+ encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
361+ fullChunkSize = encodedChunkLen chunkSzInt
362+ lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
363+ finalChunkSize = 1 + 17 + 64 + 2 + 2
364+ in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
365+
366+-- For streaming S3, we need to update the content-encoding header.
367+addContentEncoding :: [Header] -> [Header]
368+addContentEncoding hs =
369+ -- assume there is at most one content-encoding header.
370+ let (ceHdrs, others) = partition ((== hContentEncoding) . fst) hs
371+ in maybe
372+ (hContentEncoding, "aws-chunked")
373+ (\(k, v) -> (k, v <> ",aws-chunked"))
374+ (listToMaybe ceHdrs)
375+ : others
376+
377+signV4Stream ::
378+ Int64 ->
379+ SignParams ->
380+ NC.Request ->
381+ (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
382+signV4Stream !payloadLength !sp !req =
383+ let ts = spTimeStamp sp
384+
385+ -- compute the updated list of headers to be added for signing purposes.
386+ signedContentLength = signedStreamLength payloadLength
387+ extraHeaders =
388+ [ ("X-Amz-Date", awsTimeFormatBS $ spTimeStamp sp),
389+ ("x-amz-decoded-content-length", showBS payloadLength),
390+ ("content-length", showBS signedContentLength),
391+ ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
392+ ]
393+ ++ maybeToList ((mk amzSecurityToken,) . BA.convert <$> spSessionToken sp)
394+ requestHeaders =
395+ addContentEncoding $
396+ foldr setHeader (NC.requestHeaders req) extraHeaders
397+
398+ -- 1. Compute Seed Signature
399+ -- 1.1 Canonical Request
400+ (canonicalReq, signedHeaderKeys) =
401+ getCanonicalRequestAndSignedHeaders
402+ (IsStreamingLength payloadLength)
403+ sp
404+ req
405+ requestHeaders
406+
407+ scope = credentialScope sp
408+ accessKey = spAccessKey sp
409+ -- 1.2 String toSign
410+ stringToSign = mkStringToSign ts scope canonicalReq
411+ -- 1.3 Compute signature
412+ -- 1.3.1 compute signing key
413+ signingKey = getSigningKey sp
414+ -- 1.3.2 Compute signature
415+ seedSignature = computeSignature stringToSign signingKey
416+ -- 1.3.3 Compute Auth Header
417+ authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
418+ -- 1.4 Updated headers for the request
419+ finalReqHeaders = authHeader : requestHeaders
420+ -- headersToAdd = authHeader : datePair : streamingHeaders
421+
422+ toHexStr n = B8.pack $ printf "%x" n
423+ (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
424+ -- Function to compute string to sign for each chunk.
425+ chunkStrToSign prevSign currChunkHash =
426+ B.intercalate
427+ "\n"
428+ [ "AWS4-HMAC-SHA256-PAYLOAD",
429+ awsTimeFormatBS ts,
430+ scope,
431+ prevSign,
432+ hashSHA256 "",
433+ currChunkHash
434+ ]
435+ -- Read n byte from upstream and return a strict bytestring.
436+ mustTakeN n = do
437+ bs <- LB.toStrict <$> (C.takeCE n C..| C.sinkLazy)
438+ when (B.length bs /= n) $
439+ throwIO MErrVStreamingBodyUnexpectedEOF
440+ return bs
441+ signerConduit n lps prevSign =
442+ -- First case encodes a full chunk of length
443+ -- 'chunkSizeConstant'.
444+ if
445+ | n > 0 -> do
446+ bs <- mustTakeN chunkSizeConstant
447+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
448+ nextSign = computeSignature strToSign signingKey
449+ chunkBS =
450+ toHexStr chunkSizeConstant
451+ <> ";chunk-signature="
452+ <> nextSign
453+ <> "\r\n"
454+ <> bs
455+ <> "\r\n"
456+ C.yield chunkBS
457+ signerConduit (n - 1) lps nextSign
458+
459+ -- Second case encodes the last chunk which is smaller than
460+ -- 'chunkSizeConstant'
461+ | lps > 0 -> do
462+ bs <- mustTakeN $ fromIntegral lps
463+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
464+ nextSign = computeSignature strToSign signingKey
465+ chunkBS =
466+ toHexStr lps
467+ <> ";chunk-signature="
468+ <> nextSign
469+ <> "\r\n"
470+ <> bs
471+ <> "\r\n"
472+ C.yield chunkBS
473+ signerConduit 0 0 nextSign
474+
475+ -- Last case encodes the final signature chunk that has no
476+ -- data.
477+ | otherwise -> do
478+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")
479+ nextSign = computeSignature strToSign signingKey
480+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
481+ C.yield lastChunkBS
482+ in \src ->
483+ req
484+ { NC.requestHeaders = finalReqHeaders,
485+ NC.requestBody =
486+ NC.requestBodySource signedContentLength $
487+ src C..| signerConduit numParts lastPSize seedSignature
488+ }
489+
490+-- "setHeader r hdr" adds the hdr to r, replacing it in r if it already exists.
491+setHeader :: Header -> RequestHeaders -> RequestHeaders
492+setHeader hdr r =
493+ let r' = filter (\(name, _) -> name /= fst hdr) r
494+ in hdr : r'
src/Network/Minio/Utils.hs+167−87
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -16,88 +16,146 @@
16
16
17module Network.Minio.Utils where
17module Network.Minio.Utils where
18
18
19−import qualified Control.Concurrent.Async.Lifted as A
19+import qualified Conduit as C
20−import qualified Control.Concurrent.QSem.Lifted as Q
20+import Control.Monad.IO.Unlift (MonadUnliftIO)
21−import qualified Control.Exception.Lifted as ExL
22−import qualified Control.Monad.Catch as MC
23import qualified Control.Monad.Trans.Resource as R
21import qualified Control.Monad.Trans.Resource as R
24−
25import qualified Data.ByteString as B
22import qualified Data.ByteString as B
26import qualified Data.ByteString.Lazy as LB
23import qualified Data.ByteString.Lazy as LB
27−import qualified Data.Conduit as C
24+import Data.CaseInsensitive (mk, original)
28import qualified Data.Conduit.Binary as CB
25import qualified Data.Conduit.Binary as CB
26+import qualified Data.HashMap.Strict as H
29import qualified Data.Text as T
27import qualified Data.Text as T
30−import Data.Text.Encoding.Error (lenientDecode)
31−import Data.Text.Read (decimal)
28+import Data.Text.Read (decimal)
32−import Data.Time
29+import Data.Time
30+ ( defaultTimeLocale,
31+ parseTimeM,
32+ rfc822DateFormat,
33+ )
34+import Lib.Prelude
33−import Network.HTTP.Conduit (Response)
35+import Network.HTTP.Conduit (Response)
34import qualified Network.HTTP.Conduit as NC
36import qualified Network.HTTP.Conduit as NC
35import qualified Network.HTTP.Types as HT
37import qualified Network.HTTP.Types as HT
36import qualified Network.HTTP.Types.Header as Hdr
38import 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)
37import qualified System.IO as IO
42import qualified System.IO as IO
38−import Data.CaseInsensitive (mk)
43+import qualified UnliftIO as U
39−
44+import qualified UnliftIO.Async as A
40−
41−import Lib.Prelude
42
45
43−import Network.Minio.XmlParser (parseErrResponse)
44−
45−allocateReadFile :: (R.MonadResource m, R.MonadResourceBase m, MonadCatch m)
46+allocateReadFile ::
47+ (MonadUnliftIO m, R.MonadResource m) =>
48+ FilePath ->
46− => FilePath -> m (R.ReleaseKey, Handle)
49+ m (R.ReleaseKey, Handle)
47allocateReadFile fp = do
50allocateReadFile fp = do
48 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
51 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
49− either (\(e :: IOException) -> throwM e) (return . (rk,)) hdlE
52+ either (\(e :: U.IOException) -> throwIO e) (return . (rk,)) hdlE
50 where
53 where
51− openReadFile f = ExL.try $ IO.openBinaryFile f IO.ReadMode
54+ openReadFile f = U.try $ IO.openBinaryFile f IO.ReadMode
52 cleanup = either (const $ return ()) IO.hClose
55 cleanup = either (const $ return ()) IO.hClose
53
56
54-- | Queries the file size from the handle. Catches any file operation
57-- | Queries the file size from the handle. Catches any file operation
55-- exceptions and returns Nothing instead.
58-- exceptions and returns Nothing instead.
56−getFileSize :: (R.MonadResourceBase m, R.MonadResource m)
59+getFileSize ::
60+ (MonadUnliftIO m) =>
61+ Handle ->
57− => Handle -> m (Maybe Int64)
62+ m (Maybe Int64)
58getFileSize h = do
63getFileSize h = do
59 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
64 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
60 case resE of
65 case resE of
61− Left (_ :: IOException) -> return Nothing
66+ Left (_ :: U.IOException) -> return Nothing
62 Right s -> return $ Just s
67 Right s -> return $ Just s
63
68
64-- | Queries if handle is seekable. Catches any file operation
69-- | Queries if handle is seekable. Catches any file operation
65-- exceptions and return False instead.
70-- exceptions and return False instead.
66−isHandleSeekable :: (R.MonadResource m, R.MonadResourceBase m)
71+isHandleSeekable ::
72+ (R.MonadResource m) =>
67− => Handle -> m Bool
73+ Handle ->
74+ m Bool
68isHandleSeekable h = do
75isHandleSeekable h = do
69 resE <- liftIO $ try $ IO.hIsSeekable h
76 resE <- liftIO $ try $ IO.hIsSeekable h
70 case resE of
77 case resE of
71− Left (_ :: IOException) -> return False
78+ Left (_ :: U.IOException) -> return False
72 Right v -> return v
79 Right v -> return v
73
80
74-- | Helper function that opens a handle to the filepath and performs
81-- | Helper function that opens a handle to the filepath and performs
75-- the given action on it. Exceptions of type MError are caught and
82-- the given action on it. Exceptions of type MError are caught and
76-- returned - both during file handle allocation and when the action
83-- returned - both during file handle allocation and when the action
77-- is run.
84-- is run.
78−withNewHandle :: (R.MonadResourceBase m, R.MonadResource m, MonadCatch m)
85+withNewHandle ::
86+ (MonadUnliftIO m, R.MonadResource m) =>
87+ FilePath ->
88+ (Handle -> m a) ->
79− => FilePath -> (Handle -> m a) -> m (Either IOException a)
89+ m (Either U.IOException a)
80withNewHandle fp fileAction = do
90withNewHandle fp fileAction = do
81 -- opening a handle can throw MError exception.
91 -- opening a handle can throw MError exception.
82− handleE <- MC.try $ allocateReadFile fp
92+ handleE <- try $ allocateReadFile fp
83 either (return . Left) doAction handleE
93 either (return . Left) doAction handleE
84 where
94 where
85 doAction (rkey, h) = do
95 doAction (rkey, h) = do
86 -- fileAction may also throw MError exception, so we catch and
96 -- fileAction may also throw MError exception, so we catch and
87 -- return it.
97 -- return it.
88− resE <- MC.try $ fileAction h
98+ resE <- try $ fileAction h
89 R.release rkey
99 R.release rkey
90 return resE
100 return resE
91
101
92mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
102mkHeaderFromPairs :: [(ByteString, ByteString)] -> [HT.Header]
93−mkHeaderFromPairs = map ((\(x, y) -> (mk x, y)))
103+mkHeaderFromPairs = map (first mk)
94
104
95lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
105lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
96−lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
106+lookupHeader hdr = listToMaybe . map snd . filter (\(h, _) -> h == hdr)
97
107
98getETagHeader :: [HT.Header] -> Maybe Text
108getETagHeader :: [HT.Header] -> Maybe Text
99getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
109getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
100
110
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+
101getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
159getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
102getLastModifiedHeader hs = do
160getLastModifiedHeader hs = do
103 modTimebs <- decodeUtf8Lenient <$> lookupHeader Hdr.hLastModified hs
161 modTimebs <- decodeUtf8Lenient <$> lookupHeader Hdr.hLastModified hs
@@ -106,77 +164,103 @@ getLastModifiedHeader hs = do
106getContentLength :: [HT.Header] -> Maybe Int64
164getContentLength :: [HT.Header] -> Maybe Int64
107getContentLength hs = do
165getContentLength hs = do
108 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
166 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
109− fst <$> hush (decimal nbs)
167+ fst <$> either (const Nothing) Just (decimal nbs)
110−
111
168
112decodeUtf8Lenient :: ByteString -> Text
169decodeUtf8Lenient :: ByteString -> Text
113decodeUtf8Lenient = decodeUtf8With lenientDecode
170decodeUtf8Lenient = decodeUtf8With lenientDecode
114
171
115isSuccessStatus :: HT.Status -> Bool
172isSuccessStatus :: HT.Status -> Bool
173+isSuccessStatus sts =
116−isSuccessStatus sts = let s = HT.statusCode sts
174+ let s = HT.statusCode sts
117− in (s >= 200 && s < 300)
175+ in (s >= 200 && s < 300)
118
176
177+httpLbs ::
119−httpLbs :: (R.MonadThrow m, MonadIO m)
178+ (MonadIO m) =>
120− => NC.Request -> NC.Manager
179+ NC.Request ->
180+ NC.Manager ->
121− -> m (NC.Response LByteString)
181+ m (NC.Response LByteString)
122httpLbs req mgr = do
182httpLbs req mgr = do
123 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
183 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
124− resp <- either throwM return respE
184+ resp <- either throwIO return respE
125 unless (isSuccessStatus $ NC.responseStatus resp) $
185 unless (isSuccessStatus $ NC.responseStatus resp) $
126 case contentTypeMay resp of
186 case contentTypeMay resp of
127 Just "application/xml" -> do
187 Just "application/xml" -> do
128 sErr <- parseErrResponse $ NC.responseBody resp
188 sErr <- parseErrResponse $ NC.responseBody resp
129− throwM sErr
189+ throwIO sErr
130−
190+ Just "application/json" -> do
191+ sErr <- parseErrResponseJSON $ NC.responseBody resp
192+ throwIO sErr
193+ _ ->
194+ throwIO $
131− _ -> throwM $ NC.HttpExceptionRequest req $
195+ NC.HttpExceptionRequest req $
132− NC.StatusCodeException (void resp) (show resp)
196+ NC.StatusCodeException (void resp) (showBS resp)
133
197
134 return resp
198 return resp
135 where
199 where
200+ tryHttpEx ::
136− tryHttpEx :: IO (NC.Response LByteString)
201+ IO (NC.Response LByteString) ->
137− -> IO (Either NC.HttpException (NC.Response LByteString))
202+ IO (Either NC.HttpException (NC.Response LByteString))
138 tryHttpEx = try
203 tryHttpEx = try
204+ contentTypeMay resp =
139− contentTypeMay resp = lookupHeader Hdr.hContentType $
205+ lookupHeader Hdr.hContentType $
140− NC.responseHeaders resp
206+ NC.responseHeaders resp
141
207
208+http ::
142−http :: (R.MonadResourceBase m, R.MonadResource m)
209+ (MonadUnliftIO m, R.MonadResource m) =>
143− => NC.Request -> NC.Manager
210+ NC.Request ->
211+ NC.Manager ->
144− -> m (Response (C.ResumableSource m ByteString))
212+ m (Response (C.ConduitT () ByteString m ()))
145http req mgr = do
213http req mgr = do
146 respE <- tryHttpEx $ NC.http req mgr
214 respE <- tryHttpEx $ NC.http req mgr
147− resp <- either throwM return respE
215+ resp <- either throwIO return respE
148 unless (isSuccessStatus $ NC.responseStatus resp) $
216 unless (isSuccessStatus $ NC.responseStatus resp) $
149 case contentTypeMay resp of
217 case contentTypeMay resp of
150 Just "application/xml" -> do
218 Just "application/xml" -> do
151− respBody <- NC.responseBody resp C.$$+- CB.sinkLbs
219+ respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
152 sErr <- parseErrResponse respBody
220 sErr <- parseErrResponse respBody
153− throwM sErr
221+ throwIO sErr
154−
155 _ -> do
222 _ -> do
156 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
223 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
224+ throwIO $
157− throwM $ NC.HttpExceptionRequest req $
225+ NC.HttpExceptionRequest req $
158− NC.StatusCodeException (void resp) content
226+ NC.StatusCodeException (void resp) content
159−
160
227
161 return resp
228 return resp
162 where
229 where
163− tryHttpEx :: (R.MonadResourceBase m) => m a
230+ tryHttpEx ::
231+ (MonadUnliftIO m) =>
232+ m a ->
164− -> m (Either NC.HttpException a)
233+ m (Either NC.HttpException a)
165− tryHttpEx = ExL.try
234+ tryHttpEx = try
235+ contentTypeMay resp =
166− contentTypeMay resp = lookupHeader Hdr.hContentType $ NC.responseHeaders resp
236+ lookupHeader Hdr.hContentType $
237+ NC.responseHeaders resp
167
238
168-- Similar to mapConcurrently but limits the number of threads that
239-- Similar to mapConcurrently but limits the number of threads that
169-- can run using a quantity semaphore.
240-- can run using a quantity semaphore.
170−limitedMapConcurrently :: (MonadIO m, R.MonadBaseControl IO m)
241+limitedMapConcurrently ::
242+ (MonadUnliftIO m) =>
171− => Int -> (t -> m a) -> [t] -> m [a]
243+ Int ->
244+ (t -> m a) ->
245+ [t] ->
246+ m [a]
247+limitedMapConcurrently 0 _ _ = return []
172limitedMapConcurrently count act args = do
248limitedMapConcurrently count act args = do
173− qSem <- liftIO $ Q.newQSem count
249+ t' <- U.newTVarIO count
174− threads <- mapM (A.async . wThread qSem) args
250+ threads <- mapM (A.async . wThread t') args
175 mapM A.wait threads
251 mapM A.wait threads
176 where
252 where
177− -- grab 1 unit from semaphore, run action and release it
178− wThread qs arg =
253+ wThread t arg =
179− ExL.bracket_ (Q.waitQSem qs) (Q.signalQSem qs) $ act 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)
180
264
181-- helper function to 'drop' empty optional parameter.
265-- helper function to 'drop' empty optional parameter.
182mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
266mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -185,21 +269,17 @@ mkQuery k mv = (k,) <$> mv
185-- helper function to build query parameters that are optional.
269-- helper function to build query parameters that are optional.
186-- don't use it with mandatory query params with empty value.
270-- don't use it with mandatory query params with empty value.
187mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
271mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
188−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
272+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
189
273
190−chunkBSConduit :: (Monad m, Integral a)
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.
191− => [a] -> C.Conduit ByteString m ByteString
278+chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
192−chunkBSConduit s = loop 0 [] s
279+chunkBSConduit [] = return ()
193− where
280+chunkBSConduit (s : ss) = do
194− loop _ _ [] = return ()
281+ bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
195− loop n readChunks (size:sizes) = do
282+ if
196− bsMay <- C.await
283+ | B.length bs == s -> C.yield bs >> chunkBSConduit ss
197− case bsMay of
284+ | B.length bs > 0 -> C.yield bs
198− Nothing -> when (n > 0) $ C.yield $ B.concat readChunks
285+ | otherwise -> return ()
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.hsnew file+65
@@ -0,0 +1,65 @@
1+--
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.XmlCommon where
18+
19+import qualified Data.Text as T
20+import Data.Text.Read (decimal)
21+import Data.Time (UTCTime)
22+import Data.Time.Format.ISO8601 (iso8601ParseM)
23+import Lib.Prelude (throwIO)
24+import Network.Minio.Errors
25+import Text.XML (Name (Name), def, parseLBS)
26+import Text.XML.Cursor (Axis, Cursor, content, element, fromDocument, laxElement, ($/), (&/))
27+
28+s3Name :: Text -> Text -> Name
29+s3Name ns s = Name s (Just ns) Nothing
30+
31+uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e
32+uncurry4 f (a, b, c, d) = f a b c d
33+
34+uncurry6 :: (a -> b -> c -> d -> e -> f -> g) -> (a, b, c, d, e, f) -> g
35+uncurry6 f (a, b, c, d, e, g) = f a b c d e g
36+
37+-- | Parse time strings from XML
38+parseS3XMLTime :: (MonadIO m) => Text -> m UTCTime
39+parseS3XMLTime t =
40+ maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
41+ iso8601ParseM $
42+ toString t
43+
44+parseDecimal :: (MonadIO m, Integral a) => Text -> m a
45+parseDecimal numStr =
46+ either (throwIO . MErrVXmlParse . show) return $
47+ fst <$> decimal numStr
48+
49+parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
50+parseDecimals numStr = forM numStr parseDecimal
51+
52+s3Elem :: Text -> Text -> Axis
53+s3Elem ns = element . s3Name ns
54+
55+parseRoot :: (MonadIO m) => LByteString -> m Cursor
56+parseRoot =
57+ either (throwIO . MErrVXmlParse . show) (return . fromDocument)
58+ . parseLBS def
59+
60+parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
61+parseErrResponse xmldata = do
62+ r <- parseRoot xmldata
63+ let code = T.concat $ r $/ laxElement "Code" &/ content
64+ message = T.concat $ r $/ laxElement "Message" &/ content
65+ return $ toServiceErr code message
src/Network/Minio/XmlGenerator.hs+183−54
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,88 +15,217 @@
15--
15--
16
16
17module Network.Minio.XmlGenerator
17module Network.Minio.XmlGenerator
18− ( mkCreateBucketConfig
18+ ( mkCreateBucketConfig,
19− , mkCompleteMultipartUploadRequest
19+ mkCompleteMultipartUploadRequest,
20− , mkPutNotificationRequest
20+ mkPutNotificationRequest,
21+ mkSelectRequest,
22+ )
21− ) where
23+where
22−
23
24
24import qualified Data.ByteString.Lazy as LBS
25import qualified Data.ByteString.Lazy as LBS
25−import qualified Data.Map as M
26+import qualified Data.Text as T
26−import qualified Data.Text as T
27+import Network.Minio.Data
28+import Network.Minio.XmlCommon
27−import Text.XML
29+import Text.XML
28−
29−import Lib.Prelude
30−
31−import Network.Minio.Data
32−
33
30
34-- | Create a bucketConfig request body XML
31-- | Create a bucketConfig request body XML
35−mkCreateBucketConfig :: Region -> ByteString
32+mkCreateBucketConfig :: Text -> Region -> ByteString
36−mkCreateBucketConfig location = LBS.toStrict $ renderLBS def bucketConfig
33+mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
37 where
34 where
38− s3Element n = Element (s3Name n) M.empty
35+ s3Element n = Element (s3Name ns n) mempty
36+ root =
37+ s3Element
39− root = s3Element "CreateBucketConfiguration"
38+ "CreateBucketConfiguration"
39+ [ NodeElement $
40+ s3Element
40− [ NodeElement $ s3Element "LocationConstraint"
41+ "LocationConstraint"
41− [ NodeContent location]
42+ [NodeContent location]
42 ]
43 ]
43− bucketConfig = Document (Prologue [] Nothing []) root []
44+ bucketConfig = Document (Prologue [] Nothing []) root []
44
45
45-- | Create a completeMultipartUpload request body XML
46-- | Create a completeMultipartUpload request body XML
46mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
47mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
47mkCompleteMultipartUploadRequest partInfo =
48mkCompleteMultipartUploadRequest partInfo =
48 LBS.toStrict $ renderLBS def cmur
49 LBS.toStrict $ renderLBS def cmur
49 where
50 where
51+ root =
50− root = Element "CompleteMultipartUpload" M.empty $
52+ Element "CompleteMultipartUpload" mempty $
51− map (NodeElement . mkPart) partInfo
53+ map (NodeElement . mkPart) partInfo
52− mkPart (n, etag) = Element "Part" M.empty
54+ mkPart (n, etag) =
55+ Element
56+ "Part"
57+ mempty
53− [ NodeElement $ Element "PartNumber" M.empty
58+ [ NodeElement $
59+ Element
60+ "PartNumber"
61+ mempty
54− [NodeContent $ T.pack $ show n]
62+ [NodeContent $ T.pack $ show n],
55− , NodeElement $ Element "ETag" M.empty
63+ NodeElement $
64+ Element
65+ "ETag"
66+ mempty
56− [NodeContent etag]
67+ [NodeContent etag]
57− ]
68+ ]
58 cmur = Document (Prologue [] Nothing []) root []
69 cmur = Document (Prologue [] Nothing []) root []
59
70
60-- Simplified XML representation without element attributes.
71-- Simplified XML representation without element attributes.
72+data XNode
61−data XNode = XNode Text [XNode]
73+ = XNode Text [XNode]
62− | XLeaf Text Text
74+ | XLeaf Text Text
63− deriving (Eq, Show)
75+ deriving stock (Eq, Show)
64−
76+
65−toXML :: XNode -> ByteString
77+toXML :: Text -> XNode -> ByteString
78+toXML ns node =
79+ LBS.toStrict $
66−toXML node = LBS.toStrict $ renderLBS def $
80+ renderLBS def $
67− Document (Prologue [] Nothing []) (xmlNode node) []
81+ Document (Prologue [] Nothing []) (xmlNode node) []
68 where
82 where
69 xmlNode :: XNode -> Element
83 xmlNode :: XNode -> Element
70− xmlNode (XNode name nodes) = Element (s3Name name) M.empty $
84+ xmlNode (XNode name nodes) =
85+ Element (s3Name ns name) mempty $
71− map (NodeElement . xmlNode) nodes
86+ map (NodeElement . xmlNode) nodes
72− xmlNode (XLeaf name content) = Element (s3Name name) M.empty
87+ xmlNode (XLeaf name content) =
88+ Element
89+ (s3Name ns name)
90+ mempty
73− [NodeContent content]
91+ [NodeContent content]
74
92
75class ToXNode a where
93class ToXNode a where
76 toXNode :: a -> XNode
94 toXNode :: a -> XNode
77
95
78instance ToXNode Event where
96instance ToXNode Event where
79− toXNode = XLeaf "Event" . show
97+ toXNode = XLeaf "Event" . toText
80
98
81instance ToXNode Notification where
99instance ToXNode Notification where
100+ toXNode (Notification qc tc lc) =
82− toXNode (Notification qc tc lc) = XNode "NotificationConfiguration" $
101+ XNode "NotificationConfiguration" $
83− map (toXNodesWithArnName "QueueConfiguration" "Queue") qc ++
102+ map (toXNodesWithArnName "QueueConfiguration" "Queue") qc
84− map (toXNodesWithArnName "TopicConfiguration" "Topic") tc ++
103+ ++ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc
85− map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
104+ ++ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
86
105
87toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
106toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
88−toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
107+toXNodesWithArnName eltName arnName (NotificationConfig itemId arn events fRule) =
108+ XNode eltName $
89− XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events ++
109+ [XLeaf "Id" itemId, XLeaf arnName arn]
110+ ++ map toXNode events
90− [toXNode fRule]
111+ ++ [toXNode fRule]
91
112
92instance ToXNode Filter where
113instance ToXNode Filter where
93 toXNode (Filter (FilterKey (FilterRules rules))) =
114 toXNode (Filter (FilterKey (FilterRules rules))) =
94 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
115 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
95
116
96getFRXNode :: FilterRule -> XNode
117getFRXNode :: FilterRule -> XNode
97−getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
118+getFRXNode (FilterRule n v) =
119+ XNode
120+ "FilterRule"
121+ [ XLeaf "Name" n,
98− , XLeaf "Value" v
122+ XLeaf "Value" v
99− ]
123+ ]
100−
124+
101−mkPutNotificationRequest :: Notification -> ByteString
125+mkPutNotificationRequest :: Text -> Notification -> ByteString
102−mkPutNotificationRequest = toXML . toXNode
126+mkPutNotificationRequest ns = toXML ns . toXNode
127+
128+mkSelectRequest :: SelectRequest -> ByteString
129+mkSelectRequest r = LBS.toStrict $ renderLBS def sr
130+ where
131+ sr = Document (Prologue [] Nothing []) root []
132+ root =
133+ Element "SelectRequest" mempty $
134+ [ NodeElement
135+ ( Element
136+ "Expression"
137+ mempty
138+ [NodeContent $ srExpression r]
139+ ),
140+ NodeElement
141+ ( Element
142+ "ExpressionType"
143+ mempty
144+ [NodeContent $ show $ srExpressionType r]
145+ ),
146+ NodeElement
147+ ( Element "InputSerialization" mempty $
148+ inputSerializationNodes $
149+ srInputSerialization r
150+ ),
151+ NodeElement
152+ ( Element "OutputSerialization" mempty $
153+ outputSerializationNodes $
154+ srOutputSerialization r
155+ )
156+ ]
157+ ++ maybe [] reqProgElem (srRequestProgressEnabled r)
158+ reqProgElem enabled =
159+ [ NodeElement
160+ ( Element
161+ "RequestProgress"
162+ mempty
163+ [ NodeElement
164+ ( Element
165+ "Enabled"
166+ mempty
167+ [ NodeContent
168+ (if enabled then "TRUE" else "FALSE")
169+ ]
170+ )
171+ ]
172+ )
173+ ]
174+ inputSerializationNodes is =
175+ comprTypeNode (isCompressionType is)
176+ ++ [NodeElement $ formatNode (isFormatInfo is)]
177+ comprTypeNode (Just c) =
178+ [ NodeElement $
179+ Element
180+ "CompressionType"
181+ mempty
182+ [ NodeContent $ case c of
183+ CompressionTypeNone -> "NONE"
184+ CompressionTypeGzip -> "GZIP"
185+ CompressionTypeBzip2 -> "BZIP2"
186+ ]
187+ ]
188+ comprTypeNode Nothing = []
189+ kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
190+ formatNode (InputFormatCSV c) =
191+ Element
192+ "CSV"
193+ mempty
194+ (map (NodeElement . kvElement) (csvPropsList c))
195+ formatNode (InputFormatJSON p) =
196+ Element
197+ "JSON"
198+ mempty
199+ [ NodeElement
200+ ( Element
201+ "Type"
202+ mempty
203+ [ NodeContent $ case jsonipType p of
204+ JSONTypeDocument -> "DOCUMENT"
205+ JSONTypeLines -> "LINES"
206+ ]
207+ )
208+ ]
209+ formatNode InputFormatParquet = Element "Parquet" mempty []
210+ outputSerializationNodes (OutputSerializationJSON j) =
211+ [ NodeElement
212+ ( Element "JSON" mempty $
213+ rdElem $
214+ jsonopRecordDelimiter j
215+ )
216+ ]
217+ outputSerializationNodes (OutputSerializationCSV c) =
218+ [ NodeElement $
219+ Element
220+ "CSV"
221+ mempty
222+ (map (NodeElement . kvElement) (csvPropsList c))
223+ ]
224+ rdElem Nothing = []
225+ rdElem (Just t) =
226+ [ NodeElement $
227+ Element
228+ "RecordDelimiter"
229+ mempty
230+ [NodeContent t]
231+ ]
src/Network/Minio/XmlParser.hs+142−154
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,228 +15,216 @@
15--
15--
16
16
17module Network.Minio.XmlParser
17module Network.Minio.XmlParser
18− ( parseListBuckets
18+ ( parseListBuckets,
19− , parseLocation
19+ parseLocation,
20− , parseNewMultipartUpload
20+ parseNewMultipartUpload,
21− , parseCompleteMultipartUploadResponse
21+ parseCompleteMultipartUploadResponse,
22− , parseCopyObjectResponse
22+ parseCopyObjectResponse,
23− , parseListObjectsResponse
23+ parseListObjectsResponse,
24− , parseListObjectsV1Response
24+ parseListObjectsV1Response,
25− , parseListUploadsResponse
25+ parseListUploadsResponse,
26− , parseListPartsResponse
26+ parseListPartsResponse,
27− , parseErrResponse
27+ parseErrResponse,
28− , parseNotification
28+ parseNotification,
29+ parseSelectProgress,
30+ )
29− ) where
31+where
30−
32+
31−import Control.Monad.Trans.Resource
33+import qualified Data.ByteString.Lazy as LB
34+import qualified Data.HashMap.Strict as H
32−import Data.List (zip3, zip4)
35+import Data.List (zip4, zip6)
33−import qualified Data.Text as T
36+import qualified Data.Text as T
34−import Data.Text.Read (decimal)
35−import Data.Time
37+import Data.Time
36−import Text.XML
38+import Network.Minio.Data
39+import Network.Minio.XmlCommon
37−import Text.XML.Cursor hiding (bool)
40+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
72
41
73-- | Parse the response XML of a list buckets call.
42-- | Parse the response XML of a list buckets call.
74−parseListBuckets :: (MonadThrow m) => LByteString -> m [BucketInfo]
43+parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
75parseListBuckets xmldata = do
44parseListBuckets xmldata = do
76 r <- parseRoot xmldata
45 r <- parseRoot xmldata
77− let
46+ ns <- asks getSvcNamespace
47+ let s3Elem' = s3Elem ns
78− names = r $// s3Elem "Bucket" &// s3Elem "Name" &/ content
48+ names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
79− timeStrings = r $// s3Elem "Bucket" &// s3Elem "CreationDate" &/ content
49+ timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
80
50
81 times <- mapM parseS3XMLTime timeStrings
51 times <- mapM parseS3XMLTime timeStrings
82 return $ zipWith BucketInfo names times
52 return $ zipWith BucketInfo names times
83
53
84-- | Parse the response XML of a location request.
54-- | Parse the response XML of a location request.
85−parseLocation :: (MonadThrow m) => LByteString -> m Region
55+parseLocation :: (MonadIO m) => LByteString -> m Region
86parseLocation xmldata = do
56parseLocation xmldata = do
87 r <- parseRoot xmldata
57 r <- parseRoot xmldata
88 let region = T.concat $ r $/ content
58 let region = T.concat $ r $/ content
89 return $ bool "us-east-1" region $ region /= ""
59 return $ bool "us-east-1" region $ region /= ""
90
60
91-- | Parse the response XML of an newMultipartUpload call.
61-- | Parse the response XML of an newMultipartUpload call.
92−parseNewMultipartUpload :: (MonadThrow m) => LByteString -> m UploadId
62+parseNewMultipartUpload :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m UploadId
93parseNewMultipartUpload xmldata = do
63parseNewMultipartUpload xmldata = do
94 r <- parseRoot xmldata
64 r <- parseRoot xmldata
65+ ns <- asks getSvcNamespace
66+ let s3Elem' = s3Elem ns
95− return $ T.concat $ r $// s3Elem "UploadId" &/ content
67+ return $ T.concat $ r $// s3Elem' "UploadId" &/ content
96
68
97-- | Parse the response XML of completeMultipartUpload call.
69-- | Parse the response XML of completeMultipartUpload call.
98−parseCompleteMultipartUploadResponse :: (MonadThrow m) => LByteString -> m ETag
70+parseCompleteMultipartUploadResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ETag
99parseCompleteMultipartUploadResponse xmldata = do
71parseCompleteMultipartUploadResponse xmldata = do
100 r <- parseRoot xmldata
72 r <- parseRoot xmldata
73+ ns <- asks getSvcNamespace
74+ let s3Elem' = s3Elem ns
101− return $ T.concat $ r $// s3Elem "ETag" &/ content
75+ return $ T.concat $ r $// s3Elem' "ETag" &/ content
102
76
103-- | Parse the response XML of copyObject and copyObjectPart
77-- | Parse the response XML of copyObject and copyObjectPart
104−parseCopyObjectResponse :: (MonadThrow m) => LByteString -> m (ETag, UTCTime)
78+parseCopyObjectResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m (ETag, UTCTime)
105parseCopyObjectResponse xmldata = do
79parseCopyObjectResponse xmldata = do
106 r <- parseRoot xmldata
80 r <- parseRoot xmldata
107− let
81+ ns <- asks getSvcNamespace
82+ let s3Elem' = s3Elem ns
108− mtimeStr = T.concat $ r $// s3Elem "LastModified" &/ content
83+ mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
109
84
110 mtime <- parseS3XMLTime mtimeStr
85 mtime <- parseS3XMLTime mtimeStr
111− return (T.concat $ r $// s3Elem "ETag" &/ content, mtime)
86+ return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
112
87
113-- | Parse the response XML of a list objects v1 call.
88-- | Parse the response XML of a list objects v1 call.
114−parseListObjectsV1Response :: (MonadThrow m)
89+parseListObjectsV1Response ::
90+ (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
91+ LByteString ->
115− => LByteString -> m ListObjectsV1Result
92+ m ListObjectsV1Result
116parseListObjectsV1Response xmldata = do
93parseListObjectsV1Response xmldata = do
117 r <- parseRoot xmldata
94 r <- parseRoot xmldata
118− let
95+ ns <- asks getSvcNamespace
96+ let s3Elem' = s3Elem ns
119− hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
97+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
120−
121− nextMarker = headMay $ r $/ s3Elem "NextMarker" &/ content
98+ nextMarker = listToMaybe $ r $/ s3Elem' "NextMarker" &/ content
122−
123− prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content
99+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
124−
125− keys = r $/ s3Elem "Contents" &/ s3Elem "Key" &/ content
100+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
126− modTimeStr = r $/ s3Elem "Contents" &/ s3Elem "LastModified" &/ content
101+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
127− etagsList = r $/ s3Elem "Contents" &/ s3Elem "ETag" &/ content
102+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
128− -- if response xml contains empty etag response fill them with as
103+ -- if response xml contains empty etag response fill them with as
129− -- many empty Text for the zip4 below to work as intended.
104+ -- many empty Text for the zip4 below to work as intended.
130− etags = etagsList ++ repeat ""
105+ etags = etagsList ++ repeat ""
131− sizeStr = r $/ s3Elem "Contents" &/ s3Elem "Size" &/ content
106+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
132
107
133 modTimes <- mapM parseS3XMLTime modTimeStr
108 modTimes <- mapM parseS3XMLTime modTimeStr
134 sizes <- parseDecimals sizeStr
109 sizes <- parseDecimals sizeStr
135
110
136− let
111+ let objects =
112+ map (uncurry6 ObjectInfo) $
137− objects = map (uncurry4 ObjectInfo) $ zip4 keys modTimes etags sizes
113+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
138
114
139 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
115 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
140
116
141-- | Parse the response XML of a list objects call.
117-- | Parse the response XML of a list objects call.
142−parseListObjectsResponse :: (MonadThrow m) => LByteString -> m ListObjectsResult
118+parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ListObjectsResult
143parseListObjectsResponse xmldata = do
119parseListObjectsResponse xmldata = do
144 r <- parseRoot xmldata
120 r <- parseRoot xmldata
145− let
121+ ns <- asks getSvcNamespace
122+ let s3Elem' = s3Elem ns
146− hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
123+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
147−
148− nextToken = headMay $ r $/ s3Elem "NextContinuationToken" &/ content
124+ nextToken = listToMaybe $ r $/ s3Elem' "NextContinuationToken" &/ content
149−
150− prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content
125+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
151−
152− keys = r $/ s3Elem "Contents" &/ s3Elem "Key" &/ content
126+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
153− modTimeStr = r $/ s3Elem "Contents" &/ s3Elem "LastModified" &/ content
127+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
154− etagsList = r $/ s3Elem "Contents" &/ s3Elem "ETag" &/ content
128+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
155− -- if response xml contains empty etag response fill them with as
129+ -- if response xml contains empty etag response fill them with as
156− -- many empty Text for the zip4 below to work as intended.
130+ -- many empty Text for the zip4 below to work as intended.
157− etags = etagsList ++ repeat ""
131+ etags = etagsList ++ repeat ""
158− sizeStr = r $/ s3Elem "Contents" &/ s3Elem "Size" &/ content
132+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
159
133
160 modTimes <- mapM parseS3XMLTime modTimeStr
134 modTimes <- mapM parseS3XMLTime modTimeStr
161 sizes <- parseDecimals sizeStr
135 sizes <- parseDecimals sizeStr
162
136
163− let
137+ let objects =
138+ map (uncurry6 ObjectInfo) $
164− objects = map (uncurry4 ObjectInfo) $ zip4 keys modTimes etags sizes
139+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
165
140
166 return $ ListObjectsResult hasMore nextToken objects prefixes
141 return $ ListObjectsResult hasMore nextToken objects prefixes
167
142
168-- | Parse the response XML of a list incomplete multipart upload call.
143-- | Parse the response XML of a list incomplete multipart upload call.
169−parseListUploadsResponse :: (MonadThrow m) => LByteString -> m ListUploadsResult
144+parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ListUploadsResult
170parseListUploadsResponse xmldata = do
145parseListUploadsResponse xmldata = do
171 r <- parseRoot xmldata
146 r <- parseRoot xmldata
172− let
147+ ns <- asks getSvcNamespace
148+ let s3Elem' = s3Elem ns
173− hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
149+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
174− prefixes = r $/ s3Elem "CommonPrefixes" &/ s3Elem "Prefix" &/ content
150+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
175− nextKey = headMay $ r $/ s3Elem "NextKeyMarker" &/ content
151+ nextKey = listToMaybe $ r $/ s3Elem' "NextKeyMarker" &/ content
176− nextUpload = headMay $ r $/ s3Elem "NextUploadIdMarker" &/ content
152+ nextUpload = listToMaybe $ r $/ s3Elem' "NextUploadIdMarker" &/ content
177− uploadKeys = r $/ s3Elem "Upload" &/ s3Elem "Key" &/ content
153+ uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
178− uploadIds = r $/ s3Elem "Upload" &/ s3Elem "UploadId" &/ content
154+ uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
179− uploadInitTimeStr = r $/ s3Elem "Upload" &/ s3Elem "Initiated" &/ content
155+ uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
180
156
181 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
157 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
182
158
183− let
184− uploads = zip3 uploadKeys uploadIds uploadInitTimes
159+ let uploads = zip3 uploadKeys uploadIds uploadInitTimes
185
160
186 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
161 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
187
162
188−parseListPartsResponse :: (MonadThrow m) => LByteString -> m ListPartsResult
163+parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m ListPartsResult
189parseListPartsResponse xmldata = do
164parseListPartsResponse xmldata = do
190 r <- parseRoot xmldata
165 r <- parseRoot xmldata
191− let
166+ ns <- asks getSvcNamespace
167+ let s3Elem' = s3Elem ns
192− hasMore = ["true"] == (r $/ s3Elem "IsTruncated" &/ content)
168+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
193− nextPartNumStr = headMay $ r $/ s3Elem "NextPartNumberMarker" &/ content
169+ nextPartNumStr = listToMaybe $ r $/ s3Elem' "NextPartNumberMarker" &/ content
194− partNumberStr = r $/ s3Elem "Part" &/ s3Elem "PartNumber" &/ content
170+ partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
195− partModTimeStr = r $/ s3Elem "Part" &/ s3Elem "LastModified" &/ content
171+ partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
196− partETags = r $/ s3Elem "Part" &/ s3Elem "ETag" &/ content
172+ partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
197− partSizeStr = r $/ s3Elem "Part" &/ s3Elem "Size" &/ content
173+ partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
198
174
199 partModTimes <- mapM parseS3XMLTime partModTimeStr
175 partModTimes <- mapM parseS3XMLTime partModTimeStr
200 partSizes <- parseDecimals partSizeStr
176 partSizes <- parseDecimals partSizeStr
201 partNumbers <- parseDecimals partNumberStr
177 partNumbers <- parseDecimals partNumberStr
202 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
178 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
203
179
204− let
180+ let partInfos =
205− partInfos = map (uncurry4 ObjectPartInfo) $
181+ map (uncurry4 ObjectPartInfo) $
206− zip4 partNumbers partETags partSizes partModTimes
182+ zip4 partNumbers partETags partSizes partModTimes
207
183
208 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
184 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
209
185
210−
211−parseErrResponse :: (MonadThrow m) => LByteString -> m ServiceErr
212−parseErrResponse xmldata = do
213− r <- parseRoot xmldata
214− let code = T.concat $ r $/ element "Code" &/ content
215− message = T.concat $ r $/ element "Message" &/ content
216− return $ toServiceErr code message
217−
218−parseNotification :: (MonadThrow m) => LByteString -> m Notification
186+parseNotification :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m Notification
219parseNotification xmldata = do
187parseNotification xmldata = do
220 r <- parseRoot xmldata
188 r <- parseRoot xmldata
189+ ns <- asks getSvcNamespace
190+ let s3Elem' = s3Elem ns
221− let qcfg = map node $ r $/ s3Elem "QueueConfiguration"
191+ qcfg = map node $ r $/ s3Elem' "QueueConfiguration"
222− tcfg = map node $ r $/ s3Elem "TopicConfiguration"
192+ tcfg = map node $ r $/ s3Elem' "TopicConfiguration"
223− lcfg = map node $ r $/ s3Elem "CloudFunctionConfiguration"
193+ lcfg = map node $ r $/ s3Elem' "CloudFunctionConfiguration"
194+ Notification
224− Notification <$> (mapM (parseNode "Queue") qcfg)
195+ <$> mapM (parseNode ns "Queue") qcfg
225− <*> (mapM (parseNode "Topic") tcfg)
196+ <*> mapM (parseNode ns "Topic") tcfg
226− <*> (mapM (parseNode "CloudFunction") lcfg)
197+ <*> mapM (parseNode ns "CloudFunction") lcfg
227 where
198 where
228−
229− getFilterRule c =
199+ getFilterRule ns c =
230− let name = T.concat $ c $/ s3Elem "Name" &/ content
200+ let name = T.concat $ c $/ s3Elem ns "Name" &/ content
231− value = T.concat $ c $/ s3Elem "Value" &/ content
201+ value = T.concat $ c $/ s3Elem ns "Value" &/ content
232− in FilterRule name value
202+ in FilterRule name value
233−
234− parseNode arnName nodeData = do
203+ parseNode ns arnName nodeData = do
235 let c = fromNode nodeData
204 let c = fromNode nodeData
236− id = T.concat $ c $/ s3Elem "Id" &/ content
205+ itemId = T.concat $ c $/ s3Elem ns "Id" &/ content
237− arn = T.concat $ c $/ s3Elem arnName &/ content
206+ arn = T.concat $ c $/ s3Elem ns arnName &/ content
238− events = catMaybes $ map textToEvent $ c $/ s3Elem "Event" &/ content
207+ events = mapMaybe textToEvent (c $/ s3Elem ns "Event" &/ content)
208+ rules =
209+ c
239− rules = c $/ s3Elem "Filter" &/ s3Elem "S3Key" &/
210+ $/ s3Elem ns "Filter"
211+ &/ s3Elem ns "S3Key"
240− s3Elem "FilterRule" &| getFilterRule
212+ &/ s3Elem ns "FilterRule"
241− return $ NotificationConfig id arn events
213+ &| getFilterRule ns
214+ return $
215+ NotificationConfig
216+ itemId
217+ arn
218+ events
242− (Filter $ FilterKey $ FilterRules rules)
219+ (Filter $ FilterKey $ FilterRules rules)
220+
221+parseSelectProgress :: (MonadIO m) => ByteString -> m Progress
222+parseSelectProgress xmldata = do
223+ r <- parseRoot $ LB.fromStrict xmldata
224+ let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
225+ bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content
226+ bReturned = T.concat $ r $/ element "BytesReturned" &/ content
227+ Progress
228+ <$> parseDecimal bScanned
229+ <*> parseDecimal bProcessed
230+ <*> parseDecimal bReturned
stack.yaml+4−4
@@ -15,7 +15,7 @@
15# resolver:
15# resolver:
16# name: custom-snapshot
16# name: custom-snapshot
17# location: "./custom-snapshot.yaml"
17# location: "./custom-snapshot.yaml"
18−resolver: lts-9.1
18+resolver: lts-22.19
19
19
20# User packages to be built.
20# User packages to be built.
21# Various formats can be used as shown in the example below.
21# Various formats can be used as shown in the example below.
@@ -36,17 +36,17 @@ resolver: lts-9.1
36# non-dependency (i.e. a user package), and its test suites and benchmarks
36# non-dependency (i.e. a user package), and its test suites and benchmarks
37# will not be run. This is useful for tweaking upstream packages.
37# will not be run. This is useful for tweaking upstream packages.
38packages:
38packages:
39−- '.'
39+ - "."
40# Dependency packages to be pulled from upstream that are not in the resolver
40# Dependency packages to be pulled from upstream that are not in the resolver
41# (e.g., acme-missiles-0.3)
41# (e.g., acme-missiles-0.3)
42−extra-deps: []
42+extra-deps:
43+ - crypton-connection-0.3.2
43
44
44# Override default flag values for local packages and extra-deps
45# Override default flag values for local packages and extra-deps
45flags: {}
46flags: {}
46
47
47# Extra package databases containing global packages
48# Extra package databases containing global packages
48extra-package-dbs: []
49extra-package-dbs: []
49−
50# Control whether we use the GHC we find on the path
50# Control whether we use the GHC we find on the path
51# system-ghc: true
51# system-ghc: true
52#
52#
stack.yaml.locknew file+19
@@ -0,0 +1,19 @@
1+# This file was autogenerated by Stack.
2+# You should not edit this file by hand.
3+# For more information, please see the documentation at:
4+# https://docs.haskellstack.org/en/stable/lock_files
5+
6+packages:
7+- completed:
8+ hackage: crypton-connection-0.3.2@sha256:c7937edc25ab022bcf167703f2ec5ab73b62908e545bb587d2aa42b33cd6f6cc,1581
9+ pantry-tree:
10+ sha256: f986ad29b008cbe5732606e9cde1897191c486a2f1f169a4cb75fd915bce397c
11+ size: 394
12+ original:
13+ hackage: crypton-connection-0.3.2
14+snapshots:
15+- completed:
16+ sha256: e5cac927cf7ccbd52aa41476baa68b88c564ee6ddc3bc573dbf4210069287fe7
17+ size: 713340
18+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/19.yaml
19+ original: lts-22.19
test/LiveServer.hs+1135−478
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,38 +14,34 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17−import qualified Test.QuickCheck as Q
17+import Conduit (replicateC)
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
18+import qualified Conduit as C
26−
27−import qualified Control.Monad.Catch as MC
28−import qualified Control.Monad.Trans.Resource as R
19+import qualified Control.Monad.Trans.Resource as R
29−import qualified Data.ByteString as BS
20+import qualified Data.ByteString as BS
30−import Data.Conduit (yield, ($$))
21+import Data.Conduit (yield)
31−import qualified Data.Conduit as C
32−import qualified Data.Conduit.Binary as CB
22+import qualified Data.Conduit.Binary as CB
33−import Data.Conduit.Combinators (sinkList)
23+import Data.Conduit.Combinators (sinkList)
34−import Data.Default (Default (..))
35−import qualified Data.Map.Strict as Map
24+import qualified Data.HashMap.Strict as H
36−import qualified Data.Text as T
25+import qualified Data.Text as T
26+import Data.Time (fromGregorian)
37−import qualified Data.Time as Time
27+import qualified Data.Time as Time
28+import Lib.Prelude
38import qualified Network.HTTP.Client.MultipartFormData as Form
29import qualified Network.HTTP.Client.MultipartFormData as Form
39−import qualified Network.HTTP.Conduit as NC
30+import qualified Network.HTTP.Conduit as NC
40−import qualified Network.HTTP.Types as HT
31+import qualified Network.HTTP.Types as HT
41−import System.Environment (lookupEnv)
42−
43−import Network.Minio
32+import Network.Minio
44−import Network.Minio.Data
33+import Network.Minio.Credentials (Creds (CredsStatic))
45−import Network.Minio.ListOps
34+import Network.Minio.Data
46−import Network.Minio.PutObject
35+import Network.Minio.Data.Crypto
47−import Network.Minio.S3API
36+import Network.Minio.S3API
48−import Network.Minio.Utils
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
49
45
50main :: IO ()
46main :: IO ()
51main = defaultMain tests
47main = defaultMain tests
@@ -53,451 +49,574 @@ main = defaultMain tests
53tests :: TestTree
49tests :: TestTree
54tests = testGroup "Tests" [liveServerUnitTests]
50tests = testGroup "Tests" [liveServerUnitTests]
55
51
52+lookupTestEnv :: IO (Maybe ConnectInfo, (AccessKey, SecretKey))
53+lookupTestEnv = do
54+ val <- Env.lookupEnv "MINIO_LOCAL"
55+ secure <- Env.lookupEnv "MINIO_SECURE"
56+ let localServerMaybe = case (val, secure) of
57+ (Just _, Just _) -> Just "https://localhost:9000"
58+ (Just _, Nothing) -> Just "http://localhost:9000"
59+ _ -> Nothing
60+ accessKeyE <- Env.lookupEnv "MINIO_ACCESS_KEY"
61+ let accessKey = maybe "minioadmin" fromString accessKeyE
62+ secretKeyE <- Env.lookupEnv "MINIO_SECRET_KEY"
63+ let secretKey = maybe "minioadmin" fromString secretKeyE
64+ return (localServerMaybe, (accessKey, secretKey))
65+
66+loadTestServerConnInfo :: IO ConnectInfo
67+loadTestServerConnInfo = do
68+ (localServerMaybe, (ak, sk)) <- lookupTestEnv
69+ return $ case localServerMaybe of
70+ Just localServer -> do
71+ setCreds (CredentialValue ak sk mempty) localServer
72+ _ -> minioPlayCI
73+
74+loadTestServerConnInfoSTS :: IO ConnectInfo
75+loadTestServerConnInfoSTS = do
76+ (localServerMaybe, (ak, sk)) <- lookupTestEnv
77+ case localServerMaybe of
78+ Just localServer -> do
79+ let creds = CredentialValue ak sk mempty
80+ assumeRole =
81+ STSAssumeRole
82+ { sarCredentials = creds,
83+ sarOptions = defaultSTSAssumeRoleOptions
84+ }
85+ setSTSCredential assumeRole localServer
86+ Nothing -> do
87+ let creds = case connectCreds minioPlayCI of
88+ CredsStatic c -> c
89+ _ -> error "unexpected play creds"
90+ assumeRole =
91+ STSAssumeRole
92+ { sarCredentials = creds,
93+ sarOptions = defaultSTSAssumeRoleOptions
94+ }
95+ setSTSCredential assumeRole minioPlayCI
96+
56-- conduit that generates random binary stream of given length
97-- conduit that generates random binary stream of given length
57−randomDataSrc :: MonadIO m => Int64 -> C.Producer m ByteString
98+randomDataSrc :: (MonadIO m) => Int64 -> C.ConduitM () ByteString m ()
58−randomDataSrc s' = genBS s'
99+randomDataSrc = genBS
59 where
100 where
60− oneMiB = 1024*1024
101+ concatIt bs n =
61−
102+ BS.concat $
62− concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++
103+ replicate (fromIntegral q) bs
63− [BS.take (fromIntegral r) bs]
104+ ++ [BS.take (fromIntegral r) bs]
105+ where
64− where (q, r) = n `divMod` fromIntegral (BS.length bs)
106+ (q, r) = n `divMod` fromIntegral (BS.length bs)
65−
66 genBS s = do
107 genBS s = do
67 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
108 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
68 let byteArr64 = BS.pack w8s
109 let byteArr64 = BS.pack w8s
69 if s < oneMiB
110 if s < oneMiB
70 then yield $ concatIt byteArr64 s
111 then yield $ concatIt byteArr64 s
112+ else do
71− else do yield $ concatIt byteArr64 oneMiB
113+ yield $ concatIt byteArr64 oneMiB
72− genBS (s - oneMiB)
114+ genBS (s - oneMiB)
73
115
74−mkRandFile :: R.MonadResource m => Int64 -> m FilePath
116+mkRandFile :: (R.MonadResource m) => Int64 -> m FilePath
75mkRandFile size = do
117mkRandFile size = do
76− dir <- liftIO $ getTemporaryDirectory
118+ dir <- liftIO getTemporaryDirectory
77− randomDataSrc size C.$$ CB.sinkTempFile dir "miniohstest.random"
119+ C.runConduit $ randomDataSrc size C..| CB.sinkTempFile dir "miniohstest.random"
78
120
79funTestBucketPrefix :: Text
121funTestBucketPrefix :: Text
80funTestBucketPrefix = "miniohstest-"
122funTestBucketPrefix = "miniohstest-"
81
123
124+funTestWithBucketNoSTS ::
82−funTestWithBucket :: TestName
125+ TestName ->
83− -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree
126+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
127+ TestTree
84−funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
128+funTestWithBucketNoSTS t minioTest = testCaseSteps t $ \step -> do
85 -- generate a random name for the bucket
129 -- generate a random name for the bucket
86 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
130 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
87 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
131 let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
88 liftStep = liftIO . step
132 liftStep = liftIO . step
89− connInfo <- maybe minioPlayCI (const def) <$> lookupEnv "MINIO_LOCAL"
133+ connInfo <- loadTestServerConnInfo
90 ret <- runMinio connInfo $ do
134 ret <- runMinio connInfo $ do
91 liftStep $ "Creating bucket for test - " ++ t
135 liftStep $ "Creating bucket for test - " ++ t
92 foundBucket <- bucketExists b
136 foundBucket <- bucketExists b
93 liftIO $ foundBucket @?= False
137 liftIO $ foundBucket @?= False
94− makeBucket b def
138+ makeBucket b Nothing
95 minioTest liftStep b
139 minioTest liftStep b
96 deleteBucket b
140 deleteBucket b
97 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
141 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
98
142
99−lowLevelMultipartTest :: TestTree
143+funTestWithBucket ::
100−lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
144+ TestName ->
101− \step bucket -> do
145+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
102− -- low-level multipart operation tests.
146+ TestTree
103− let object = "newmpupload"
147+funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
104− mb15 = 15 * 1024 * 1024
148+ -- generate a random name for the bucket
105−
149+ bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
106− step "Prepare for low-level multipart tests."
150+ let b = T.concat [funTestBucketPrefix, T.pack bktSuffix]
107− step "create new multipart upload"
151+ liftStep = liftIO . step
108− uid <- newMultipartUpload bucket object []
152+ connInfo <- loadTestServerConnInfo
109− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
153+ ret <- runMinio connInfo $ do
110−
154+ liftStep $ "Creating bucket for test - " ++ t
111− randFile <- mkRandFile mb15
155+ foundBucket <- bucketExists b
112−
156+ liftIO $ foundBucket @?= False
113− step "put object parts 1 of 1"
157+ makeBucket b Nothing
114− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
158+ minioTest liftStep b
115− partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
159+ deleteBucket b
116−
160+ isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
117− step "complete multipart"
118− void $ completeMultipartUpload bucket object uid [partInfo]
119−
120− destFile <- mkRandFile 0
121− step "Retrieve the created object and check size"
122− fGetObject bucket object destFile
123− gotSize <- withNewHandle destFile getFileSize
124− liftIO $ gotSize == Right (Just mb15) @?
125− "Wrong file size of put file after getting"
126−
127− step "Cleanup actions"
128− removeObject bucket object
129−
130−putObjectNoSizeTest :: TestTree
131−putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
132− \step bucket -> do
133− -- putObject test (conduit source, no size specified)
134− let obj = "mpart"
135− mb70 = 70 * 1024 * 1024
136−
137− step "Prepare for putObject with from source without providing size."
138− rFile <- mkRandFile mb70
139−
140− step "Upload multipart file."
141− putObject bucket obj (CB.sourceFile rFile) Nothing
142
161
143− step "Retrieve and verify file size"
162+ connInfoSTS <- loadTestServerConnInfoSTS
144− destFile <- mkRandFile 0
163+ let t' = t ++ " (with AssumeRole Credentials)"
145− fGetObject bucket obj destFile
164+ ret' <- runMinio connInfoSTS $ do
146− gotSize <- withNewHandle destFile getFileSize
165+ liftStep $ "Creating bucket for test - " ++ t'
147− liftIO $ gotSize == Right (Just mb70) @?
166+ foundBucket <- bucketExists b
148− "Wrong file size of put file after getting"
167+ liftIO $ foundBucket @?= False
168+ makeBucket b Nothing
169+ minioTest liftStep b
170+ deleteBucket b
171+ isRight ret' @? ("Functional test " ++ t' ++ " failed => " ++ show ret')
149
172
150− step "Cleanup actions"
173+liveServerUnitTests :: TestTree
151− deleteObject bucket obj
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+ ]
152
196
153−highLevelListingTest :: TestTree
197+basicTests :: TestTree
154−highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
198+basicTests = funTestWithBucket "Basic tests" $
155 \step bucket -> do
199 \step bucket -> do
156− step "High-level listObjects Test"
200+ step "getService works and contains the test bucket."
157− step "put 3 objects"
201+ buckets <- getService
158− let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3"]
202+ unless (length (filter (== bucket) $ map biName buckets) == 1) $
159− forM_ expectedObjects $
203+ liftIO $
160− \obj -> fPutObject bucket obj "/etc/lsb-release"
204+ assertFailure
161−
205+ ( "The bucket "
162− step "High-level listing of objects"
206+ ++ show bucket
163− objects <- listObjects bucket Nothing True $$ sinkList
207+ ++ " was expected to exist."
164−
208+ )
165− liftIO $ assertEqual "Objects match failed!" (sort expectedObjects)
209+
166− (map oiObject objects)
210+ step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
167−
211+ mbE <- try $ makeBucket bucket Nothing
168− step "High-level listing of objects (version 1)"
212+ case mbE of
169− objects <- listObjectsV1 bucket Nothing True $$ sinkList
213+ Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
170−
214+ _ -> return ()
171− step "Cleanup actions"
215+
172− forM_ expectedObjects $
216+ step "makeBucket with an invalid bucket name and check for appropriate exception."
173− \obj -> removeObject bucket obj
217+ invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
174−
218+ case invalidMBE of
175− step "High-level listIncompleteUploads Test"
219+ Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
176− let object = "newmpupload"
220+ _ -> return ()
177− step "create 10 multipart uploads"
221+
178− forM_ [1..10::Int] $ \_ -> do
222+ step "getLocation works"
179− uid <- newMultipartUpload bucket object []
223+ region <- getLocation bucket
180− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
224+ liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region)
181−
225+
182− step "High-level listing of incomplete multipart uploads"
226+ step "singlepart putObject works"
183− uploads <- listIncompleteUploads bucket Nothing True $$ sinkList
227+ testFilepath <- mkRandFile 200
184− -- Minio server behaviour changed to list no incomplete uploads,
228+ fPutObject bucket "test-file" testFilepath defaultPutObjectOptions
185− -- so the check below reflects this; this test is expected to
229+
186− -- fail on AWS S3.
230+ step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception"
187− liftIO $ length uploads @?= 0
231+ fpE <- try $ fPutObject "nosuchbucket" "test-file-2" testFilepath defaultPutObjectOptions
188−
232+ case fpE of
189− step "cleanup"
233+ Left exn -> liftIO $ exn @?= NoSuchBucket
190− forM_ uploads $ \(UploadInfo _ uid _ _) ->
234+ _ -> return ()
191− abortMultipartUpload bucket object uid
235+
192−
236+ outFile <- mkRandFile 0
193− step "High-level listIncompleteParts Test"
237+ step "simple fGetObject works"
194− let mb5 = 5 * 1024 * 1024
238+ fGetObject bucket "test-file" outFile defaultGetObjectOptions
195−
239+
196− step "create a multipart upload"
240+ let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
197− uid <- newMultipartUpload bucket object []
241+ step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
198− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
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+ }
199
291
200− step "put object parts 1..10"
292+ step "fGetObject a non-existent object and check for NoSuchKey exception"
201− inputFile <- mkRandFile mb5
293+ resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
202− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
294+ case resE3 of
203− forM_ [1..10] $ \pnum ->
295+ Left exn -> liftIO $ exn @?= NoSuchKey
204− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
296+ _ -> return ()
205
297
206− step "fetch list parts"
298+ step "create new multipart upload works"
207− incompleteParts <- listIncompleteParts bucket object uid $$ sinkList
299+ uid <- newMultipartUpload bucket "newmpupload" []
208− liftIO $ length incompleteParts @?= 10
300+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
209
301
210− step "cleanup"
302+ step "abort a new multipart upload works"
211− abortMultipartUpload bucket object uid
303+ abortMultipartUpload bucket "newmpupload" uid
212
304
213−listingTest :: TestTree
305+ step "delete object works"
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
306+ deleteObject bucket "test-file"
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.")
266
307
267− step "put object parts 1..10"
308+ step "statObject test"
309+ let object = "sample"
310+ step "create an object"
268− inputFile <- mkRandFile mb5
311+ inputFile <- mkRandFile 0
269− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
312+ fPutObject bucket object inputFile defaultPutObjectOptions
270− forM_ [1..10] $ \pnum ->
271− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
272
313
273− step "fetch list parts"
314+ step "get metadata of the object"
274− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
315+ res <- statObject bucket object defaultGetObjectOptions
275− liftIO $ (length $ lprParts listPartsResult) @?= 10
316+ liftIO $ oiSize res @?= 0
276− abortMultipartUpload bucket object uid
277
317
318+ step "delete object"
319+ deleteObject bucket object
278
320
279−liveServerUnitTests :: TestTree
321+lowLevelMultipartTest :: TestTree
280−liveServerUnitTests = testGroup "Unit tests against a live server"
322+lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
281− [ basicTests
282− , listingTest
283− , highLevelListingTest
284− , lowLevelMultipartTest
285− , putObjectNoSizeTest
286− , funTestWithBucket "Multipart Tests" $
287− \step bucket -> do
323+ \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.
324+ -- low-level multipart operation tests.
310− let object = "newmpupload"
325+ let object = "newmpupload"
311− kb5 = 5 * 1024
326+ mb15 = 15 * 1024 * 1024
312−
327+
328+ step "Prepare for low-level multipart tests."
313− step "create new multipart upload"
329+ step "create new multipart upload"
314− uid <- newMultipartUpload bucket object []
330+ uid <- newMultipartUpload bucket object []
315− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
331+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
316−
332+
317− randFile <- mkRandFile kb5
333+ randFile <- mkRandFile mb15
318−
334+
319− step "upload 2 parts"
335+ step "put object parts 1 of 1"
320− forM_ [1,2] $ \partNum -> do
336+ partInfo <-
321− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
337+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
322− void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
338+ putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
323−
339+
324− step "remove ongoing upload"
340+ step "complete multipart"
325− removeIncompleteUpload bucket object
341+ void $ completeMultipartUpload bucket object uid [partInfo]
326− uploads <- listIncompleteUploads bucket (Just object) False C.$$ sinkList
342+
327− liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
343+ destFile <- mkRandFile 0
328−
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
329
361
330− , funTestWithBucket "copyObject related tests" $ \step bucket -> do
362+ step "Prepare for putObject with from source with size."
331− step "copyObjectSingle basic tests"
363+ rFile <- mkRandFile mb1
332− let object = "xxx"
333− objCopy = "xxxCopy"
334− size1 = 100 :: Int64
335
364
336− step "create server object to copy"
365+ step "Upload single file."
337− inputFile <- mkRandFile size1
366+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
338− fPutObject bucket object inputFile
339
367
340− step "copy object"
368+ step "Retrieve and verify file size"
341− let cps = def { cpSource = format "/{}/{}" [bucket, object] }
369+ destFile <- mkRandFile 0
342− (etag, modTime) <- copyObjectSingle bucket objCopy cps []
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"
343
376
344− -- retrieve obj info to check
377+ step "Cleanup actions"
345− ObjectInfo _ t e s <- headObject bucket objCopy
378+ deleteObject bucket obj
346
379
347− let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
380+putObjectNoSizeTest :: TestTree
381+putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
382+ \step bucket -> do
383+ -- putObject test (conduit source, no size specified)
384+ let obj = "mpart"
385+ mb70 = 70 * 1024 * 1024
348
386
349− liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
387+ step "Prepare for putObject with from source without providing size."
350− "Copied object did not match expected."
388+ rFile <- mkRandFile mb70
351
389
352− step "cleanup actions"
390+ step "Upload multipart file."
353− removeObject bucket object
391+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
354− removeObject bucket objCopy
355
392
356− step "copyObjectPart basic tests"
393+ step "Retrieve and verify file size"
357− let srcObj = "XXX"
394+ destFile <- mkRandFile 0
358− copyObj = "XXXCopy"
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"
359
401
360− step "Prepare"
402+ step "Cleanup actions"
361− let mb15 = 15 * 1024 * 1024
403+ deleteObject bucket obj
362− mb5 = 5 * 1024 * 1024
363− randFile <- mkRandFile mb15
364− fPutObject bucket srcObj randFile
365
404
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"
366− step "create new multipart upload"
502+ step "create 10 multipart uploads"
503+ forM_ [1 .. 10 :: Int] $ \_ -> do
367− uid <- newMultipartUpload bucket copyObj []
504+ uid <- newMultipartUpload bucket object []
368 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
505 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
369
506
370− step "put object parts 1-3"
507+ step "High-level listing of incomplete multipart uploads"
371− let cps' = def {cpSource = format "/{}/{}" [bucket, srcObj]}
508+ uploads <-
372− parts <- forM [1..3] $ \p -> do
509+ C.runConduit $
373− (etag', _) <- copyObjectPart bucket copyObj cps'{
510+ listIncompleteUploads bucket (Just "newmpupload") True
374− cpSourceRange = Just ((p-1)*mb5, (p-1)*mb5 + (mb5 - 1))
511+ C..| sinkList
375− } uid (fromIntegral p) []
512+ liftIO $ length uploads @?= 10
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
513
514+ step "cleanup"
515+ forM_ uploads $ \(UploadInfo _ uid _ _) ->
516+ abortMultipartUpload bucket object uid
478
517
479− step "create new multipart upload works"
518+ step "High-level listIncompleteParts Test"
480− uid <- newMultipartUpload bucket "newmpupload" []
519+ let mb5 = 5 * 1024 * 1024
481− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
482
520
483− step "abort a new multipart upload works"
521+ step "create a multipart upload"
484− abortMultipartUpload bucket "newmpupload" uid
522+ uid <- newMultipartUpload bucket object []
523+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
485
524
486− step "delete object works"
525+ step "put object parts 1..10"
487− deleteObject bucket "lsb-release"
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
488
530
489− step "statObject test"
531+ step "fetch list parts"
490− let object = "sample"
532+ incompleteParts <-
491− step "create an object"
533+ C.runConduit $
492− inputFile <- mkRandFile 0
534+ listIncompleteParts bucket object uid
493− fPutObject bucket object inputFile
535+ C..| sinkList
536+ liftIO $ length incompleteParts @?= 10
494
537
495− step "get metadata of the object"
538+ step "cleanup"
496− res <- statObject bucket object
539+ abortMultipartUpload bucket object uid
497− liftIO $ (oiSize res) @?= 0
498
540
499− step "delete object"
541+listingTest :: TestTree
542+listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
543+ step "listObjects' test"
544+ step "put 10 objects"
545+ let objects = (\s -> T.concat ["test-file-", T.pack (show s)]) <$> [1 .. 10 :: Int]
546+
547+ testFilepath <- mkRandFile 200
548+ forM_ [1 .. 10 :: Int] $ \s ->
549+ fPutObject bucket (T.concat ["test-file-", T.pack (show s)]) testFilepath defaultPutObjectOptions
550+
551+ step "Simple list"
552+ res <- listObjects' bucket Nothing Nothing Nothing Nothing
553+ let expectedObjects = sort objects
554+ liftIO $
555+ assertEqual
556+ "Objects match failed!"
557+ expectedObjects
558+ (map oiObject $ lorObjects res)
559+
560+ step "Simple list version 1"
561+ resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
562+ let expected =
563+ sort $
564+ map
565+ ( T.concat
566+ . ("test-file-" :)
567+ . (: [])
568+ . T.pack
569+ . show
570+ )
571+ [1 .. 10 :: Int]
572+ liftIO $
573+ assertEqual
574+ "Objects match failed!"
575+ expected
576+ (map oiObject $ lorObjects' resV1)
577+
578+ step "Cleanup actions"
500− deleteObject bucket object
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
501
620
502presignedUrlFunTest :: TestTree
621presignedUrlFunTest :: TestTree
503presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
622presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -509,97 +628,120 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
509 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
628 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
510
629
511 step "PUT object presigned URL - makePresignedUrl"
630 step "PUT object presigned URL - makePresignedUrl"
631+ putUrl <-
512− putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
632+ makePresignedUrl
633+ 3600
634+ HT.methodPut
635+ (Just bucket)
636+ (Just obj)
513− (Just obj) (Just "us-east-1") [] []
637+ (Just "us-east-1")
514−
638+ []
639+ []
640+
641+ -- print putUrl
515 let size1 = 1000 :: Int64
642 let size1 = 1000 :: Int64
516 inputFile <- mkRandFile size1
643 inputFile <- mkRandFile size1
517
644
518 -- attempt to upload using the presigned URL
645 -- attempt to upload using the presigned URL
519 putResp <- putR size1 inputFile mgr putUrl
646 putResp <- putR size1 inputFile mgr putUrl
647+ liftIO $
520− liftIO $ (NC.responseStatus putResp == HT.status200) @?
648+ (NC.responseStatus putResp == HT.status200)
521− "presigned PUT failed"
649+ @? "presigned PUT failed"
522
650
523 step "GET object presigned URL - makePresignedUrl"
651 step "GET object presigned URL - makePresignedUrl"
652+ getUrl <-
524− getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
653+ makePresignedUrl
654+ 3600
655+ HT.methodGet
656+ (Just bucket)
657+ (Just obj)
525− (Just obj) (Just "us-east-1") [] []
658+ (Just "us-east-1")
659+ []
660+ []
526
661
527 getResp <- getR mgr getUrl
662 getResp <- getR mgr getUrl
663+ liftIO $
528− liftIO $ (NC.responseStatus getResp == HT.status200) @?
664+ (NC.responseStatus getResp == HT.status200)
529− "presigned GET failed"
665+ @? "presigned GET failed"
530
666
531 -- read content from file to compare with response above
667 -- read content from file to compare with response above
532− bs <- CB.sourceFile inputFile $$ CB.sinkLbs
668+ bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
669+ liftIO $
533− liftIO $ (bs == NC.responseBody getResp) @?
670+ (bs == NC.responseBody getResp)
534− "presigned put and get got mismatched data"
671+ @? "presigned put and get got mismatched data"
535
672
536 step "PUT object presigned - presignedPutObjectURL"
673 step "PUT object presigned - presignedPutObjectURL"
537− putUrl2 <- presignedPutObjectUrl bucket obj2 3600 []
674+ putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
538
675
539 let size2 = 1200
676 let size2 = 1200
540 testFile <- mkRandFile size2
677 testFile <- mkRandFile size2
541
678
542 putResp2 <- putR size2 testFile mgr putUrl2
679 putResp2 <- putR size2 testFile mgr putUrl2
680+ liftIO $
543− liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
681+ (NC.responseStatus putResp2 == HT.status200)
544− "presigned PUT failed (presignedPutObjectUrl)"
682+ @? "presigned PUT failed (presignedPutObjectUrl)"
545
683
546 step "HEAD object presigned URL - presignedHeadObjectUrl"
684 step "HEAD object presigned URL - presignedHeadObjectUrl"
547 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
685 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
548
686
549− headResp <- do req <- NC.parseRequest $ toS headUrl
687+ headResp <- do
688+ let req = NC.parseRequest_ $ decodeUtf8 headUrl
550− NC.httpLbs (req {NC.method = HT.methodHead}) mgr
689+ NC.httpLbs (req {NC.method = HT.methodHead}) mgr
690+ liftIO $
551− liftIO $ (NC.responseStatus headResp == HT.status200) @?
691+ (NC.responseStatus headResp == HT.status200)
552− "presigned HEAD failed (presignedHeadObjectUrl)"
692+ @? "presigned HEAD failed (presignedHeadObjectUrl)"
553
693
554 -- check that header info is accurate
694 -- check that header info is accurate
555− let h = Map.fromList $ NC.responseHeaders headResp
695+ let h = H.fromList $ NC.responseHeaders headResp
556− cLen = Map.findWithDefault "0" HT.hContentLength h
696+ cLen = H.lookupDefault "0" HT.hContentLength h
557− liftIO $ (cLen == show size2) @? "Head req returned bad content length"
697+ liftIO $ (cLen == showBS size2) @? "Head req returned bad content length"
558
698
559 step "GET object presigned URL - presignedGetObjectUrl"
699 step "GET object presigned URL - presignedGetObjectUrl"
560 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
700 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
561
701
562 getResp2 <- getR mgr getUrl2
702 getResp2 <- getR mgr getUrl2
703+ liftIO $
563− liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
704+ (NC.responseStatus getResp2 == HT.status200)
564− "presigned GET failed (presignedGetObjectUrl)"
705+ @? "presigned GET failed (presignedGetObjectUrl)"
565
706
566 -- read content from file to compare with response above
707 -- read content from file to compare with response above
567− bs2 <- CB.sourceFile testFile $$ CB.sinkLbs
708+ bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
709+ liftIO $
568− liftIO $ (bs2 == NC.responseBody getResp2) @?
710+ (bs2 == NC.responseBody getResp2)
569− "presigned put and get got mismatched data (presigned*Url)"
711+ @? "presigned put and get got mismatched data (presigned*Url)"
570−
571
712
572 mapM_ (removeObject bucket) [obj, obj2]
713 mapM_ (removeObject bucket) [obj, obj2]
573 where
714 where
574 putR size filePath mgr url = do
715 putR size filePath mgr url = do
575− req <- NC.parseRequest $ toS url
716+ let req = NC.parseRequest_ $ decodeUtf8 url
717+ let req' =
718+ req
576− let req' = req { NC.method = HT.methodPut
719+ { NC.method = HT.methodPut,
720+ NC.requestBody =
577− , NC.requestBody = NC.requestBodySource size $
721+ NC.requestBodySource size $
578− CB.sourceFile filePath}
722+ CB.sourceFile filePath
723+ }
579 NC.httpLbs req' mgr
724 NC.httpLbs req' mgr
580−
581 getR mgr url = do
725 getR mgr url = do
582− req <- NC.parseRequest $ toS url
726+ let req = NC.parseRequest_ $ decodeUtf8 url
583 NC.httpLbs req mgr
727 NC.httpLbs req mgr
584
728
585presignedPostPolicyFunTest :: TestTree
729presignedPostPolicyFunTest :: TestTree
586−presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
730+presignedPostPolicyFunTest = funTestWithBucketNoSTS "Presigned Post Policy tests" $
587 \step bucket -> do
731 \step bucket -> do
588−
589 step "presignedPostPolicy basic test"
732 step "presignedPostPolicy basic test"
590− now <- liftIO $ Time.getCurrentTime
733+ now <- liftIO Time.getCurrentTime
591
734
592 let key = "presignedPostPolicyTest/myfile"
735 let key = "presignedPostPolicyTest/myfile"
736+ policyConds =
593− policyConds = [ ppCondBucket bucket
737+ [ ppCondBucket bucket,
594− , ppCondKey key
738+ ppCondKey key,
595− , ppCondContentLengthRange 1 1000
739+ ppCondContentLengthRange 1 1000,
596− , ppCondContentType "application/octet-stream"
740+ ppCondContentType "application/octet-stream",
597− , ppCondSuccessActionStatus 200
741+ ppCondSuccessActionStatus 200
598− ]
742+ ]
599−
600 expirationTime = Time.addUTCTime 3600 now
743 expirationTime = Time.addUTCTime 3600 now
601 postPolicyE = newPostPolicy expirationTime policyConds
744 postPolicyE = newPostPolicy expirationTime policyConds
602−
603 size = 1000 :: Int64
745 size = 1000 :: Int64
604
746
605 inputFile <- mkRandFile size
747 inputFile <- mkRandFile size
@@ -610,17 +752,532 @@ presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
610 (url, formData) <- presignedPostPolicy postPolicy
752 (url, formData) <- presignedPostPolicy postPolicy
611 -- liftIO (print url) >> liftIO (print formData)
753 -- liftIO (print url) >> liftIO (print formData)
612 result <- liftIO $ postForm url formData inputFile
754 result <- liftIO $ postForm url formData inputFile
755+ liftIO $
613− liftIO $ (NC.responseStatus result == HT.status200) @?
756+ (NC.responseStatus result == HT.status200)
614− "presigned POST failed"
757+ @? "presigned POST failed"
615
758
616 mapM_ (removeObject bucket) [key]
759 mapM_ (removeObject bucket) [key]
617− where
760+ where
618−
761+ postForm url formData inputFile = do
619− postForm url formData inputFile = do
762+ req <- NC.parseRequest $ decodeUtf8 url
620− req <- NC.parseRequest $ toS url
763+ let parts =
621− let parts = map (\(x, y) -> Form.partBS x y) $
764+ map (uncurry Form.partBS) $
622− Map.toList formData
765+ H.toList formData
623− parts' = parts ++ [Form.partFile "file" inputFile]
766+ parts' = parts ++ [Form.partFile "file" inputFile]
624− req' <- Form.formDataBody parts' req
767+ req' <- Form.formDataBody parts' req
625− mgr <- NC.newManager NC.tlsManagerSettings
768+ mgr <- NC.newManager NC.tlsManagerSettings
626− NC.httpLbs req' mgr
769+ NC.httpLbs req' mgr
770+
771+bucketPolicyFunTest :: TestTree
772+bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
773+ \step bucket -> do
774+ step "bucketPolicy basic test - no policy exception"
775+ resE <- try $ getBucketPolicy bucket
776+ case resE of
777+ Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
778+ _ -> return ()
779+
780+ resE' <- try $ setBucketPolicy bucket T.empty
781+ case resE' of
782+ Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
783+ _ -> return ()
784+
785+ let expectedPolicyJSON = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::testbucket/*\"]}]}"
786+
787+ step "try a malformed policy, expect error"
788+ resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
789+ case resE'' of
790+ Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
791+ _ -> return ()
792+
793+ let expectedPolicyJSON' = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "\"]},{\"Action\":[\"s3:GetObject\"],\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Resource\":[\"arn:aws:s3:::" <> bucket <> "/*\"]}]}"
794+
795+ step "set bucket policy"
796+ setBucketPolicy bucket expectedPolicyJSON'
797+
798+ let obj = "myobject"
799+
800+ step "verify bucket policy: (1) create `myobject`"
801+ putObject bucket obj (replicateC 100 "c") Nothing defaultPutObjectOptions
802+
803+ step "verify bucket policy: (2) get `myobject` anonymously"
804+ connInfo <- asks mcConnInfo
805+ let proto = bool "http://" "https://" $ connectIsSecure connInfo
806+ url =
807+ BS.concat
808+ [ proto,
809+ getHostAddr connInfo,
810+ "/",
811+ encodeUtf8 bucket,
812+ "/",
813+ encodeUtf8 obj
814+ ]
815+ respE <-
816+ liftIO $
817+ fmap (Right . toStrictBS) (NC.simpleHttp $ decodeUtf8 url)
818+ `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
819+ case respE of
820+ Left err -> liftIO $ assertFailure $ show err
821+ Right s -> liftIO $ s @?= BS.concat (replicate 100 "c")
822+
823+ deleteObject bucket obj
824+
825+ step "delete bucket policy"
826+ setBucketPolicy bucket T.empty
827+
828+multipartTest :: TestTree
829+multipartTest = funTestWithBucket "Multipart Tests" $
830+ \step bucket -> do
831+ -- Commenting out test since it's platform specific.
832+ -- FIXME: Need to find a platform agnostic way to test this.
833+ -- step "Prepare for putObjectInternal with non-seekable file, with size."
834+ -- step "Upload multipart file."
835+ -- let mb80 = 80 * 1024 * 1024
836+ -- obj = "mpart"
837+
838+ -- void $ putObjectInternal bucket obj defaultPutObjectOptions $ ODFile "/dev/zero" (Just mb80)
839+
840+ -- step "Retrieve and verify file size"
841+ -- destFile <- mkRandFile 0
842+ -- fGetObject bucket obj destFile defaultGetObjectOptions
843+ -- gotSize <- withNewHandle destFile getFileSize
844+ -- liftIO $
845+ -- gotSize == Right (Just mb80)
846+ -- @? "Wrong file size of put file after getting"
847+
848+ -- step "Cleanup actions"
849+ -- removeObject bucket obj
850+
851+ -- step "cleanup"
852+ -- removeObject bucket "big"
853+
854+ step "Prepare for removeIncompleteUpload"
855+ -- low-level multipart operation tests.
856+ let object = "newmpupload"
857+ kb5 = 5 * 1024
858+
859+ step "create new multipart upload"
860+ uid <- newMultipartUpload bucket object []
861+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
862+
863+ randFile <- mkRandFile kb5
864+
865+ step "upload 2 parts"
866+ forM_ [1, 2] $ \partNum -> do
867+ UIO.withBinaryFile randFile UIO.ReadMode $ \h ->
868+ void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
869+
870+ step "remove ongoing upload"
871+ removeIncompleteUpload bucket object
872+ uploads <-
873+ C.runConduit $
874+ listIncompleteUploads bucket (Just object) False
875+ C..| sinkList
876+ liftIO $ null uploads @? "removeIncompleteUploads didn't complete successfully"
877+
878+putObjectContentTypeTest :: TestTree
879+putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
880+ \step bucket -> do
881+ step "fPutObject content type test"
882+ let object = "xxx-content-type"
883+ size1 = 100 :: Int64
884+
885+ step "create server object with content-type"
886+ inputFile <- mkRandFile size1
887+ fPutObject
888+ bucket
889+ object
890+ inputFile
891+ defaultPutObjectOptions
892+ { pooContentType = Just "application/javascript"
893+ }
894+
895+ -- retrieve obj info to check
896+ oi <- headObject bucket object []
897+ let m = oiMetadata oi
898+
899+ step "Validate content-type"
900+ liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
901+
902+ step "upload object with content-encoding set to identity"
903+ fPutObject
904+ bucket
905+ object
906+ inputFile
907+ defaultPutObjectOptions
908+ { pooContentEncoding = Just "identity"
909+ }
910+
911+ oiCE <- headObject bucket object []
912+ let m' = oiMetadata oiCE
913+
914+ step "Validate content-encoding"
915+ liftIO $
916+ assertEqual
917+ "Content-Encoding did not match"
918+ (Just "identity")
919+ (H.lookup "Content-Encoding" m')
920+
921+ step "Cleanup actions"
922+
923+ removeObject bucket object
924+
925+putObjectContentLanguageTest :: TestTree
926+putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage tests" $
927+ \step bucket -> do
928+ step "fPutObject content language test"
929+ let object = "xxx-content-language"
930+ size1 = 100 :: Int64
931+
932+ step "create server object with content-language"
933+ inputFile <- mkRandFile size1
934+ fPutObject
935+ bucket
936+ object
937+ inputFile
938+ defaultPutObjectOptions
939+ { pooContentLanguage = Just "en-US"
940+ }
941+
942+ -- retrieve obj info to check
943+ oi <- headObject bucket object []
944+ let m = oiMetadata oi
945+
946+ step "Validate content-language"
947+ liftIO $
948+ assertEqual
949+ "content-language did not match"
950+ (Just "en-US")
951+ (H.lookup "Content-Language" m)
952+ step "Cleanup actions"
953+
954+ removeObject bucket object
955+
956+putObjectUserMetadataTest :: TestTree
957+putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
958+ \step bucket -> do
959+ step "putObject user-metadata test"
960+ let object = "object-with-metadata"
961+ size1 = 100 :: Int64
962+
963+ step "create server object with usermetdata"
964+ inputFile <- mkRandFile size1
965+
966+ fPutObject
967+ bucket
968+ object
969+ inputFile
970+ defaultPutObjectOptions
971+ { pooUserMetadata =
972+ [ ("x-Amz-meta-mykey1", "myval1"),
973+ ("mykey2", "myval2")
974+ ]
975+ }
976+
977+ step "Validate user-metadata"
978+ -- retrieve obj info to check
979+ oi <- headObject bucket object []
980+ let m = oiUserMetadata oi
981+ -- need to do a case-insensitive comparison
982+ sortedMeta =
983+ sort $
984+ map (bimap T.toLower T.toLower) $
985+ H.toList m
986+ ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
987+
988+ liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
989+
990+ step "Cleanup actions"
991+ removeObject bucket object
992+
993+getObjectTest :: TestTree
994+getObjectTest = funTestWithBucket "getObject test" $
995+ \step bucket -> do
996+ step "putObject with some metadata"
997+ let object = "object-with-metadata"
998+ size1 = 100 :: Int64
999+
1000+ inputFile <- mkRandFile size1
1001+ fPutObject
1002+ bucket
1003+ object
1004+ inputFile
1005+ defaultPutObjectOptions
1006+ { pooUserMetadata =
1007+ [ ("x-Amz-meta-mykey1", "myval1"),
1008+ ("mykey2", "myval2")
1009+ ]
1010+ }
1011+
1012+ step "get the object - check the metadata matches"
1013+ -- retrieve obj info to check
1014+ gor <- getObject bucket object defaultGetObjectOptions
1015+ let m = oiUserMetadata $ gorObjectInfo gor
1016+ -- need to do a case-insensitive comparison
1017+ sortedMeta =
1018+ sort $
1019+ map (bimap T.toLower T.toLower) $
1020+ H.toList m
1021+ ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
1022+
1023+ liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
1024+
1025+ step "get the object content"
1026+ getObjectHash <- hashSHA256FromSource $ gorObjectStream gor
1027+ inputHash <- hashSHA256FromSource $ C.sourceFile inputFile
1028+ liftIO $ (getObjectHash == inputHash) @? "Input file and output file mismatched!"
1029+
1030+ step "Cleanup actions"
1031+ removeObject bucket object
1032+
1033+putObjectStorageClassTest :: TestTree
1034+putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
1035+ \step bucket -> do
1036+ step "fPutObject storage class test"
1037+ let object = "xxx-storage-class-standard"
1038+ object' = "xxx-storage-class-reduced"
1039+ object'' = "xxx-storage-class-invalid"
1040+ size1 = 100 :: Int64
1041+ size0 = 0 :: Int64
1042+
1043+ step "create server objects with storageClass"
1044+ inputFile <- mkRandFile size1
1045+ inputFile' <- mkRandFile size1
1046+ inputFile'' <- mkRandFile size0
1047+
1048+ fPutObject
1049+ bucket
1050+ object
1051+ inputFile
1052+ defaultPutObjectOptions
1053+ { pooStorageClass = Just "STANDARD"
1054+ }
1055+
1056+ fPutObject
1057+ bucket
1058+ object'
1059+ inputFile'
1060+ defaultPutObjectOptions
1061+ { pooStorageClass = Just "REDUCED_REDUNDANCY"
1062+ }
1063+
1064+ removeObject bucket object
1065+
1066+ -- retrieve obj info to check
1067+ oi' <- headObject bucket object' []
1068+ let m' = oiMetadata oi'
1069+
1070+ step "Validate x-amz-storage-class rrs"
1071+ liftIO $
1072+ assertEqual
1073+ "storageClass did not match"
1074+ (Just "REDUCED_REDUNDANCY")
1075+ (H.lookup "X-Amz-Storage-Class" m')
1076+
1077+ fpE <-
1078+ try $
1079+ fPutObject
1080+ bucket
1081+ object''
1082+ inputFile''
1083+ defaultPutObjectOptions
1084+ { pooStorageClass = Just "INVALID_STORAGE_CLASS"
1085+ }
1086+ case fpE of
1087+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
1088+ _ -> return ()
1089+
1090+ step "Cleanup actions"
1091+
1092+ removeObject bucket object'
1093+
1094+copyObjectTests :: TestTree
1095+copyObjectTests = funTestWithBucket "copyObject related tests" $
1096+ \step bucket -> do
1097+ step "copyObjectSingle basic tests"
1098+ let object = "xxx"
1099+ objCopy = "xxxCopy"
1100+ size1 = 100 :: Int64
1101+
1102+ step "create server object to copy"
1103+ inputFile <- mkRandFile size1
1104+ fPutObject bucket object inputFile defaultPutObjectOptions
1105+
1106+ step "copy object"
1107+ let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
1108+ (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
1109+
1110+ -- retrieve obj info to check
1111+ oi <- headObject bucket objCopy []
1112+ let t = oiModTime oi
1113+ let e = oiETag oi
1114+ let s = oiSize oi
1115+
1116+ let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
1117+
1118+ liftIO $
1119+ (s == size1 && e == etag && isMTimeDiffOk)
1120+ @? "Copied object did not match expected."
1121+
1122+ step "cleanup actions"
1123+ removeObject bucket object
1124+ removeObject bucket objCopy
1125+
1126+ step "copyObjectPart basic tests"
1127+ let srcObj = "XXX"
1128+ copyObj = "XXXCopy"
1129+
1130+ step "Prepare"
1131+ let mb15 = 15 * 1024 * 1024
1132+ mb5 = 5 * 1024 * 1024
1133+ randFile <- mkRandFile mb15
1134+ fPutObject bucket srcObj randFile defaultPutObjectOptions
1135+
1136+ step "create new multipart upload"
1137+ uid <- newMultipartUpload bucket copyObj []
1138+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
1139+
1140+ step "put object parts 1-3"
1141+ let srcInfo' = defaultSourceInfo {srcBucket = bucket, srcObject = srcObj}
1142+ dstInfo' = defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1143+ parts <- forM [1 .. 3] $ \p -> do
1144+ (etag', _) <-
1145+ copyObjectPart
1146+ dstInfo'
1147+ srcInfo'
1148+ { srcRange = Just $ (,) ((p - 1) * mb5) ((p - 1) * mb5 + (mb5 - 1))
1149+ }
1150+ uid
1151+ (fromIntegral p)
1152+ []
1153+ return (fromIntegral p, etag')
1154+
1155+ step "complete multipart"
1156+ void $ completeMultipartUpload bucket copyObj uid parts
1157+
1158+ step "verify copied object size"
1159+ oi' <- headObject bucket copyObj []
1160+ let s' = oiSize oi'
1161+
1162+ liftIO $ (s' == mb15) @? "Size failed to match"
1163+
1164+ step "Cleanup actions"
1165+ removeObject bucket srcObj
1166+ removeObject bucket copyObj
1167+
1168+ step "copyObject basic tests"
1169+ let srcs = ["XXX", "XXXL"]
1170+ copyObjs = ["XXXCopy", "XXXLCopy"]
1171+ sizes = map (* (1024 * 1024)) [15, 65]
1172+
1173+ step "Prepare"
1174+ forM_ (zip srcs sizes) $ \(src, size) -> do
1175+ inputFile' <- mkRandFile size
1176+ fPutObject bucket src inputFile' defaultPutObjectOptions
1177+
1178+ step "make small and large object copy"
1179+ forM_ (zip copyObjs srcs) $ \(cp, src) ->
1180+ copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = cp} defaultSourceInfo {srcBucket = bucket, srcObject = src}
1181+
1182+ step "verify uploaded objects"
1183+ uploadedSizes <- fmap oiSize <$> forM copyObjs (\o -> headObject bucket o [])
1184+
1185+ liftIO $ (sizes == uploadedSizes) @? "Uploaded obj sizes failed to match"
1186+
1187+ forM_ (srcs ++ copyObjs) (removeObject bucket)
1188+
1189+ step "copyObject with offset test "
1190+ let src = "XXX"
1191+ size = 15 * 1024 * 1024
1192+
1193+ step "Prepare"
1194+ inputFile' <- mkRandFile size
1195+ fPutObject bucket src inputFile' defaultPutObjectOptions
1196+
1197+ step "copy last 10MiB of object"
1198+ copyObject
1199+ defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1200+ defaultSourceInfo
1201+ { srcBucket = bucket,
1202+ srcObject = src,
1203+ srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
1204+ }
1205+
1206+ step "verify uploaded object"
1207+ cSize <- oiSize <$> headObject bucket copyObj []
1208+
1209+ liftIO $ (cSize == 10 * 1024 * 1024) @? "Uploaded obj size mismatched!"
1210+
1211+ forM_ [src, copyObj] (removeObject bucket)
1212+
1213+getNPutSSECTest :: TestTree
1214+getNPutSSECTest =
1215+ funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
1216+ -- Skip this test if the server is not using TLS as encryption is
1217+ -- disabled anyway.
1218+ isTLSConn <- asks (connectIsSecure . mcConnInfo)
1219+ if isTLSConn
1220+ then do
1221+ step "Make an encryption key"
1222+ key <- case mkSSECKey $ BS.pack [0 .. 31] of
1223+ Nothing -> liftIO $ assertFailure "This should not happen"
1224+ Just k -> return k
1225+
1226+ let mb1 = 1024 * 1024
1227+ obj = "1"
1228+ step "Upload an object using the encryption key"
1229+ rFile <- mkRandFile mb1
1230+ let putOpts = defaultPutObjectOptions {pooSSE = Just $ SSEC key}
1231+ fPutObject bucket obj rFile putOpts
1232+
1233+ step "Stat object without key - should fail"
1234+ headRes <- try $ statObject bucket obj defaultGetObjectOptions
1235+ case headRes of
1236+ Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
1237+ Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
1238+ | NC.responseStatus rsp == HT.status400 -> return ()
1239+ | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1240+ Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1241+
1242+ step "Get file and check length"
1243+ dstFile <- mkRandFile 0
1244+ let getOpts = defaultGetObjectOptions {gooSSECKey = Just key}
1245+ fGetObject bucket obj dstFile getOpts
1246+
1247+ gotSize <- withNewHandle dstFile getFileSize
1248+ liftIO $
1249+ gotSize
1250+ == Right (Just mb1)
1251+ @? "Wrong file size of object when getting"
1252+
1253+ step "Cleanup"
1254+ deleteObject bucket obj
1255+ else step "Skipping encryption test as server is not using TLS"
1256+
1257+assumeRoleRequestTest :: TestTree
1258+assumeRoleRequestTest = testCaseSteps "Assume Role STS API" $ \step -> do
1259+ step "Load credentials"
1260+ (_, (ak, sk)) <- lookupTestEnv
1261+ val <- Env.lookupEnv "MINIO_LOCAL"
1262+ isSecure <- Env.lookupEnv "MINIO_SECURE"
1263+ let localMinioCred = Just $ CredentialValue ak sk mempty
1264+ playCreds =
1265+ case connectCreds minioPlayCI of
1266+ CredsStatic c -> Just c
1267+ _ -> Nothing
1268+ (cvMay, loc) =
1269+ case (val, isSecure) of
1270+ (Just _, Just _) -> (localMinioCred, "https://localhost:9000")
1271+ (Just _, Nothing) -> (localMinioCred, "http://localhost:9000")
1272+ (Nothing, _) -> (playCreds, "https://play.min.io:9000")
1273+ cv <- maybe (assertFailure "bad creds") return cvMay
1274+ let assumeRole =
1275+ STSAssumeRole cv $
1276+ defaultSTSAssumeRoleOptions
1277+ { saroLocation = Just "us-east-1",
1278+ saroEndpoint = Just loc
1279+ }
1280+ step "AssumeRole request"
1281+ res <- requestSTSCredential assumeRole
1282+ let v = credentialValueText $ fst res
1283+ print (v, snd res)
test/Network/Minio/API/Test.hs+91−25
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,36 +15,102 @@
15--
15--
16
16
17module Network.Minio.API.Test
17module Network.Minio.API.Test
18− ( bucketNameValidityTests
18+ ( bucketNameValidityTests,
19− , objectNameValidityTests
19+ objectNameValidityTests,
20+ parseServerInfoJSONTest,
21+ parseHealStatusTest,
22+ parseHealStartRespTest,
23+ )
20− ) where
24+where
21
25
26+import Data.Aeson (eitherDecode)
27+import Network.Minio.API
28+import Network.Minio.AdminAPI
22import Test.Tasty
29import Test.Tasty
23import Test.Tasty.HUnit
30import Test.Tasty.HUnit
24
31
25−import Lib.Prelude
32+assertBool' :: Bool -> Assertion
26−
27−import Network.Minio.API
28−
29assertBool' = assertBool "Test failed!"
33assertBool' = assertBool "Test failed!"
30
34
31bucketNameValidityTests :: TestTree
35bucketNameValidityTests :: TestTree
32−bucketNameValidityTests = testGroup "Bucket Name Validity Tests"
36+bucketNameValidityTests =
37+ testGroup
38+ "Bucket Name Validity Tests"
33− [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName ""
39+ [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName "",
34− , testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab"
40+ testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab",
35− , testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
41+ testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
36− , testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD"
42+ testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD",
37− , testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2"
43+ testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2",
38− , testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-"
44+ testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-",
39− , testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg"
45+ testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg",
40− , testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1"
46+ testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1",
41− , testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea"
47+ testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea",
42− , testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d"
48+ testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d",
43− , testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
49+ testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
44− ]
50+ ]
45
51
46objectNameValidityTests :: TestTree
52objectNameValidityTests :: TestTree
47−objectNameValidityTests = testGroup "Object Name Validity Tests"
53+objectNameValidityTests =
54+ testGroup
55+ "Object Name Validity Tests"
48− [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""
56+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName "",
49− , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
57+ testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
50− ]
58+ ]
59+
60+parseServerInfoJSONTest :: TestTree
61+parseServerInfoJSONTest =
62+ testGroup "Parse MinIO Admin API ServerInfo JSON test" $
63+ map
64+ ( \(tName, tDesc, tfn, tVal) ->
65+ testCase tName $
66+ assertBool tDesc $
67+ tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
68+ )
69+ testCases
70+ where
71+ testCases =
72+ [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON),
73+ ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON),
74+ ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
75+ ]
76+ fsJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":20530,\"Backend\":{\"Type\":1,\"OnlineDisks\":0,\"OfflineDisks\":0,\"StandardSCData\":0,\"StandardSCParity\":0,\"RRSCData\":0,\"RRSCParity\":0,\"Sets\":null}},\"network\":{\"transferred\":808,\"received\":1160},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":1,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":5992503019270,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
77+ erasureJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":2,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
78+ invalidJSON = "[{\"error\":\"\",\"addr\":\"192.168.1.218:9000\",\"data\":{\"storage\":{\"Used\":83084,\"Backend\":{\"Type\":42,\"OnlineDisks\":4,\"OfflineDisks\":0,\"StandardSCData\":2,\"StandardSCParity\":2,\"RRSCData\":2,\"RRSCParity\":2,\"Sets\":[[{\"uuid\":\"16ec6f2c-9197-4787-904a-36bb2c2683f8\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"4052e086-ef99-4aa5-ae2b-8e27559432f6\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"d0639950-ddd3-45b0-93ca-fd86f5d79f72\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"30ec68c0-37e1-4592-82c1-26b143c0ac10\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]]}},\"network\":{\"transferred\":404,\"received\":0},\"http\":{\"totalHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successHEADs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successGETs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPUTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successPOSTs\":{\"count\":0,\"avgDuration\":\"0s\"},\"totalDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"},\"successDELETEs\":{\"count\":0,\"avgDuration\":\"0s\"}},\"server\":{\"uptime\":2738903073,\"version\":\"DEVELOPMENT.GOGET\",\"commitID\":\"DEVELOPMENT.GOGET\",\"region\":\"\",\"sqsARN\":[]}}}]"
79+
80+parseHealStatusTest :: TestTree
81+parseHealStatusTest =
82+ testGroup "Parse MinIO Admin API HealStatus JSON test" $
83+ map
84+ ( \(tName, tDesc, tfn, tVal) ->
85+ testCase tName $
86+ assertBool tDesc $
87+ tfn (eitherDecode tVal :: Either [Char] HealStatus)
88+ )
89+ testCases
90+ where
91+ testCases =
92+ [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON'),
93+ ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON'),
94+ ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
95+ ]
96+ erasureJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
97+ invalidJSON' = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"metadata\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]"
98+ invalidItemType = "{\"Summary\":\"finished\",\"StartTime\":\"2018-06-05T08:09:47.644465513Z\",\"NumDisks\":4,\"Settings\":{\"recursive\":false,\"dryRun\":false},\"Items\":[{\"resultId\":1,\"type\":\"hello\",\"bucket\":\"\",\"object\":\"\",\"detail\":\"disk-format\",\"diskCount\":4,\"setCount\":1,\"before\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"after\":{\"drives\":[{\"uuid\":\"c3487166-b8a4-481a-b1e7-fb9b249e2500\",\"endpoint\":\"/tmp/1\",\"state\":\"ok\"},{\"uuid\":\"55a6e787-184f-4e4c-bf09-03dcada658a9\",\"endpoint\":\"/tmp/2\",\"state\":\"ok\"},{\"uuid\":\"f035d8c3-fca1-4407-b89c-38c2bcf4a641\",\"endpoint\":\"/tmp/3\",\"state\":\"ok\"},{\"uuid\":\"4f8b79d3-db90-4c1d-87c2-35a28b0d9a13\",\"endpoint\":\"/tmp/4\",\"state\":\"ok\"}]},\"objectSize\":0}]}"
99+
100+parseHealStartRespTest :: TestTree
101+parseHealStartRespTest =
102+ testGroup "Parse MinIO Admin API HealStartResp JSON test" $
103+ map
104+ ( \(tName, tDesc, tfn, tVal) ->
105+ testCase tName $
106+ assertBool tDesc $
107+ tfn (eitherDecode tVal :: Either [Char] HealStartResp)
108+ )
109+ testCases
110+ where
111+ testCases =
112+ [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON),
113+ ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
114+ ]
115+ hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
116+ missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
test/Network/Minio/JsonParser/Test.hsnew file+63
@@ -0,0 +1,63 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.JsonParser.Test
18+ ( jsonParserTests,
19+ )
20+where
21+
22+import Lib.Prelude
23+import Network.Minio.Errors
24+import Network.Minio.JsonParser
25+import Test.Tasty
26+import Test.Tasty.HUnit
27+import UnliftIO (MonadUnliftIO)
28+
29+jsonParserTests :: TestTree
30+jsonParserTests =
31+ testGroup
32+ "JSON Parser Tests"
33+ [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34+ ]
35+
36+tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
37+tryValidationErr = try
38+
39+assertValidationErr :: MErrV -> Assertion
40+assertValidationErr e = assertFailure $ "Failed due to validation error => " ++ show e
41+
42+testParseErrResponseJSON :: Assertion
43+testParseErrResponseJSON = do
44+ -- 1. Test parsing of an invalid error json.
45+ parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
46+ when (isRight parseResE) $
47+ assertFailure $
48+ "Parsing should have failed => " ++ show parseResE
49+
50+ forM_ cases $ \(jsondata, sErr) -> do
51+ parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
52+ either assertValidationErr (@?= sErr) parseErr
53+ where
54+ cases =
55+ [ -- 2. Test parsing of a valid error json.
56+ ( "{\"Code\":\"InvalidAccessKeyId\",\"Message\":\"The access key ID you provided does not exist in our records.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
57+ ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58+ ),
59+ -- 3. Test parsing of a valid, empty Resource.
60+ ( "{\"Code\":\"SignatureDoesNotMatch\",\"Message\":\"The request signature we calculated does not match the signature you provided. Check your key and signing method.\",\"Key\":\"\",\"BucketName\":\"\",\"Resource\":\"/minio/admin/v1/info\",\"RequestId\":\"3L137\",\"HostId\":\"3L137\"}",
61+ ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
62+ )
63+ ]
test/Network/Minio/TestHelpers.hsnew file+32
@@ -0,0 +1,32 @@
1+--
2+-- MinIO Haskell SDK, (C) 2018 MinIO, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module Network.Minio.TestHelpers
18+ ( runTestNS,
19+ )
20+where
21+
22+import Network.Minio.Data
23+
24+newtype TestNS = TestNS {testNamespace :: Text}
25+
26+instance HasSvcNamespace TestNS where
27+ getSvcNamespace = testNamespace
28+
29+runTestNS :: ReaderT TestNS m a -> m a
30+runTestNS =
31+ flip runReaderT $
32+ TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs+14−16
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -15,33 +15,31 @@
15--
15--
16
16
17module Network.Minio.Utils.Test
17module Network.Minio.Utils.Test
18− (
19− limitedMapConcurrentlyTests
18+ ( limitedMapConcurrentlyTests,
19+ )
20− ) where
20+where
21
21
22+import Network.Minio.Utils
22import Test.Tasty
23import Test.Tasty
23import Test.Tasty.HUnit
24import Test.Tasty.HUnit
24
25
25−import Lib.Prelude
26−
27−import Network.Minio.Utils
28−
29limitedMapConcurrentlyTests :: TestTree
26limitedMapConcurrentlyTests :: TestTree
30−limitedMapConcurrentlyTests = testGroup "limitedMapConcurrently Tests"
27+limitedMapConcurrentlyTests =
28+ testGroup
29+ "limitedMapConcurrently Tests"
31− [ testCase "Test with various thread counts" testLMC
30+ [ testCase "Test with various thread counts" testLMC
32− ]
31+ ]
33
32
34testLMC :: Assertion
33testLMC :: Assertion
35testLMC = do
34testLMC = do
36 let maxNum = 50
35 let maxNum = 50
37 -- test with thread count of 1 to 2*maxNum
36 -- test with thread count of 1 to 2*maxNum
38− forM_ [1..(2*maxNum)] $ \threads -> do
37+ forM_ [1 .. (2 * maxNum)] $ \threads -> do
39− res <- limitedMapConcurrently threads compute [1..maxNum]
38+ res <- limitedMapConcurrently threads compute [1 .. maxNum]
40 sum res @?= overallResultCheck maxNum
39 sum res @?= overallResultCheck maxNum
41 where
40 where
42 -- simple function to run in each thread
41 -- simple function to run in each thread
43 compute :: Int -> IO Int
42 compute :: Int -> IO Int
44− compute n = return $ sum [1..n]
43+ compute n = return $ sum [1 .. n]
45−
46 -- function to check overall result
44 -- function to check overall result
47− overallResultCheck n = sum $ map (\t -> (t * (t+1)) `div` 2) [1..n]
45+ overallResultCheck n = sum $ map (\t -> (t * (t + 1)) `div` 2) [1 .. n]
test/Network/Minio/XmlGenerator/Test.hs+148−61
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,83 +13,170 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16+{-# LANGUAGE QuasiQuotes #-}
16
17
17module Network.Minio.XmlGenerator.Test
18module Network.Minio.XmlGenerator.Test
18− ( xmlGeneratorTests
19+ ( xmlGeneratorTests,
20+ )
19− ) where
21+where
20
22
21−import Test.Tasty
23+import qualified Data.ByteString.Lazy as LBS
22−import Test.Tasty.HUnit
23−
24−import Lib.Prelude
24+import Lib.Prelude
25−
25+import Network.Minio.Data
26−import Data.Default (def)
27−
28−import Network.Minio.Data
26+import Network.Minio.TestHelpers
29−import Network.Minio.XmlGenerator
27+import Network.Minio.XmlGenerator
30−import Network.Minio.XmlParser (parseNotification)
28+import Network.Minio.XmlParser (parseNotification)
29+import Test.Tasty
30+import Test.Tasty.HUnit
31+import Text.RawString.QQ (r)
32+import Text.XML (def, parseLBS)
31
33
32xmlGeneratorTests :: TestTree
34xmlGeneratorTests :: TestTree
33−xmlGeneratorTests = testGroup "XML Generator Tests"
35+xmlGeneratorTests =
36+ testGroup
37+ "XML Generator Tests"
34− [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig
38+ [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig,
35− , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest
39+ testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest,
36− , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest
40+ testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest,
41+ testCase "Test mkSelectRequest" testMkSelectRequest
37− ]
42+ ]
38
43
39testMkCreateBucketConfig :: Assertion
44testMkCreateBucketConfig :: Assertion
40testMkCreateBucketConfig = do
45testMkCreateBucketConfig = do
46+ let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
41 assertEqual "CreateBucketConfiguration xml should match: " expected $
47 assertEqual "CreateBucketConfiguration xml should match: " expected $
42− mkCreateBucketConfig "EU"
48+ mkCreateBucketConfig ns "EU"
43 where
49 where
50+ expected =
44− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
51+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
45− \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
52+ \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
46− \<LocationConstraint>EU</LocationConstraint>\
53+ \<LocationConstraint>EU</LocationConstraint>\
47− \</CreateBucketConfiguration>"
54+ \</CreateBucketConfiguration>"
48
55
49testMkCompleteMultipartUploadRequest :: Assertion
56testMkCompleteMultipartUploadRequest :: Assertion
50testMkCompleteMultipartUploadRequest =
57testMkCompleteMultipartUploadRequest =
51 assertEqual "completeMultipartUpload xml should match: " expected $
58 assertEqual "completeMultipartUpload xml should match: " expected $
52− mkCompleteMultipartUploadRequest [(1, "abc")]
59+ mkCompleteMultipartUploadRequest [(1, "abc")]
53 where
60 where
61+ expected =
54− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
62+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
55− \<CompleteMultipartUpload>\
63+ \<CompleteMultipartUpload>\
56− \<Part>\
64+ \<Part>\
57− \<PartNumber>1</PartNumber><ETag>abc</ETag>\
65+ \<PartNumber>1</PartNumber><ETag>abc</ETag>\
58− \</Part>\
66+ \</Part>\
59− \</CompleteMultipartUpload>"
67+ \</CompleteMultipartUpload>"
60
68
61testMkPutNotificationRequest :: Assertion
69testMkPutNotificationRequest :: Assertion
62testMkPutNotificationRequest =
70testMkPutNotificationRequest =
63 forM_ cases $ \val -> do
71 forM_ cases $ \val -> do
72+ let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
64− let result = toS $ mkPutNotificationRequest val
73+ result = fromStrictBS $ mkPutNotificationRequest ns val
65− ntf <- runExceptT $ parseNotification result
74+ ntf <- runExceptT $ runTestNS $ parseNotification result
75+ either
66− either (\_ -> assertFailure "XML Parse Error!")
76+ (\_ -> assertFailure "XML Parse Error!")
67− (@?= val) ntf
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
68 where
124 where
125+ assertFn (a, b) =
126+ let generatedReqDoc = parseLBS def $ LBS.fromStrict $ mkSelectRequest a
127+ expectedReqDoc = parseLBS def $ LBS.fromStrict b
128+ in case (generatedReqDoc, expectedReqDoc) of
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
69− cases = [ Notification []
132+ cases =
70− [ NotificationConfig
133+ [ ( SelectRequest
71− "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
134+ "Select * from S3Object"
72− "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
135+ SQL
73− [ReducedRedundancyLostObject, ObjectCreated] def
136+ ( InputSerialization
74− ]
137+ (Just CompressionTypeGzip)
75− []
138+ ( InputFormatCSV $
76− , Notification
139+ fileHeaderInfo FileHeaderIgnore
77− [ NotificationConfig
140+ <> recordDelimiter "\n"
78− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
141+ <> fieldDelimiter ","
79− [ObjectCreatedPut]
142+ <> quoteCharacter "\""
80− (Filter $ FilterKey $ FilterRules
143+ <> quoteEscapeCharacter "\""
81− [ FilterRule "prefix" "images/"
144+ )
82− , FilterRule "suffix" ".jpg"])
145+ )
83− , NotificationConfig
146+ ( OutputSerializationCSV $
84− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
147+ quoteFields QuoteFieldsAsNeeded
85− [ObjectCreated] def
148+ <> recordDelimiter "\n"
86− ]
149+ <> fieldDelimiter ","
87− [ NotificationConfig
150+ <> quoteCharacter "\""
88− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
151+ <> quoteEscapeCharacter "\""
89− [ReducedRedundancyLostObject] def
152+ )
90− ]
153+ (Just False),
91− [ NotificationConfig
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>
92− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
155+</RecordDelimiter></CSV></InputSerialization><OutputSerialization><CSV><FieldDelimiter>,</FieldDelimiter><QuoteCharacter>"</QuoteCharacter><QuoteEscapeCharacter>"</QuoteEscapeCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
93− [ObjectCreated] def
156+</RecordDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
94− ]
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+ )
95− ]
182+ ]
test/Network/Minio/XmlParser/Test.hs+319−274
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -13,46 +13,49 @@
13-- See the License for the specific language governing permissions and
13-- See the License for the specific language governing permissions and
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16+{-# LANGUAGE QuasiQuotes #-}
16
17
17module Network.Minio.XmlParser.Test
18module Network.Minio.XmlParser.Test
18− (
19− xmlParserTests
19+ ( xmlParserTests,
20+ )
20− ) where
21+where
21
22
22−import qualified Control.Monad.Catch as MC
23+import qualified Data.HashMap.Strict as H
23−import Data.Time (fromGregorian)
24+import Data.Time (fromGregorian)
24−import Test.Tasty
25−import Test.Tasty.HUnit
26−
27−import Lib.Prelude
25+import Lib.Prelude
28−
29−import Data.Default (def)
30−
31−import Network.Minio.Data
26+import Network.Minio.Data
32−import Network.Minio.Errors
27+import Network.Minio.Errors
28+import Network.Minio.TestHelpers
33−import Network.Minio.XmlParser
29+import Network.Minio.XmlParser
30+import Test.Tasty
31+import Test.Tasty.HUnit
32+import Text.RawString.QQ (r)
33+import UnliftIO (MonadUnliftIO)
34
34
35xmlParserTests :: TestTree
35xmlParserTests :: TestTree
36−xmlParserTests = testGroup "XML Parser Tests"
36+xmlParserTests =
37+ testGroup
38+ "XML Parser Tests"
37− [ testCase "Test parseLocation" testParseLocation
39+ [ testCase "Test parseLocation" testParseLocation,
38− , testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload
40+ testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload,
39− , testCase "Test parseListObjectsResponse" testParseListObjectsResult
41+ testCase "Test parseListObjectsResponse" testParseListObjectsResult,
40− , testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result
42+ testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result,
41− , testCase "Test parseListUploadsresponse" testParseListIncompleteUploads
43+ testCase "Test parseListUploadsresponse" testParseListIncompleteUploads,
42− , testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse
44+ testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse,
43− , testCase "Test parseListPartsResponse" testParseListPartsResponse
45+ testCase "Test parseListPartsResponse" testParseListPartsResponse,
44− , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse
46+ testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse,
45− , testCase "Test parseNotification" testParseNotification
47+ testCase "Test parseNotification" testParseNotification,
48+ testCase "Test parseSelectProgress" testParseSelectProgress
46− ]
49+ ]
47
50
48−tryValidationErr :: (MC.MonadCatch m) => m a -> m (Either MErrV a)
51+tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
49−tryValidationErr act = MC.try act
52+tryValidationErr = try
50
53
51assertValidtionErr :: MErrV -> Assertion
54assertValidtionErr :: MErrV -> Assertion
52assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
55assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
53
56
54eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
57eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
55−eitherValidationErr (Left e) _ = assertValidtionErr e
58+eitherValidationErr (Left e) _ = assertValidtionErr e
56eitherValidationErr (Right a) f = f a
59eitherValidationErr (Right a) f = f a
57
60
58testParseLocation :: Assertion
61testParseLocation :: Assertion
@@ -60,299 +63,341 @@ testParseLocation = do
60 -- 1. Test parsing of an invalid location constraint xml.
63 -- 1. Test parsing of an invalid location constraint xml.
61 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
64 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
62 when (isRight parseResE) $
65 when (isRight parseResE) $
66+ assertFailure $
63− assertFailure $ "Parsing should have failed => " ++ show parseResE
67+ "Parsing should have failed => " ++ show parseResE
64
68
65 forM_ cases $ \(xmldata, expectedLocation) -> do
69 forM_ cases $ \(xmldata, expectedLocation) -> do
66 parseLocE <- tryValidationErr $ parseLocation xmldata
70 parseLocE <- tryValidationErr $ parseLocation xmldata
67 either assertValidtionErr (@?= expectedLocation) parseLocE
71 either assertValidtionErr (@?= expectedLocation) parseLocE
68 where
72 where
69− cases = [
73+ cases =
70− -- 2. Test parsing of a valid location xml.
74+ [ -- 2. Test parsing of a valid location xml.
71− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
75+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
72− \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
76+ \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
73− "EU"
77+ "EU"
74− )
78+ ),
75− ,
76− -- 3. Test parsing of a valid, empty location xml.
79+ -- 3. Test parsing of a valid, empty location xml.
77− ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
80+ ( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
78− "us-east-1"
81+ "us-east-1"
79− )
82+ )
80 ]
83 ]
81
84
82−
83testParseNewMultipartUpload :: Assertion
85testParseNewMultipartUpload :: Assertion
84testParseNewMultipartUpload = do
86testParseNewMultipartUpload = do
85 forM_ cases $ \(xmldata, expectedUploadId) -> do
87 forM_ cases $ \(xmldata, expectedUploadId) -> do
86− parsedUploadIdE <- tryValidationErr $ parseNewMultipartUpload xmldata
88+ parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
87 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
89 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
88 where
90 where
89− cases = [
91+ cases =
90− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
92+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
91− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
93+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
92− \ <Bucket>example-bucket</Bucket>\
94+ \ <Bucket>example-bucket</Bucket>\
93− \ <Key>example-object</Key>\
95+ \ <Key>example-object</Key>\
94− \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
96+ \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
95− \</InitiateMultipartUploadResult>",
97+ \</InitiateMultipartUploadResult>",
96− "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
98+ "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
97− ),
99+ ),
98− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
100+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
99− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
101+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
100− \ <Bucket>example-bucket</Bucket>\
102+ \ <Bucket>example-bucket</Bucket>\
101− \ <Key>example-object</Key>\
103+ \ <Key>example-object</Key>\
102− \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
104+ \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
103− \</InitiateMultipartUploadResult>",
105+ \</InitiateMultipartUploadResult>",
104− "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
106+ "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
105− )
107+ )
106 ]
108 ]
107
109
108testParseListObjectsResult :: Assertion
110testParseListObjectsResult :: Assertion
109testParseListObjectsResult = do
111testParseListObjectsResult = do
110− let
112+ let xmldata =
111− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
113+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
112− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
114+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
113− \<Name>bucket</Name>\
115+ \<Name>bucket</Name>\
114− \<Prefix/>\
116+ \<Prefix/>\
115− \<NextContinuationToken>opaque</NextContinuationToken>\
117+ \<NextContinuationToken>opaque</NextContinuationToken>\
116− \<KeyCount>1000</KeyCount>\
118+ \<KeyCount>1000</KeyCount>\
117− \<MaxKeys>1000</MaxKeys>\
119+ \<MaxKeys>1000</MaxKeys>\
118− \<IsTruncated>true</IsTruncated>\
120+ \<IsTruncated>true</IsTruncated>\
119− \<Contents>\
121+ \<Contents>\
120− \<Key>my-image.jpg</Key>\
122+ \<Key>my-image.jpg</Key>\
121− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
123+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
122− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
124+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
123− \<Size>434234</Size>\
125+ \<Size>434234</Size>\
124− \<StorageClass>STANDARD</StorageClass>\
126+ \<StorageClass>STANDARD</StorageClass>\
125− \</Contents>\
127+ \</Contents>\
126− \</ListBucketResult>"
128+ \</ListBucketResult>"
127−
128− expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
129+ expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
129− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234
130+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
130− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
131+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
131
132
132− parsedListObjectsResult <- tryValidationErr $ parseListObjectsResponse xmldata
133+ parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
133 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
134 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
134
135
135testParseListObjectsV1Result :: Assertion
136testParseListObjectsV1Result :: Assertion
136testParseListObjectsV1Result = do
137testParseListObjectsV1Result = do
137− let
138+ let xmldata =
138− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
139+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
139− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
140+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
140− \<Name>bucket</Name>\
141+ \<Name>bucket</Name>\
141− \<Prefix/>\
142+ \<Prefix/>\
142− \<NextMarker>my-image1.jpg</NextMarker>\
143+ \<NextMarker>my-image1.jpg</NextMarker>\
143− \<KeyCount>1000</KeyCount>\
144+ \<KeyCount>1000</KeyCount>\
144− \<MaxKeys>1000</MaxKeys>\
145+ \<MaxKeys>1000</MaxKeys>\
145− \<IsTruncated>true</IsTruncated>\
146+ \<IsTruncated>true</IsTruncated>\
146− \<Contents>\
147+ \<Contents>\
147− \<Key>my-image.jpg</Key>\
148+ \<Key>my-image.jpg</Key>\
148− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
149+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
149− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
150+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
150− \<Size>434234</Size>\
151+ \<Size>434234</Size>\
151− \<StorageClass>STANDARD</StorageClass>\
152+ \<StorageClass>STANDARD</StorageClass>\
152− \</Contents>\
153+ \</Contents>\
153− \</ListBucketResult>"
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
154
158
155− expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
156− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234
157− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
158−
159− parsedListObjectsV1Result <- tryValidationErr $ parseListObjectsV1Response xmldata
159+ parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
160 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
160 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
161
161
162testParseListIncompleteUploads :: Assertion
162testParseListIncompleteUploads :: Assertion
163testParseListIncompleteUploads = do
163testParseListIncompleteUploads = do
164− let
164+ let xmldata =
165− xmldata = "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
165+ "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
166− \<Bucket>example-bucket</Bucket>\
166+ \<Bucket>example-bucket</Bucket>\
167− \<KeyMarker/>\
167+ \<KeyMarker/>\
168− \<UploadIdMarker/>\
168+ \<UploadIdMarker/>\
169− \<NextKeyMarker>sample.jpg</NextKeyMarker>\
169+ \<NextKeyMarker>sample.jpg</NextKeyMarker>\
170− \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
170+ \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
171− \<Delimiter>/</Delimiter>\
171+ \<Delimiter>/</Delimiter>\
172− \<Prefix/>\
172+ \<Prefix/>\
173− \<MaxUploads>1000</MaxUploads>\
173+ \<MaxUploads>1000</MaxUploads>\
174− \<IsTruncated>false</IsTruncated>\
174+ \<IsTruncated>false</IsTruncated>\
175− \<Upload>\
175+ \<Upload>\
176− \<Key>sample.jpg</Key>\
176+ \<Key>sample.jpg</Key>\
177− \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
177+ \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
178− \<Initiator>\
178+ \<Initiator>\
179− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
179+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
180− \<DisplayName>s3-nickname</DisplayName>\
180+ \<DisplayName>s3-nickname</DisplayName>\
181− \</Initiator>\
181+ \</Initiator>\
182− \<Owner>\
182+ \<Owner>\
183− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
183+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
184− \<DisplayName>s3-nickname</DisplayName>\
184+ \<DisplayName>s3-nickname</DisplayName>\
185− \</Owner>\
185+ \</Owner>\
186− \<StorageClass>STANDARD</StorageClass>\
186+ \<StorageClass>STANDARD</StorageClass>\
187− \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
187+ \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
188− \</Upload>\
188+ \</Upload>\
189− \<CommonPrefixes>\
189+ \<CommonPrefixes>\
190− \<Prefix>photos/</Prefix>\
190+ \<Prefix>photos/</Prefix>\
191− \</CommonPrefixes>\
191+ \</CommonPrefixes>\
192− \<CommonPrefixes>\
192+ \<CommonPrefixes>\
193− \<Prefix>videos/</Prefix>\
193+ \<Prefix>videos/</Prefix>\
194− \</CommonPrefixes>\
194+ \</CommonPrefixes>\
195− \</ListMultipartUploadsResult>"
195+ \</ListMultipartUploadsResult>"
196− expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
196+ expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
197− uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
197+ uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
198− initTime = UTCTime (fromGregorian 2010 11 26) 69857
198+ initTime = UTCTime (fromGregorian 2010 11 26) 69857
199− prefixes = ["photos/", "videos/"]
199+ prefixes = ["photos/", "videos/"]
200
200
201− parsedListUploadsResult <- tryValidationErr $ parseListUploadsResponse xmldata
201+ parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
202 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
202 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
203
203
204−
205testParseCompleteMultipartUploadResponse :: Assertion
204testParseCompleteMultipartUploadResponse :: Assertion
206testParseCompleteMultipartUploadResponse = do
205testParseCompleteMultipartUploadResponse = do
207− let
206+ let xmldata =
208− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
207+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
209−\<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
208+ \<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
210− \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
209+ \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
211− \<Bucket>Example-Bucket</Bucket>\
210+ \<Bucket>Example-Bucket</Bucket>\
212− \<Key>Example-Object</Key>\
211+ \<Key>Example-Object</Key>\
213− \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
212+ \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
214−\</CompleteMultipartUploadResult>"
213+ \</CompleteMultipartUploadResult>"
215− expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
214+ expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
216
215
217− parsedETagE <- runExceptT $ parseCompleteMultipartUploadResponse xmldata
216+ parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
218 eitherValidationErr parsedETagE (@?= expectedETag)
217 eitherValidationErr parsedETagE (@?= expectedETag)
219
218
220testParseListPartsResponse :: Assertion
219testParseListPartsResponse :: Assertion
221testParseListPartsResponse = do
220testParseListPartsResponse = do
222− let
221+ let xmldata =
223− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
222+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
224−\<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
223+ \<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
225− \<Bucket>example-bucket</Bucket>\
224+ \<Bucket>example-bucket</Bucket>\
226− \<Key>example-object</Key>\
225+ \<Key>example-object</Key>\
227− \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
226+ \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
228− \<Initiator>\
227+ \<Initiator>\
229− \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
228+ \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
230− \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
229+ \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
231− \</Initiator>\
230+ \</Initiator>\
232− \<Owner>\
231+ \<Owner>\
233− \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
232+ \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
234− \<DisplayName>someName</DisplayName>\
233+ \<DisplayName>someName</DisplayName>\
235− \</Owner>\
234+ \</Owner>\
236− \<StorageClass>STANDARD</StorageClass>\
235+ \<StorageClass>STANDARD</StorageClass>\
237− \<PartNumberMarker>1</PartNumberMarker>\
236+ \<PartNumberMarker>1</PartNumberMarker>\
238− \<NextPartNumberMarker>3</NextPartNumberMarker>\
237+ \<NextPartNumberMarker>3</NextPartNumberMarker>\
239− \<MaxParts>2</MaxParts>\
238+ \<MaxParts>2</MaxParts>\
240− \<IsTruncated>true</IsTruncated>\
239+ \<IsTruncated>true</IsTruncated>\
241− \<Part>\
240+ \<Part>\
242− \<PartNumber>2</PartNumber>\
241+ \<PartNumber>2</PartNumber>\
243− \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
242+ \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
244− \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
243+ \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
245− \<Size>10485760</Size>\
244+ \<Size>10485760</Size>\
246− \</Part>\
245+ \</Part>\
247− \<Part>\
246+ \<Part>\
248− \<PartNumber>3</PartNumber>\
247+ \<PartNumber>3</PartNumber>\
249− \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
248+ \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
250− \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
249+ \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
251− \<Size>10485760</Size>\
250+ \<Size>10485760</Size>\
252− \</Part>\
251+ \</Part>\
253−\</ListPartsResult>"
252+ \</ListPartsResult>"
254−
255− expectedListResult = ListPartsResult True (Just 3) [part1, part2]
253+ expectedListResult = ListPartsResult True (Just 3) [part1, part2]
256− part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
254+ part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
257− modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
255+ modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
258− part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
256+ part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
259− modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
257+ modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
260
258
261− parsedListPartsResult <- runExceptT $ parseListPartsResponse xmldata
259+ parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
262 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
260 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
263
261
264testParseCopyObjectResponse :: Assertion
262testParseCopyObjectResponse :: Assertion
265testParseCopyObjectResponse = do
263testParseCopyObjectResponse = do
266− let
264+ let cases =
267− cases = [ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
265+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
268−\<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
266+ \<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
269− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
267+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
270− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
268+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
271−\</CopyObjectResult>",
269+ \</CopyObjectResult>",
272− ("\"9b2cf535f27731c974343645a3985328\"",
270+ ( "\"9b2cf535f27731c974343645a3985328\"",
273− UTCTime (fromGregorian 2009 10 28) 81120))
271+ UTCTime (fromGregorian 2009 10 28) 81120
272+ )
273+ ),
274− , ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
274+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
275−\<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
275+ \<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
276− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
276+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
277− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
277+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
278−\</CopyPartResult>",
278+ \</CopyPartResult>",
279− ("\"9b2cf535f27731c974343645a3985328\"",
279+ ( "\"9b2cf535f27731c974343645a3985328\"",
280− UTCTime (fromGregorian 2009 10 28) 81120))]
280+ UTCTime (fromGregorian 2009 10 28) 81120
281+ )
282+ )
283+ ]
281
284
282 forM_ cases $ \(xmldata, (etag, modTime)) -> do
285 forM_ cases $ \(xmldata, (etag, modTime)) -> do
283− parseResult <- runExceptT $ parseCopyObjectResponse xmldata
286+ parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
284 eitherValidationErr parseResult (@?= (etag, modTime))
287 eitherValidationErr parseResult (@?= (etag, modTime))
285
288
286testParseNotification :: Assertion
289testParseNotification :: Assertion
287testParseNotification = do
290testParseNotification = do
288− let
291+ let cases =
289− cases = [ ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
292+ [ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
290−\ <TopicConfiguration>\
293+ \ <TopicConfiguration>\
291−\ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
294+ \ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
292−\ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
295+ \ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
293−\ <Event>s3:ReducedRedundancyLostObject</Event>\
296+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
294−\ <Event>s3:ObjectCreated:*</Event>\
297+ \ <Event>s3:ObjectCreated:*</Event>\
295−\ </TopicConfiguration>\
298+ \ </TopicConfiguration>\
296−\</NotificationConfiguration>",
299+ \</NotificationConfiguration>",
297− Notification []
300+ Notification
301+ []
298− [ NotificationConfig
302+ [ NotificationConfig
299 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
303 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
300 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
304 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
301− [ReducedRedundancyLostObject, ObjectCreated] def
305+ [ReducedRedundancyLostObject, ObjectCreated]
306+ defaultFilter
302− ]
307+ ]
303− [])
308+ []
309+ ),
304− , ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
310+ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
305−\ <CloudFunctionConfiguration>\
311+ \ <CloudFunctionConfiguration>\
306−\ <Id>ObjectCreatedEvents</Id>\
312+ \ <Id>ObjectCreatedEvents</Id>\
307−\ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
313+ \ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
308−\ <Event>s3:ObjectCreated:*</Event>\
314+ \ <Event>s3:ObjectCreated:*</Event>\
309−\ </CloudFunctionConfiguration>\
315+ \ </CloudFunctionConfiguration>\
310−\ <QueueConfiguration>\
316+ \ <QueueConfiguration>\
311−\ <Id>1</Id>\
317+ \ <Id>1</Id>\
312−\ <Filter>\
318+ \ <Filter>\
313−\ <S3Key>\
319+ \ <S3Key>\
314−\ <FilterRule>\
320+ \ <FilterRule>\
315−\ <Name>prefix</Name>\
321+ \ <Name>prefix</Name>\
316−\ <Value>images/</Value>\
322+ \ <Value>images/</Value>\
317−\ </FilterRule>\
323+ \ </FilterRule>\
318−\ <FilterRule>\
324+ \ <FilterRule>\
319−\ <Name>suffix</Name>\
325+ \ <Name>suffix</Name>\
320−\ <Value>.jpg</Value>\
326+ \ <Value>.jpg</Value>\
321−\ </FilterRule>\
327+ \ </FilterRule>\
322−\ </S3Key>\
328+ \ </S3Key>\
323−\ </Filter>\
329+ \ </Filter>\
324−\ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
330+ \ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
325−\ <Event>s3:ObjectCreated:Put</Event>\
331+ \ <Event>s3:ObjectCreated:Put</Event>\
326−\ </QueueConfiguration>\
332+ \ </QueueConfiguration>\
327−\ <TopicConfiguration>\
333+ \ <TopicConfiguration>\
328−\ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
334+ \ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
329−\ <Event>s3:ReducedRedundancyLostObject</Event>\
335+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
330−\ </TopicConfiguration>\
336+ \ </TopicConfiguration>\
331−\ <QueueConfiguration>\
337+ \ <QueueConfiguration>\
332−\ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
338+ \ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
333−\ <Event>s3:ObjectCreated:*</Event>\
339+ \ <Event>s3:ObjectCreated:*</Event>\
334−\ </QueueConfiguration>)\
340+ \ </QueueConfiguration>)\
335−\</NotificationConfiguration>",
341+ \</NotificationConfiguration>",
342+ Notification
336− Notification [ NotificationConfig
343+ [ NotificationConfig
344+ "1"
337− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
345+ "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
338− [ObjectCreatedPut]
346+ [ObjectCreatedPut]
347+ ( Filter $
348+ FilterKey $
339− (Filter $ FilterKey $ FilterRules
349+ FilterRules
340− [FilterRule "prefix" "images/",
350+ [ FilterRule "prefix" "images/",
341− FilterRule "suffix" ".jpg"])
351+ FilterRule "suffix" ".jpg"
352+ ]
353+ ),
342− , NotificationConfig
354+ NotificationConfig
355+ ""
343− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
356+ "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
344− [ObjectCreated] def
357+ [ObjectCreated]
358+ defaultFilter
345− ]
359+ ]
346− [ NotificationConfig
360+ [ NotificationConfig
361+ ""
347− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
362+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
348− [ReducedRedundancyLostObject] def
363+ [ReducedRedundancyLostObject]
364+ defaultFilter
349− ]
365+ ]
350− [ NotificationConfig
366+ [ NotificationConfig
367+ "ObjectCreatedEvents"
351− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
368+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
352− [ObjectCreated] def
369+ [ObjectCreated]
370+ defaultFilter
353− ])
371+ ]
372+ )
354− ]
373+ ]
355
374
356 forM_ cases $ \(xmldata, val) -> do
375 forM_ cases $ \(xmldata, val) -> do
357− result <- runExceptT $ parseNotification xmldata
376+ result <- runExceptT $ runTestNS $ parseNotification xmldata
358 eitherValidationErr result (@?= val)
377 eitherValidationErr result (@?= val)
378+
379+-- | Tests parsing of both progress and stats
380+testParseSelectProgress :: Assertion
381+testParseSelectProgress = do
382+ let cases =
383+ [ ( [r|<?xml version="1.0" encoding="UTF-8"?>
384+<Progress>
385+ <BytesScanned>512</BytesScanned>
386+ <BytesProcessed>1024</BytesProcessed>
387+ <BytesReturned>1024</BytesReturned>
388+</Progress>|],
389+ Progress 512 1024 1024
390+ ),
391+ ( [r|<?xml version="1.0" encoding="UTF-8"?>
392+<Stats>
393+ <BytesScanned>512</BytesScanned>
394+ <BytesProcessed>1024</BytesProcessed>
395+ <BytesReturned>1024</BytesReturned>
396+</Stats>|],
397+ Progress 512 1024 1024
398+ )
399+ ]
400+
401+ forM_ cases $ \(xmldata, progress) -> do
402+ result <- runExceptT $ parseSelectProgress xmldata
403+ eitherValidationErr result (@?= progress)
test/Spec.hs+81−66
@@ -1,5 +1,5 @@
1--
1--
2−-- Minio Haskell SDK, (C) 2017 Minio, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2023 MinIO, Inc.
3--
3--
4-- Licensed under the Apache License, Version 2.0 (the "License");
4-- Licensed under the Apache License, Version 2.0 (the "License");
5-- you may not use this file except in compliance with the License.
5-- you may not use this file except in compliance with the License.
@@ -14,18 +14,17 @@
14-- limitations under the License.
14-- limitations under the License.
15--
15--
16
16
17−import Test.Tasty
17+import qualified Data.ByteString as B
18−import Test.Tasty.QuickCheck as QC
19−
20import qualified Data.List as L
18import qualified Data.List as L
21−
22−import Lib.Prelude
19+import Lib.Prelude
23−
24−import Network.Minio.API.Test
20+import Network.Minio.API.Test
21+import Network.Minio.CopyObject
25−import Network.Minio.PutObject
22+import Network.Minio.Data
26−import Network.Minio.Utils.Test
23+import Network.Minio.Utils.Test
27−import Network.Minio.XmlGenerator.Test
24+import Network.Minio.XmlGenerator.Test
28−import Network.Minio.XmlParser.Test
25+import Network.Minio.XmlParser.Test
26+import Test.Tasty
27+import Test.Tasty.QuickCheck as QC
29
28
30main :: IO ()
29main :: IO ()
31main = defaultMain tests
30main = defaultMain tests
@@ -48,71 +47,87 @@ properties = testGroup "Properties" [qcProps] -- [scProps]
48-- ]
47-- ]
49
48
50qcProps :: TestTree
49qcProps :: TestTree
50+qcProps =
51+ testGroup
51−qcProps = testGroup "(checked by QuickCheck)"
52+ "(checked by QuickCheck)"
52− [ QC.testProperty "selectPartSizes:" $
53+ [ QC.testProperty "selectPartSizes:" $
54+ \n ->
53− \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
55+ let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
54−
55 -- check that pns increments from 1.
56 -- check that pns increments from 1.
56− isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1..]
57+ isPNumsAscendingFrom1 = all (uncurry (==)) $ zip pns [1 ..]
57−
58 consPairs [] = []
58 consPairs [] = []
59 consPairs [_] = []
59 consPairs [_] = []
60− consPairs (a:(b:c)) = (a, b):(consPairs (b:c))
60+ consPairs (a : (b : c)) = (a, b) : consPairs (b : c)
61−
62 -- check `offs` is monotonically increasing.
61 -- check `offs` is monotonically increasing.
63− isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs
62+ isOffsetsAsc = all (uncurry (<)) $ consPairs offs
64−
65 -- check sizes sums to n.
63 -- check sizes sums to n.
66 isSumSizeOk = sum sizes == n
64 isSumSizeOk = sum sizes == n
67−
68 -- check sizes are constant except last
65 -- check sizes are constant except last
69 isSizesConstantExceptLast =
66 isSizesConstantExceptLast =
70− all (\(a, b) -> a == b) (consPairs $ L.init sizes)
67+ all (uncurry (==)) (consPairs $ L.init sizes)
71−
72 -- check each part except last is at least minPartSize;
68 -- check each part except last is at least minPartSize;
73 -- last part may be 0 only if it is the only part.
69 -- last part may be 0 only if it is the only part.
74 nparts = length sizes
70 nparts = length sizes
75 isMinPartSizeOk =
71 isMinPartSizeOk =
72+ if
76− if | nparts > 1 -> -- last part can be smaller but > 0
73+ | nparts > 1 -> -- last part can be smaller but > 0
77− all (>= minPartSize) (take (nparts - 1) sizes) &&
74+ all (>= minPartSize) (take (nparts - 1) sizes)
78− all (\s -> s > 0) (drop (nparts - 1) sizes)
75+ && all (> 0) (drop (nparts - 1) sizes)
79− | nparts == 1 -> -- size may be 0 here.
76+ | nparts == 1 -> -- size may be 0 here.
80− maybe True (\x -> x >= 0 && x <= minPartSize) $
77+ maybe True (\x -> x >= 0 && x <= minPartSize) $
81− headMay sizes
78+ listToMaybe sizes
82− | otherwise -> False
79+ | otherwise -> False
83−
84− in n < 0 ||
80+ in n < 0
85− (isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk &&
81+ || ( isPNumsAscendingFrom1
82+ && isOffsetsAsc
83+ && isSumSizeOk
86− isSizesConstantExceptLast && isMinPartSizeOk)
84+ && isSizesConstantExceptLast
87−
85+ && isMinPartSizeOk
86+ ),
88− , QC.testProperty "selectCopyRanges:" $
87+ QC.testProperty "selectCopyRanges:" $
89− \(start, end) ->
88+ \(start, end) ->
90− let (_, pairs) = L.unzip (selectCopyRanges (start, end))
89+ let (_, pairs) = L.unzip (selectCopyRanges (start, end))
91−
92− -- is last part's snd offset end?
90+ -- is last part's snd offset end?
93− isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
91+ isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
94− -- is first part's fst offset start
92+ -- is first part's fst offset start
95− isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
93+ isFirstPartOk = maybe False ((start ==) . fst) $ listToMaybe pairs
96−
97− -- each pair is >=64MiB except last, and all those parts
94+ -- each pair is >=64MiB except last, and all those parts
98− -- have same size.
95+ -- have same size.
99− initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs
96+ initSizes = maybe [] (map (\(a, b) -> b - a + 1) . init) (nonEmpty pairs)
97+ isPartSizesOk =
100− isPartSizesOk = all (>= minPartSize) initSizes &&
98+ all (>= minPartSize) initSizes
99+ && maybe
100+ True
101− maybe True (\k -> all (== k) initSizes)
101+ (\k -> all (== k) initSizes)
102− (headMay initSizes)
102+ (listToMaybe initSizes)
103−
104− -- returned offsets are contiguous.
103+ -- returned offsets are contiguous.
105− fsts = drop 1 $ map fst pairs
104+ fsts = drop 1 $ map fst pairs
106− snds = take (length pairs - 1) $ map snd pairs
105+ snds = take (length pairs - 1) $ map snd pairs
106+ isContParts =
107− isContParts = length fsts == length snds &&
107+ length fsts == length snds
108− and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
108+ && all (\(a, b) -> a == b + 1) (zip fsts snds)
109−
110− in start < 0 || start > end ||
109+ in start < 0
110+ || start > end
111− (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts)
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
112− ]
119+ ]
113
120
114unitTests :: TestTree
121unitTests :: TestTree
122+unitTests =
123+ testGroup
124+ "Unit tests"
115−unitTests = testGroup "Unit tests" [xmlGeneratorTests, xmlParserTests,
125+ [ xmlGeneratorTests,
126+ xmlParserTests,
116− bucketNameValidityTests,
127+ bucketNameValidityTests,
117− objectNameValidityTests,
128+ objectNameValidityTests,
129+ parseServerInfoJSONTest,
130+ parseHealStatusTest,
131+ parseHealStartRespTest,
118− limitedMapConcurrentlyTests]
132+ limitedMapConcurrentlyTests
133+ ]
test/cert/private.keynew file+28
@@ -0,0 +1,28 @@
1+-----BEGIN PRIVATE KEY-----
2+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC3G9IiC+adjf0p
3+i/2KYc+4dizeuzUFN7wraSdhiOMdQgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwI
4+rPJ61dRGQSuN12l+mzngFJQjE0sysZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPY
5+fhcUcq03onMGq44yOfE6mIhoe0Y9wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq
6+5KjHL8WW2vGg7G9edpYdxINA/A2fdLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L
7+9yRqItqddriRxJFwOXb5OPW8xx2WGaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A8
8+4TD/RXMbAgMBAAECggEBAJ7r1oUWLyGvinn0tijUm6RNbMQjVvEgXoCO008jr3pF
9+PqxVpgEMrOa/4tmwFBus0jcCNF4t3r2zhddBw3I5A/O1vEdvHnBz6NdDBQ8sP6fP
10+1fF50iEe1Y2MBibQkXFxxVMG2QRB1Gt5nuvXA9ELdqtCovK3EsMk5ukkWb/UvjH5
11+8hcmQsaSqvzFEF4wJSY2mkeGSGIJTphPhhuA22xbhaBMInQyhZu8EHsn0h6s/Wgy
12+C4Cp2+4qZTKaaf6x3/ZjJ8CuKiSX+ZsJKjOEv8sqx7j/Y7QFOmJPewInKDhwazr/
13+xIK+N0KXPbUzeSEz6ZvExNDTxtR5ZlQP2UrRDg28yQECgYEA4Is1O2BvKVzNFOkj
14+bTVz25a/bb0Xrcfgi0Y9rdfLzlNdItFjAkxLTVRSW2Hv9ICl0RDDAG+wTlktXRdh
15+rfvDjwG2CvLQo1VEdMWTTkKVg03SwMEy2hFiWV69lENFGSaY8Y6unZDbia5HQinA
16+EgSS4sCojS+a2jtzG5FVVHJDKlkCgYEA0MKhMhD4SUhr2y1idPBrmLxuW5mVozuW
17+8bYaBeSzmfS0BRsN4fP9JGODPBPDdNbfGfGC9ezWLgD/lmCgjIEyBOq8EmqWSsiS
18+Kihds1+Z7hXtbzGsFGAFJJTIh7blBCsK5QFuyuih2UG0fL9z6K/dy+UUJkzrYqph
19+vSfKixyM8pMCgYEAmUPLsNyw4325aeV8TeWnUCJERaZFDFQa21W1cfyS2yEhuEtN
20+llr3JzBACqn9vFk3VU1onNqfb8sE4L696KCpKeqUFEMK0AG6eS4Gzus53Gb5TKJS
21+kHA/PhshsZp9Bp7G1FJ8s4YVo5N2hh2zQVkn3Wh9Y+kzfHQJrK51nO9lEvkCgYBi
22+BuKWle1gzAcJdnhDHRoJMIJJtQbVDYhFnBMALXJAmu1lcFzGe0GlMq1PKqCfXr6I
23+eiXawQmZtJJP1LPPBmOsd2U06KQGHcS00xucvQmVCOrjSdnZ/3SqxsqbH8DOgj+t
24+ZUzXLwHA+N99rJEK9Hob4kfh7ECjpgobPnIXfKKazQKBgQChAuiXHtf/Qq18hY3u
25+x48zFWjGgfd6GpOBZYkXOwGdCJgnYjZbE26LZEnYbwPh8ZUA2vp7mgHRJkD5e3Fj
26+ERuJLCw86WqyYZmLEuBciYGjCZqR5nbavfwsziWD00jeNruds2ZwKxRfFm4V7o2S
27+WLd/RUatd2Uu9f3B2J78OUdnxg==
28+-----END PRIVATE KEY-----
test/cert/public.crtnew file+19
@@ -0,0 +1,19 @@
1+-----BEGIN CERTIFICATE-----
2+MIIDCzCCAfOgAwIBAgIUaIUOMI78LCu+r1zl0mmFHK8n5/AwDQYJKoZIhvcNAQEL
3+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE5MTAyNDE5NTMxOVoYDzIxMTkw
4+OTMwMTk1MzE5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
5+AQUAA4IBDwAwggEKAoIBAQC3G9IiC+adjf0pi/2KYc+4dizeuzUFN7wraSdhiOMd
6+QgCnu9Dc3t2YEsQhNdrARjOTyXd36KeM3TwIrPJ61dRGQSuN12l+mzngFJQjE0sy
7+sZHUJOLQC3rVvIrHSQ57utPg8ifxt/SunlPYfhcUcq03onMGq44yOfE6mIhoe0Y9
8+wcPQ3RjjNNS44bgmXiXwa+Do0h2hEn6/essq5KjHL8WW2vGg7G9edpYdxINA/A2f
9+dLtr8BwPNrZhOx84eee2XcUNdBuTtUUxE+0L9yRqItqddriRxJFwOXb5OPW8xx2W
10+GaV2a0wbE4gB2PTwwDvfo72mo9HXHZUHM1A84TD/RXMbAgMBAAGjUzBRMB0GA1Ud
11+DgQWBBSEWXQ2JRD+OK7/KTmlD+OW16pGmzAfBgNVHSMEGDAWgBSEWXQ2JRD+OK7/
12+KTmlD+OW16pGmzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCF
13+0zYRaabB3X0jzGI9/Lr3Phrb90GvoL1DFLRuiOuTlDkz0vrm/HrZskwHCgMNrkCj
14+OTD9Vpas4D1QZBbQbRzfnf3OOoG4bgmcCwLFZl3dy27yIDAhrmbUP++g9l1Jmy4v
15+vBR/M4lt2scQ8LcZYEPqhEaE5EzFQEjtaxDcKdWDNKY9W1NUzSIABhF9eHiAUNdH
16+AFNJlYeBlCHxcWIeqgon184Dqp/CsvKtz3z3Ni+rlwPM/zuJCFHh1VF+z++0LJjG
17+roBCV0Tro4XyiEz9yp7Cb5kQYMaj1KL9TqBG0tZx0pmv7y+lXc4TT6DEllXz6USy
18+rbIba9/uUet3BqeIMTqj
19+-----END CERTIFICATE-----