Compare commits

...

581 Commits

Author SHA1 Message Date
Harshavardhana 0a3d43273f vendor: sha256 32bit updated. (#2459) 2016-08-16 16:19:29 -07:00
Harshavardhana 4dec50ba51 build: Add platform specific fixes. 2016-08-16 14:40:41 -07:00
Krishna Srinivas f2bffe6086 fs/delete-object: fs.json filepath was incorrect. (#2448) 2016-08-16 10:05:08 -07:00
Krishna Srinivas 8e2f64aea4 fs/multipart: save metadata(fs.json) for multipart uploads. (#2450) 2016-08-16 10:04:40 -07:00
Harshavardhana c054e633fd utils: Shutdown channel should be bufferred. 2016-08-15 21:01:24 -07:00
Harshavardhana e86dfcf41e api: Change listen bucket notification to be TopicConfiguration. (#2447) 2016-08-15 20:56:43 -07:00
Anis Elleuch 3b9dbd748b tests: Web handlers (#2429) 2016-08-15 16:13:03 -07:00
Harshavardhana 3d1bb8f439 tests: Fix hasExtendedHeader tests with env variable. 2016-08-15 16:09:08 -07:00
Krishna Srinivas bb8a425d49 When updating the meta file, write to temp file first and then rename to the actual location.
This prevents appending the metadata to the metadata-file when a file is reuploaded.
2016-08-15 15:55:59 -07:00
Harshavardhana 0e745fdb05 fs: Enable fs.json with env MINIO_ENABLE_FSMETA 2016-08-15 15:53:48 -07:00
Anis Elleuch 51d7749c3e Check if eventN is initialized before notifying in Upload web handler (#2435) 2016-08-15 12:15:46 -07:00
Harshavardhana 76d56c6ff2 typo: Fix typos across the codebase. (#2442) 2016-08-15 02:44:48 -07:00
Harshavardhana b41bfcbf2f utils: Fix unit tests issue. (#2441) 2016-08-15 01:59:28 -07:00
Yurii Rashkovskii 341171f326 Problem: AWS documentation defines event timestamp as 1970-01-01T00:00:00.000Z (#2440)
While Minio is using 20160814T124605Z

(See http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html)

Solution: adhere to AWS documentation
2016-08-15 01:50:07 -07:00
karthic rao a3592228f5 bug-fix: fix for tests failure when cache is disabled (#2439) 2016-08-15 01:25:41 -07:00
Anis Elleuch 5526ac13d2 Protect shutdown callbacks lists with a mutex (#2432) 2016-08-14 23:55:48 -07:00
Harshavardhana 9606cb9bcd posix: Disk free verification should have relaxed handling of inodes. (#2431)
Some filesystems do not implement a way to provide total inodes available, instead inodes
are allocated based on available disk space. For example CephFS, StoreNext CVSFS, AzureFile
driver. Allow for the available disk to be separately validate and we will validate inodes
only if the total inodes are provided by the underlying filesystem.

Fixes #2364
2016-08-13 02:30:15 -07:00
Yurii Rashkovskii 7829ccea2c Routing key was misspelled as routine key (#2430) 2016-08-12 22:23:06 -07:00
Anis Elleuch 723153951c Test api responses (#2427)
* Test List Multipart Uploads with correct max-keys

* Test List Objects V1 V2 with valid parameters
2016-08-12 11:28:27 -07:00
Anis Elleuch 64dc2a2e7f Heal format by inspection should avoid picking minioMetaBucket dir if the latter comes first in the list (listDir not ordered) (#2426) 2016-08-12 08:38:04 -07:00
Anis Elleuch fdab984c8d Add test for fresh disks healing (#2424) 2016-08-12 08:36:43 -07:00
karthic rao 70fd38818e clean up: ineffassign fixes (#2411) 2016-08-12 00:26:30 -07:00
Jesse Lucas ef0a108dde Graceful shutdown for ServerMux (#2341) 2016-08-11 21:33:55 -07:00
karthic rao 0b225269e1 tests: posix: tests cleaning up and enhancing coverage. (#2410) 2016-08-11 19:57:14 -07:00
Anis Elleuch fe62688683 Add tests for Damerau Levenshtein algorithm (#2407) 2016-08-11 17:50:04 -07:00
Anis Elleuch fadb71351c Test Post policy parsing and checking conditions (#2408) 2016-08-11 17:49:40 -07:00
Matthieu Fronton 402af93da2 Update how-to-install-golang URL (#2406) 2016-08-11 12:01:12 -07:00
Harshavardhana d1bb8a5b21 api: refactor the bucket policy reading and writing. (#2395)
Policies are read once during server startup and subsequently
managed through in memory map. In-memory map is updated as
and when there are new changes coming in.
2016-08-10 20:10:47 -07:00
Harshavardhana 97c1289659 handlers: GetObject and HeadObject support more responses. (#2403)
- response-content-encoding.
- response-content-language.

Fixes #2393
2016-08-10 17:36:28 -07:00
Harshavardhana 8274ac2e5a tests: Make sure we try tests on free ports. (#2402)
Fixes #2376
2016-08-10 16:28:05 -07:00
Brendan Ashworth 758aa21b9c tests: add tests for certs.go and trie.go (#2394) 2016-08-10 02:26:40 -07:00
koolhead17 0dc5662f9b Doc: Fixed screenshot path for modified docs. (#2390) 2016-08-09 15:04:44 -07:00
Harshavardhana 82cd38e959 handlers: Remove 'notification.xml' when bucket is deleted. (#2389)
Do not pass around objectHandlers object, input argument
should comply to a type for only that would be used inside
the function body.
2016-08-09 11:33:45 -07:00
karthic rao e0cf4ee9fc presignV4: fix errors response and tests. (#2375)
- Fix error response when one of the query params in the presign URL is
  missing.

- Exhasutive test coverage for presignv4.
2016-08-09 09:13:15 -07:00
Dee Koder 2a920e568c docs: Readded code coverage badge in github Readme. (#2391)
We have the fix in place to hide this on docs.minio.io
2016-08-08 22:14:38 -07:00
Harshavardhana 9c7f75d1e7 handler: Remove unused accesslog handler (#2388) 2016-08-08 21:33:21 -07:00
Harshavardhana 7e46055a15 api/handlers: Implement streaming signature v4 support. (#2370)
* api/handlers: Implement streaming signature v4 support.

Fixes #2326

* tests: Add tests for quick/safe
2016-08-08 20:56:29 -07:00
koolhead17 0c125f3596 Doc: This patch new guides with titles mentioned below (#2382) 2016-08-08 19:39:01 -07:00
GarimaKapoor a1f3bf57c7 Update README.md 2016-08-07 11:08:42 -07:00
Harshavardhana 0188cd0b84 utils: Take monitorShutdownSignal to take an exitFunc which would executed upon error. (#2378)
This hook approach allows program to keep running but being able to handle exiting
of the program in the dynamic way.

Fixes #2377
2016-08-06 23:53:10 -07:00
Harshavardhana b23605a2b5 pkg/objcache: Add more tests. (#2371) 2016-08-06 10:22:14 -07:00
koolhead17 8c2985a9f5 Doc: README.md/Removed codecov badge from title. (#2367) 2016-08-05 16:53:29 -07:00
Anis Elleuch d28fb5fe23 Add a generic registerShutdown function for graceful exit (#2344)
* Add a generic registerShutdown function for graceful exit
* Add shutdown callback test case
2016-08-05 13:48:31 -07:00
GarimaKapoor 62c0612eac Update README.md 2016-08-05 11:35:03 -07:00
karthic rao d63ce9d60d tests: tests for signature v4 parser (#2362) 2016-08-05 08:03:18 -07:00
Remco Verhoef 5a44c34fd7 fixed some issues in readme (#2363)
thx to @MartijnVogel_twitter
2016-08-05 08:02:11 -07:00
Harshavardhana 064c51162d api: Add new ListenBucketNotificationHandler. (#2336)
This API is precursor before implementing `minio lambda` and `mc` continous replication.

This new api is an extention to BucketNofication APIs.

// Request
```
GET /bucket?notificationARN=arn:minio:lambda:us-east-1:10:minio HTTP/1.1
...
...
```

// Response
```

{"Records": ...}
...
...
...
{"Records": ...}
```
2016-08-04 22:01:58 -07:00
Harshavardhana 90c20a8c11 Add codecov for minio. (#2359) 2016-08-04 16:48:50 -07:00
Harshavardhana e783d77c3d Add codecov for minio. 2016-08-04 15:56:26 -07:00
Krishna Srinivas e887fea485 getbucketlocation: should handle UNSIGNED-PAYLOAD for sha256 header for signature calculation. (#2358)
fixes #2355
2016-08-04 15:49:35 -07:00
Harshavardhana de5d5ff241 pkg/crypto: Deprecate cgo sha256 version. (#2354) 2016-08-04 03:19:36 -07:00
karthic rao 2e0742e309 bucket policy: Support for '?' wildcard. (#2353)
- Support for '?' wildcard for resource matching.

- Wildcard package is added with Match functions.

- Wildcard.Match supports '*' and wild.MatchExtended supports both '*'
  and '?' wildcards in the pattern string.

- Tests for the same for the wide range of cases.
2016-08-04 00:41:32 -07:00
Anis Elleuch cc0d5b6fe0 webapi: ServerInfo returns the list variables/values in the environnment of the Minio server (#2351) 2016-08-03 13:47:03 -07:00
Harshavardhana 2db51e9d61 server/config: config should be updated only when we edit the credentials. (#2345) 2016-08-02 16:48:21 -07:00
Krishna Srinivas 69fd196471 Object-cache: enforce cache size to be less than RAM. (#2338) 2016-08-02 10:04:35 -07:00
karthic rao 1494af485e tests: signature-utils test (#2342) 2016-08-01 20:54:11 -07:00
Harshavardhana c1d70f1f9e server/config: Create 'certs' directory in initServerConfig(). (#2331)
certs directory was created only if config was not present, our
expectancy is we need 'certs' directory to be present all the
time making it easier to be documented.
2016-07-30 14:55:20 -07:00
karthic rao 9baf599c91 tests: Unit tests and fixes for copyBuffer. (#2333)
- Unit tests for copyBuffer.
- Shadowing fix for copyBuffer.
2016-07-30 13:36:43 -07:00
Harshavardhana 8d090a20ce server: set globalCacheSize honoring system limits for max memory. (#2332)
On unix systems it is possible to set max memory used by
running processes using 'ulimit -m' or 'syscall.RLIMIT_AS'.

A process whence exceeds this limit, kernel would pro-actively
kill such a server with OOM. To avoid this problem of defaulting
our cache size to 8GB we should look for if the current system
limits are lower and set the cache size appropriately.
2016-07-30 08:50:49 -07:00
karthic rao 5b86dd7659 Tests: Cleanup/Enhancement: Add few more cases to posix.ReadFile tests and use a cleaner posixTestSetup for posix tests (#2330) 2016-07-30 01:26:19 -07:00
Jesse Lucas 4b05b6a6c1 Refactoring checkPortAvailability to check each tcp network (tcp, tcp4, tcp6) if a port is taken. (#2325) 2016-07-29 18:11:00 -07:00
Jesse Lucas 851d05161a Adding return error value to checkPortAvailability to enable testing of function. Adding checkport_test.go to test checkPortAvailability. Updated server-main.go to use error value from checkPortAvailability and calls fatalIf if an error is returned. (#2322) 2016-07-29 14:05:31 -07:00
Harshavardhana cf9ba7b88f tests: Add missing unit test for posix.ReadFile. (#2319) 2016-07-28 21:57:11 -07:00
Krishnan Parthasarathi 50dae0ab04 bucket-policy: Migrate bucket policy to minioMetaBuket/buckets (#2321) 2016-07-28 20:49:08 -07:00
Anis Elleuch 14cefd352c Heal corrupted formats of disks already containing objects (#2297) 2016-07-28 16:49:58 -07:00
Frank f239fcac67 Switched to faster minio/sha256-simd implementation (#2320) 2016-07-28 14:44:37 -07:00
Anis Elleuch dcc3463e48 Limit POST form fields and file size + Generic Request Size limiter (#2317)
* Use less memory when receiving a file via multipart
* Add generic http request maximum size limiter to secure against malicious clients
2016-07-28 12:02:22 -07:00
Krishna Srinivas 7850d17f48 web-browser: disable minio browser when environmental variable MINIO_BROWSER=off (#2315)
fixes #2314
2016-07-28 04:00:33 -07:00
Harshavardhana f503ac3db8 XL/Erasure: Make bit-rot verification based on xl.json algo. (#2299)
Currently `xl.json` saves algorithm information for bit-rot
verification. Since the bit-rot algo's can change in the
future make sure the erasureReadFile doesn't default to
a particular algo. Instead use the checkSumInfo.
2016-07-28 02:20:34 -07:00
Harshavardhana 65f71ce0c5 browser: Object upload should save metadata and notify. (#2309)
Object upload from browser should save additional
incoming metadata. Additionally should also notify
through bucket notifications once they are set.

Fixes #2292
2016-07-27 21:11:15 -07:00
Harshavardhana ad19bf0ec1 server: Add update referral for update notification URL. (#2308) 2016-07-27 19:59:19 -07:00
Harshavardhana f0067babe0 handlers: Add 'crossdomain.xml' handler. (#2305)
Fixes #2301
2016-07-27 19:53:55 -07:00
karthic rao 6b5fce826b placing defer file.Close() right after opening it (#2306) 2016-07-27 19:22:32 -07:00
Anis Elleuch 8b3cb3a0de POST Policy, multiple fixes: AccessDenied with unmet conditions, ${filename} in Key, missing filename in multipart (#2304)
* Unsatisfied conditions will return AccessDenied instead of MissingFields

* Require form-field `file` in POST policy and make `filename` an optional attribute

* S3 feature: Replace  in Key by filename attribute passed in multipart
2016-07-27 17:51:55 -07:00
Harshavardhana 2f7358a8a6 XL: erasure Index should have its corresponding distribution order. (#2300) 2016-07-27 11:57:08 -07:00
Harshavardhana 77248bd6e8 api: Notify events only if bucket notifications are set. (#2293)
While the existing code worked, it went to an entire cycle
of constructing event structure and end up not sending it.

Avoid this in the first place, but returning quickly if
notifications are not set on the bucket.
2016-07-26 19:10:02 -07:00
Harshavardhana 3054b74260 docs: Fix startup message for server as well. 2016-07-26 15:54:11 -07:00
koolhead17 7d42d09da8 Doc: Replaced README & FreeBSD docs with updated minio server splash (#2298)
screen.
2016-07-26 15:46:41 -07:00
Anis Elleuch 95ddf061ab Rate limit is working and supports limited waiting clients (#2295) 2016-07-26 14:17:11 -07:00
karthic rao 5fe72cf205 Removing readAllMeta from xl-v1-healing.go and placing it in xl-v1-utils.go (#2296) 2016-07-26 11:34:48 -07:00
Harshavardhana 1e3d80552f XL: format.json healing should cater for mismatching order. (#2285)
Fresh disks can be provided in any order, we need to make sure
to preserve existing disk order and populate the fresh disks
in new positions.

Thanks for Anis Elleuch <vadmeste@gmail.com> for finding this issue.
2016-07-26 03:18:47 -07:00
Harshavardhana f253dfc922 docs: Fix erasure code image embedding issue. 2016-07-26 00:09:57 -07:00
Harshavardhana 1f9e38e3cd api: Add bucket notification util tests. (#2289) 2016-07-26 00:01:35 -07:00
karthic rao 530ed67b59 Adding leak test framework (#2254) 2016-07-25 20:39:14 -07:00
Harshavardhana a2b6f0524d XL/erasure: Remove deprecated copyN function. (#2288) 2016-07-25 20:36:56 -07:00
karthic rao 091d80666a Enhancement for Erasure encode test. (#2287) 2016-07-25 20:36:41 -07:00
Harshavardhana efbf7dbc0f api: Bucket notification add filter rules check and validate. (#2272)
These filtering techniques are used to validate
object names for their prefix and suffix.
2016-07-25 17:53:55 -07:00
Krishna Srinivas 043ddbd834 optimize memory allocation during erasure-read by using temporary buffer pool. (#2259)
* XL/erasure-read: optimize memory allocation during erasure-read by using temporary buffer pool.

With the change the buffer needed during GetObject by erasureReadFile is allocated only once.
2016-07-25 14:17:01 -07:00
Dee Koder 04f90bd463 doc: Broken links fixed in the Explore further section. (#2281) 2016-07-24 22:53:35 -07:00
Harshavardhana 9212e11b90 XL/GetObject: When disk is not available, checksum should be empty. (#2276) 2016-07-24 22:49:27 -07:00
Harshavardhana 79bab6b561 XL: Operations on uploads.json should cater for disk being unavailable. (#2277) 2016-07-24 18:08:15 -07:00
Krishnan Parthasarathi 7e5a78985d tests: Using listObjects clean up remaining tree walk go routines. (#2278)
* fs: Set nextMarker independent of it having a slash or not.
* tests: Using listObjects clean up remaining tree walk go routines.
* tests: Use slices to hold data instead of enumerating test cases by hand

... also fixed numbering of test cases.
2016-07-24 15:52:12 -07:00
Anis Elleuch b0b919a1d6 Server http and https on the same port using a customized server (#2247) 2016-07-24 12:30:57 -07:00
Harshavardhana 6c2fb19ed7 docs: Removed and purged uneeded docs. (#2273) 2016-07-24 03:32:45 -07:00
Harshavardhana f248089523 api: Implement bucket notification. (#2271)
* Implement basic S3 notifications through queues

Supports multiple queues and three basic queue types:

1. NilQueue -- messages don't get sent anywhere
2. LogQueue -- messages get logged
3. AmqpQueue -- messages are sent to an AMQP queue

* api: Implement bucket notification.

Supports two different queue types

- AMQP
- ElasticSearch.

* Add support for redis
2016-07-23 22:51:12 -07:00
Harshavardhana f85d94288d api: extract http headers with some supported header list. (#2268) 2016-07-22 20:31:45 -07:00
Harshavardhana 55cb55675c api/multipart: Send S3 compatible error message, missing second sentence. (#2270) 2016-07-22 17:05:40 -07:00
koolhead17 7e076577de Update Minio-erasure-code-quickStart-guide.md (#2269)
Minor update to link the URL.
2016-07-22 13:35:27 -07:00
Harshavardhana 5d118141cd XL: Remove deadcode unionChecksumInfo. (#2261) 2016-07-21 19:07:00 -07:00
karthic rao 646ff2c64d Get Object disk not found test (#2264)
Test: GetObject disk not found test
2016-07-21 19:06:50 -07:00
Harshavardhana 0add96f655 fs: Save metadata for objects in minioMetaBucket directory. (#2251) 2016-07-21 17:31:14 -07:00
Krishna Srinivas 303f216150 tests: xl-v1-metadata.go, xl-v1-multipart-common.go - remove unused methods, add enhance tests to improve code coverage. (#2260) 2016-07-21 15:00:11 -07:00
koolhead17 a7b5b8e63f Doc: Modified the contents for Doctor. (#2262) 2016-07-21 14:58:16 -07:00
Krishnan Parthasarathi 5730d40478 tests: Added GetObject, DeleteObject and PutObject unit-tests (#2222) 2016-07-21 13:15:54 -07:00
karthic rao 0eaf684777 Remove consuming benchmarks, clean up closures, correct Get and PutObject Parallel benchmarks (#2258) 2016-07-21 11:17:28 -07:00
Harshavardhana a0635dcdd9 XL: Do not rely on getLoadBalancedQuorumDisks for NS consistency. (#2243)
The reason is any function relying on `getLoadBalancedQuorumDisks`
cannot possibly have an idempotent behavior.

The problem comes from given a set of N disks returning just a
shuffled N/2 disks.  In case of a scenario where we have N/2
number of failed disks, the returned value of `getLoadBalancedQuorumDisks`
is not equal to the same failed disks so essentially calls using such
disks might succeed or fail randomly at different intervals in time.

This proposal change is we move to `getLoadBalancedDisks()`
and use the shuffled N disks as a whole. Since most of the time we might
hit a good disk since we are not reducing our solution space. This
also provides consistent behavior for all the functions which rely
on shuffled disks.

Fixes #2242
2016-07-21 00:27:08 -07:00
Dee Koder 41f4f2806d screenshots: update with the latest optimized image. (#2249) 2016-07-20 16:15:26 -07:00
Dee Koder 2a972ef1fd images: Move screenshot for docs inside docs/screenshots directory. (#2248)
* images: Move screenshot for docs inside docs/screenshots directory. Use optimized images.

* images: This fix optimizes the images for the Erasure Code Quick Start Guide
2016-07-20 13:52:30 -07:00
Harshavardhana c1e953b368 api: Set content-encoding properly if set. (#2245)
Additionally don't set content-type if not present, golang http
server automaticaly handles this and sets it automatically.
2016-07-20 12:40:20 -07:00
Krishna Srinivas 18728a0b59 XL/erasure-read: refactor erasure read and add tests (#2232) 2016-07-20 01:30:30 -07:00
Harshavardhana cef26fd6ea XL: Refactor usage of reduceErrs and consistent behavior. (#2240)
This refactor is also needed in lieu of our quorum
requirement change for the newly understood logic behind
klauspost/reedsolom implementation.
2016-07-19 19:24:32 -07:00
Dee Koder f67c930731 doc: Fixed spacing with respect to code blocks. (#2241) 2016-07-19 19:08:43 -07:00
GarimaKapoor 3589a58179 Update Minio-erasure-code-quickStart-guide.md 2016-07-19 17:59:05 -07:00
Dee Koder e8155abc18 screenshot: Use the full path to the screenshot when embedding images (#2239) 2016-07-19 17:48:18 -07:00
Dee Koder 2e8360120d headings: We need to add a consistent heading for all docs. Adding Minio FreeBSD QuickStart Guide in the title. (#2233) 2016-07-19 14:56:34 -07:00
Dee Koder 02b191222c headings: Added standardized heading for this document. (#2234) 2016-07-19 14:56:20 -07:00
Dee Koder b699795901 docs: Remove additional headings. Added standard heading. Include numbering. (#2235) 2016-07-19 14:30:32 -07:00
Harshavardhana 86d31e99d5 api: use checkAuth now at PutBucket, DeleteBucket handlers. (#2225)
Additionally add a unit test for isReqAuthenticated function.
2016-07-18 23:56:27 -07:00
Krishna Srinivas 897d78d113 erasureReadFile and erasureCreateFile testcases. (#2229)
* unit-tests: Unit tests for erasureCreateFile and erasureReadFile.

* appendFile() should return errXLWriteQuorum.

* TestErasureReadFileOffsetLength() tests erasureReadFile() for different offset and lengths.

* Fix for the failure seen in the erasure read unit test case. Issue #2227

* Move common erasure setup code to newErasureTestSetup()

* Review fixes. Add few more test cases for erasureReadFile.
2016-07-18 23:56:16 -07:00
Harshavardhana 1f706e067d api: xmlDecoder should honor contentLength. (#2226)
This is needed so that we avoid reading large amounts
of data from compromised clients.
2016-07-18 21:20:17 -07:00
Krishnan Parthasarathi 5cc9e4e214 fs/XL: Return IncompleteBody{} error for short writes (#2228) 2016-07-18 19:06:48 -07:00
Krishna Srinivas 27a5b61f40 tree-walk: optimize tree walk such that leaf detection of entries is delayed till the entry is sent on the treeWalkResult channel. (#2220) 2016-07-17 15:16:52 -07:00
Harshavardhana aeac902747 API: ListBuckets doesn't have a body, we should never read the body. (#2218)
ListBuckets was incorrectly reading the body of the request, fix it.
2016-07-17 13:23:15 -07:00
Harshavardhana aaf7803831 api: Requests should be differentiated if possible based on http router. (#2219)
In current master ListObjectsV2 was merged into ListObjectsHandler
which also implements V1 API as well.

Move the detection of ListObject types to its rightful place
in http router.
2016-07-17 12:32:05 -07:00
Krishna Srinivas 8cc163e51a Refactor xl.GetObject and erasureReadFile. (#2211)
* XL: Refactor xl.GetObject and erasureReadFile. erasureReadFile() responsible for just erasure coding, it takes ordered disks and checkSum slice.

* move getOrderedPartsMetadata and getOrderedDisks to xl-v1-utils.go

* Review fixes.
2016-07-16 08:35:30 -07:00
Harshavardhana 2d38046a5a utils: BucketNames with double periods and ip address should be rejected. (#2213)
Fixes #2212
2016-07-15 17:30:37 -07:00
Harshavardhana cbb6b48b94 doc: update README.md 2016-07-15 16:12:54 -07:00
Harshavardhana d0636d633d doc: Move FreeBSD.md to docs. 2016-07-15 16:09:01 -07:00
Harshavardhana 41187fc2ef docker: Fix docker edge build 2016-07-15 15:10:38 -07:00
koolhead17 204ec2c6c0 doc:README.md/Updated to sync with docs.minio.io (#2210)
* doc:README.md/Updated to sync with docs.minio.io

* doc:README.me/Modified the minio server output terminal to reflect new release changes.

* docs:README.md/Modified and changed location of other markdown files.
2016-07-15 15:03:59 -07:00
Krishnan Parthasarathi 3bce5db6d1 tests: Add tests to treeWalk for sortedness, recursive listing and isEnd behaviour (#2209) 2016-07-14 18:37:43 -07:00
Harshavardhana 35d438e0ff vendorize: update all vendorized packages. (#2206)
Bring in new changes from upstream for all the packages.

Important ones include
   - gorilla/mux
   - logrus
   - jwt
2016-07-14 14:59:20 -07:00
Krishna Srinivas b090c7112e Refactor of xl.PutObjectPart and erasureCreateFile. (#2193)
* XL: Refactor of xl.PutObjectPart and erasureCreateFile.

* GetCheckSum and AddCheckSum methods for xlMetaV1

* Simple unit test case for erasureCreateFile()
2016-07-14 14:59:01 -07:00
Harshavardhana af6109f89a update: Remove extraneous '/' in update message. (#2207) 2016-07-14 14:08:16 -07:00
Anis Elleuch 3f27734c22 Use normal color instead of forced white for users who have bright terminal background (#2200) 2016-07-13 14:27:36 -07:00
Harshavardhana cdf1373f8e XL: Ignore and continue for cases when bucket does not exist. (#2205)
Fixes #2201
Fixes #2204
2016-07-13 13:44:33 -07:00
Krishnan Parthasarathi 45240f158d xl: Make namespace locking granular for PutObject (#2199) 2016-07-13 11:56:25 -07:00
Harshavardhana 0bd6b67ca5 server: Sort ips based on their last octet value. (#2198) 2016-07-13 06:34:59 -07:00
Harshavardhana 8c84df5e74 server: Change color codes for headings and sub-headings. (#2197)
This patch changes the color coding used for headings, sub-headings
and values as finalized.
2016-07-13 00:56:00 -07:00
Harshavardhana dc3bafb194 XL: isQuorum rename as isDiskQuorum, word it properly. (#2196) 2016-07-13 00:29:48 -07:00
Harshavardhana 3b69b4ada4 server: Change server startup message. (#2195)
This change brings in the new agreed startup message
for the server.

Adds additional links point to Minio SDKs as well.
2016-07-12 23:21:18 -07:00
Krishnan Parthasarathi 0610527868 XL: PutObjectPart update checksum, re-read from xl.json for the part being written. (#2191) 2016-07-12 18:23:40 -07:00
Harshavardhana 0fcfb5df3b XL/fs: Change minioMetaBucket different than '.minio' config dir. (#2190)
This fixes corruption of config directory seen when minio server
exports 'home' directory.

```
minio server ~
```
2016-07-12 15:21:29 -07:00
Harshavardhana 623e0f9243 XL: listOnlineDisks should use modTime instead of version. (#2166)
This change is needed to make reading from objects future proof
in-terms of handling online disks. Our current counter is not
based on affirmative knowledge and relies on arithmetic sequence
which can lead to bugs.

Using modTime simplifies the understanding of `xl.json` and future
tooling / debugging of the format.
2016-07-12 15:20:31 -07:00
utsl42 e5cd35aad0 XL: GetObjectInfo() store and retrieve user-defined object metadata. (#2189) 2016-07-12 12:45:17 -07:00
Anis Elleuch 5cd629adca XL/fs: DeleteVol should not return error cleaning multipart dir for errVolumeNotFound (#2188) 2016-07-12 10:07:32 -07:00
Anis Elleuch 0fddf3fe17 Avoid creating tmp directories under .minio/tmp/ to facilitate cleaning (#2187) 2016-07-12 09:38:45 -07:00
karthic rao ac6ff67546 Tool for running benchmark comparison of 2 commits (#2161) 2016-07-12 02:08:38 -07:00
Harshavardhana 126865e8df XL/bucket: Remove bucket should cleanup incomplete uploads as well. (#2173)
This behavior is in accordance with S3.

Fixes #2170
2016-07-12 01:01:47 -07:00
Krishnan Parthasarathi 1c82b81408 Rename parts/objects only on onlineDisks (#2185) 2016-07-11 22:53:54 -07:00
Bala FA 749a94f6c9 tests: Add tests for signature-jwt code (#2169)
Fixes #1989
2016-07-11 21:57:40 -07:00
Harshavardhana e9647b5f12 API/CopyObject: Refactor the code and handle if-modified-since as well. (#2183)
This completes the S3 spec behavior for CopyObject API as reported
by `s3verify`.
2016-07-11 19:24:34 -07:00
Krishnan Parthasarathi bef72f26db xl: Make locking more granular for PutObjectPart requests (#2168) 2016-07-11 17:24:49 -07:00
Harshavardhana ede4dd0f9c server: update command should check for 3s from 1ms. (#2175)
Programmer error :-)
2016-07-11 16:22:10 -07:00
Bala FA bfc59b7d50 tests: improve unit tests for xl-v1-metadata. (#2172)
Fixes #2124
2016-07-11 11:42:01 -07:00
Remco Verhoef a162198623 implemented systemd script (#2167) 2016-07-11 03:55:40 -07:00
Harshavardhana de468f92ec posix: ReadAll should handle the case when parent is not a dir. (#2163)
It can happen so that a read request can come for a file which
already has a parent i.e a file.

This fix handles this scenario - fixes #2047
2016-07-11 00:15:37 -07:00
Harshavardhana d676e660c9 API/CopyObject: If-None-Match should return Precondition failed. (#2164) 2016-07-10 17:32:59 -07:00
Krishna Srinivas aa7079fc7b XL/GetObject: If quorum not available during GetObject appropriate error should be returned. (#2135) 2016-07-10 17:12:22 -07:00
Harshavardhana bdff0848ed server: Implement --ignore-disks for ignoring disks from healing. (#2158)
By default server heals/creates missing directories and re-populates
`format.json`, in some scenarios when disk is down for maintainenance
it would be beneficial for users to ignore such disks rather than
mistakenly using `root` partition.

Fixes #2128
2016-07-10 14:38:15 -07:00
Bala FA 0793237d94 tests: Move signature calculation in separate function. (#2160)
Previously newTestRequest() creates request object and returns
signature v4 signed request.  In TestCopyObject(), its required to add
headers later to the request and sign the request.

This patch introduces two new functions
* signRequest(): signs request using given access/secret keys.
* newTestSignedRequest(): returns new request object signed with given
  access/secret keys.

Fixes #2097
2016-07-10 11:10:59 -07:00
Bala FA 2a95eabb8a benchmarks: add parallel benchmarks for PutObject/GetObject. (#2159)
Fixes #2092
2016-07-10 11:08:45 -07:00
Krishnan Parthasarathi bc8720406d Added specific error for InvalidObjectName (#2157) 2016-07-09 17:11:08 -07:00
Krishna Srinivas ae80f8ca35 ObjectLayer/GetObject: Should return the right error value. Fix done in FS and XL. (#2133)
fixes #2117
2016-07-09 13:01:32 -07:00
Harshavardhana 5102a5877e API/handler: CopyObject make it behave in accordance with S3 spec. (#2155)
Fixes bugs found while running s3verify tool - fixes #2152
2016-07-09 12:13:40 -07:00
karthic rao 3341fe9b28 organizing the benchmarks in the right test files (#2154) 2016-07-09 00:45:49 -07:00
Harshavardhana c0c8a8430e XL/PutObject: Add single putObject and multipart caching. (#2115)
- Additionally adds test cases as well for object cache.
- Adds auto-expiry with expiration and cleanup time interval.

Fixes #2080
Fixes #2091
2016-07-08 20:34:27 -07:00
karthic rao b0c180b77c Test for ObjectLayer.GetObject() (#2153) 2016-07-08 18:26:04 -07:00
karthic rao 778b870b77 placing the http range error in objct-api-errors. (#2150) 2016-07-08 17:22:55 -07:00
Harshavardhana cb415ef12e Merge pull request #2149 from harshavardhana/hash-order
XL/metadata: use new hashOrder algorithm for newXLMeta. (#2147)
2016-07-08 15:57:16 -07:00
Harshavardhana 6266328a85 XL/metadata: use new hashOrder algorithm for newXLMeta. (#2147) 2016-07-08 15:39:21 -07:00
frankw 63b3f1dcfd Use new algorithm to get fixed random order of disks (#2147) 2016-07-08 15:38:47 -07:00
Anis Elleuch 5ff1203fc0 Add PutObjectPart benchmark (#2145) 2016-07-08 14:28:06 -07:00
Harshavardhana c3ab8bbd51 Turning off OSX builds for now. 2016-07-08 11:22:25 -07:00
Harshavardhana a4a55bf134 tests: Fix erasure-readfile test formatting. 2016-07-08 11:05:30 -07:00
Harshavardhana ec35330ebb XL/fs: GetObject should validate all its inputs. (#2142)
Fixes #2141
Fixes #2139
2016-07-08 07:46:49 -07:00
Harshavardhana ca1b1921c4 XL: Implement ignore errors. (#2136)
Each metadata ops have a list of errors which can be
ignored, this is essentially needed when

  - disks are not found
  - disks are found but cannot be accessed (permission denied)
  - disks are there but fresh disks were added

This is needed since we don't have healing code in place where
it would have healed the fresh disks added.

Fixes #2072
2016-07-07 22:10:27 -07:00
Harshavardhana 4c21d6d09d tests: Remove racey failedDisks behavior in RenameObject tests. (#2138)
Additionally also initialize namespace lock only once per test
run, there is no reason to initialize it multiple times to avoid
races.

Fixes #2137
2016-07-07 19:50:44 -07:00
Bala FA 5ec7734d88 FS: Check offset is within object size in GetObject() (#2123)
Fixes #2118
2016-07-07 19:49:45 -07:00
karthic rao 2c837128ef Object layer tests revamp for individual execution (#2134) 2016-07-07 15:05:51 -07:00
Bala FA 282044d454 http: handle possible range requests properly. (#2122)
Previously range string is not validated against various combination
of values.  This patch fixes the issue.

Fixes #2098
2016-07-07 15:05:18 -07:00
Harshavardhana 8e53064bb4 XL/format: Initialize missing meta volume. (#2131)
Fixes #2127
2016-07-07 15:01:54 -07:00
Krishna Srinivas f55093cdd6 multipart: During multipart list the listing go-routine should be saved to the List-pool. (#2130) 2016-07-07 09:06:35 -07:00
Harshavardhana ddf3245677 xl/fs: offset and length cannot be negative. (#2121)
Fixes #2119
2016-07-07 01:30:34 -07:00
Harshavardhana 169c72cdab vendor: Bring new updates from blake2b-simd repo. (#2094)
This vendorization is needed to bring in new improvements
and support for AVX2 and SSE.

Fixes #2081
2016-07-06 18:24:31 -07:00
Nick Craig-Wood 8c767218a4 URL Encode X-Amz-Copy-Source as per the spec (#2114)
The documents for COPY state that the X-Amz-Copy-Source must be URL encoded.

http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectCOPY.html
2016-07-06 15:42:17 -07:00
Bala FA a51bb1d728 http: handle request range like Amazon S3. (#2112)
Fixes #2098
2016-07-06 12:50:24 -07:00
Krishna Srinivas 26b7d79a85 XL/ObjectCache: DeleteObject() should delete the object from the object cache. (#2113)
fixes #2103
2016-07-06 10:25:42 -07:00
Krishna Srinivas 01cbacd803 object-cache: use golang bytes.Buffer and bytes.NewReader instead of custom implementation. (#2108) 2016-07-06 01:29:49 -07:00
Harshavardhana 7bde27032d signv4: Validate preSigned payload properly. (#2106)
We need to only validate presigned payload only
if the payload is requested for, with default payload
i.e 'UNSIGNED-PAYLOAD' we don't need to validate.

Fixes #2105
2016-07-05 21:00:20 -07:00
Bala FA 44ae7a037b fix: allocate buffer to required size than readSizeV1 (#2095)
Refer #2077
2016-07-05 20:59:54 -07:00
karthic rao a35341448f XL: Cache: Purging partially cached content upon erasureReadFile failure (#2104) 2016-07-05 12:48:54 -07:00
Krishna Srinivas d6dfcd0ba7 unit-tests: Unit tests for functions in erasure-readfile.go (#2090) 2016-07-05 11:41:25 -07:00
Harshavardhana 8ddf52021a server: Bring in s3 compatibility fixes. (#2099)
This patch fixes majority of discrepant messages and responses
previously reported.

There are few discrepancies observed

- S3 is not honoring 'If-Modified-Since' header.
- We do not implement object policy, S3 returns a different response in this category.
- Adding new headers causes signature mismatch, but Minio server is fine for example
  TestCopyObject() to be fixed by moving the signature logic out.
  Relevant bug - https://github.com/minio/minio/issues/2097

Fixes #1955
2016-07-05 01:06:30 -07:00
Harshavardhana 8a028a9efb handler/PUT: Handle signature verification through a custom reader. (#2066)
Change brings in a new signVerifyReader which provides a io.Reader
compatible reader, additionally implements Verify() function.

Verify() function validates the signature present in the incoming
request. This approach is choosen to avoid complexities involved
in using io.Pipe().

Thanks to Krishna for his inputs on this.

Fixes #2058
Fixes #2054
Fixes #2087
2016-07-05 01:04:50 -07:00
Bala FA 0540863663 fix: use readSizeV1 wherever applicable. (#2093) 2016-07-04 19:21:15 -07:00
Harshavardhana 4cfbdb1bf0 server: Remove deadcode/deprecated code. (#2088) 2016-07-04 14:46:38 -07:00
Krishna Srinivas 1ec86dac2c server-tests: unify XL and FS tests into common code. server_test.go contains common test code. server_xl_test.go contains XL tests specific to XL. (#2089) 2016-07-04 13:18:41 -07:00
Krishna Srinivas 7a8b8cd0a1 tree-walk: unify FS and XL tree-walk with functional approach. (#2027) 2016-07-04 01:49:27 -07:00
karthic rao a8a3e95835 Api/Bucket-Policy: Handler tests (#2074) 2016-07-03 22:35:30 -07:00
Bala FA 1ad5fb8f76 posix: checkDiskFree() also checks free inodes. (#2086)
Previously checkDiskFree() checks for free available space.  This
patch enables checkDiskFree() also checks for free inodes in linux and
free clusters in windows.

Fixes #2075
2016-07-03 22:34:45 -07:00
Bala FA 52b55afce0 FS: check whether disk format is FS or not. (#2083)
Fixes #2060
2016-07-03 20:01:40 -07:00
Harshavardhana 355f06cfea tests: Add urlEncode tests. (#2078) 2016-07-03 19:25:04 -07:00
Harshavardhana d2557bb538 XL: GetObject caching implemented for XL. (#2017)
The object cache implementation is XL cache, which defaults
to 8GB worth of read cache. Currently GetObject() transparently
writes to this cache upon first client read and then subsequently
serves reads from the same cache.

Currently expiration is not implemented.
2016-07-03 17:15:38 -07:00
Bala FA 8d4365d23c tests: add unit test for posix functions. (#2037)
Unit tests for posix operations.

* MakeVol
* DeleteVol
* StatVol
* ListVols
* DeleteFile
* AppendFile
* RenameFile
* StatFile

Fixes #2021
2016-07-03 11:17:08 -07:00
karthic rao 55ae7cac42 api/bucket-policy: Refactor, Handler test. (#2071)
PR contains,
- New setup utilities for running object handler tests. Here is why they are essential, 
    - Unit tests have to be run in isolation without being have to be associated with other functionalities which are not under test. 
    - The integration tests follows the philosophy of running a Test Server and registers all handlers and fires HTTP requests over the socket to simulate the system functionality under usual work load scenarios and test for correctness. But this philosophy cannot be adopted for running unit tests for HTTP handlers. 
    - Running Unit tests for API handlers, 
        - Shouldn't run a test server. Should purely call the handlers `ServeHTTP` under isolation. 
        - Shouldn't register all handlers, should only register handlers under test and so that the system is close to be in an isolated setup. 

- As an example PutBucketPolicy test is illustrated using the new setup. Exhaustive cases has to be added and has been listen in TODO for now.
2016-07-02 19:05:16 -07:00
Harshavardhana 48ac34919f browser: Add new release for ui-assets.go (#2070)
update `ui-assets.go` using `x-amz-date` for JSON rpc.
2016-07-02 10:54:17 -07:00
Harshavardhana d64c3fd464 posix: Return errDiskNotWritable during disk initialization. (#2048)
It can happen that minio server might not have
writable permissions on the export paths command line.

Fixes #2035
2016-07-02 01:59:28 -07:00
Harshavardhana e5dd917c37 handlers/generic: Remove support for 'x-minio-date' (#2064) 2016-07-02 01:51:17 -07:00
Krishna Srinivas eb5f782c74 object-handler: skip sha256 calculation if x-amz-content-sha256=="UNSIGNED-PAYLOAD" (#2038)
fixes #2024 #2056
2016-07-01 14:34:40 -07:00
Harshavardhana 734e779b19 XL/erasureCreate: Create a limit reader if size is specified. (#2059)
This is needed so that we only write data which was requested
for, using a limit reader avoids spurious reads on the incoming
client data. Additionally using limit reader provides server
safety from rogue clients sending copious amounts of data (for
example a denial of service attack).

This patch also caters for size == -1 when content encoding from
a client is set as chunked, we happily read till io.EOF
2016-07-01 14:33:28 -07:00
Krishna Srinivas 3f2b4d9dc2 Show "https" in the "minio server export/" output if certificates are available. (#2065)
fixes #2036
2016-07-01 14:32:53 -07:00
Bala FA cd1c2db864 posix-utils: fix path handling in windows. (#2053) 2016-07-01 11:44:07 -07:00
karthic rao 48aa5f2199 api/bucket-policy: Add unit tests for more coverage, fixes couple of bugs. (#2055)
Changes to ResourceMatch logic.
Test for action match function.
2016-06-30 23:49:59 -07:00
Krishnan Parthasarathi bcb822c390 Send XML header before the first of whitespace chars (#2046)
* Sent XML header before the first of whitespace chars

XML parsing fails in aws cli due to unexpected whitespace character. To
fix this, we send the xml header before we send the first whitespace
character, if any.

* Fix race between sendWhiteSpaceChars and completeMultiUploadpart
2016-06-30 18:48:50 -07:00
Krishnan Parthasarathi 285a94d2c0 Ignored errors in cleanup in commitXLMetadata (#2044)
Deletion of tmp files where xl metadata was saved before the commit
operation doesn't change the error returned to the caller. So, it is to
be ignored.
2016-06-30 16:37:51 -07:00
Krishnan Parthasarathi 64899e5197 xl: Used unique tmp file to update xl.json in putObjectPart (#2043)
An in-place update to xl.json amidst concurrent PutObjectPart operations
lead to racy updates to xl.json making it un-parseable. To avoid this,
we create a unique tmp file where updates to xl.json are staged before
renaming it to the final location.
2016-06-30 16:28:01 -07:00
Danilo Pereira 812554087f getCertsPath should use getConfigPath instead of defaulting to users homedir. (#2039)
Fixes #2028
2016-06-30 15:49:18 -07:00
Bala FA 57bc08cc7e posix: remove disk free space check for read-only and delete methods. (#2033) 2016-06-29 11:25:35 -07:00
Harshavardhana 0e3907072c XL/fs: Initialize export paths supplied on command line (#2020)
Fixes #2013
2016-06-29 03:13:44 -07:00
karthic rao 8e8f6f90a4 adding detailed comments to server_test (#2032) 2016-06-29 02:30:36 -07:00
Bala FA 4c1a11aae6 XL: allow meta bucket name appended with tmp meta prefix. (#2007) 2016-06-29 02:28:46 -07:00
Harshavardhana ae936a0147 XL: Relax write quorum further to N/2 + 1. (#2018)
This changes behavior in some parts of the code
as well address it.

Fixes #2016
2016-06-29 02:10:40 -07:00
Harshavardhana d484157d67 XL/bitrot: Migrate to new blake2b-simd SIMD optimized implementation. (#2031)
Thanks for Frank Wessels <fwessels@xs4all.nl> for all the heavy lifting work.

Comparative benchmarks are as below.
```
benchmark                old ns/op     new ns/op     delta
BenchmarkHash64-4        742           411           -44.61%
BenchmarkHash128-4       681           346           -49.19%
BenchmarkWrite1K-4       4239          1497          -64.69%
BenchmarkWrite8K-4       33633         11514         -65.77%
BenchmarkWrite32K-4      134091        45947         -65.73%
BenchmarkWrite128K-4     537976        183643        -65.86%

benchmark                old MB/s     new MB/s     speedup
BenchmarkHash64-4        86.18        155.51       1.80x
BenchmarkHash128-4       187.96       369.10       1.96x
BenchmarkWrite1K-4       241.55       683.87       2.83x
BenchmarkWrite8K-4       3897.06      11383.41     2.92x
BenchmarkWrite32K-4      977.48       2852.63      2.92x
BenchmarkWrite128K-4     243.64       713.73       2.93x
```

Fixes #2030
2016-06-29 02:06:35 -07:00
Harshavardhana 796fe165c7 server: Minor command line doc change for XL. (#2022) 2016-06-29 02:05:57 -07:00
Harshavardhana 3ac39ff107 XL: Change minimum disks supported to 6 now. (#2023)
This change co-incides with another patch set which
reduces the writeQuorum requirement. With the
write quorum change it is now possible to support
6 disk configuration.
2016-06-29 02:05:29 -07:00
Krishnan Parthasarathi b6b9e88e47 Added unit-tests for treeWalkPool (#1969)
* Added unit-tests for treeWalkPool
* Added unit tests for tree-walk-fs
* Added period at the end of all comments.
* FS/XL: Unified tree walk tests for both backends
* Added disk failure related tests for treewalk

Replaced removeRandomDisks with removeDiskN. There is no need to
randomize disks that fail while the distribution of chunks in XL during
erasure coding data is random.
2016-06-28 22:32:00 -07:00
karthic rao 59366d8f4c Benchmarks for ObjectLayer.PutObject() (#2029) 2016-06-28 22:12:36 -07:00
Harshavardhana 748dc80047 API: add writePartTooSmallErrorResponse to extend standard error responses. (#2005)
This function is added to extend the standard error responses.
Which is needed in some cases for example CompleteMultipartUpload
should respond with ErrPartTooSmall error when parts uploaded are
lesser than 5MB (i.e minimum allowed size per part).

Fixes #1536
2016-06-28 14:51:49 -07:00
karthic rao 6dcfa7b046 Fix for tests leaving out temp directories (#2025) 2016-06-28 04:21:52 -07:00
Krishnan Parthasarathi a854e8cc5c api: Sent ErrPreconditionFailed on If-Match failure (#2009)
* api: Sent ErrPreconditionFailed on If-Match failure

ref:
http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList

* tests: Added functional tests for GetObject w/ If-Match headers set

* tests: Used verifyError to simplify errorCode and description matching on error
2016-06-28 01:18:18 -07:00
karthic rao 76f6533f8d Adding detailed comments for server_xl_test. (#2011) 2016-06-27 23:54:56 -07:00
Harshavardhana 4db2b03312 XL: Rename objectN to part.N (#2019)
Fixes #2015
2016-06-27 21:42:33 -07:00
Krishna Srinivas 5291db60c6 XL/erasure: refactor erasureReadFile. Move parallelRead into a separate function. (#2008) 2016-06-27 13:24:55 -07:00
Harshavardhana 2e1f66c37d XL: Handle quorum situations properly for write operations. (#1986)
Adds two test cases one for

 - PutObject when write quorum is not available.
 - PutObjectPart when write quorum is not available.

Fixes #1951
2016-06-27 10:01:09 -07:00
Bala FA c88720ea2c XL/listObjects: Ignore entry if getObjectInfo() returns errFileNotFound (#2004)
Fixes #1956
2016-06-26 22:10:22 -07:00
karthic rao ce7d5eddbc Misspell warnings fix (#2001) 2016-06-26 22:05:48 -07:00
Harshavardhana 0d3a9c9438 XL: Add tests for checkSufficientDisks, storageInfo. (#1988) 2016-06-26 19:48:02 -07:00
Harshavardhana 293ba00249 posix: Re-do tests for readDir(). (#1996) 2016-06-26 19:31:53 -07:00
Krishnan Parthasarathi d0be09fdd3 object: checkETag compares quoted ETags properly (#1997)
Previously, checkETag didn't handle ETags with leading and trailing
double quotes. e.g "abcdef1234" == "\"abcdef1234\"" would return false.
Now, checkETag function canonicalizes the ETags passed as arguments by
removing one leading/trailing double quote.
2016-06-26 18:10:08 -07:00
Remco Verhoef 1e52759c3c fix typo (#1987) 2016-06-26 11:27:04 -07:00
Harshavardhana 9add048f3c erasure: Add erasure encode/decode unit tests. (#1911)
Fixes #1819
2016-06-26 03:32:49 -07:00
Harshavardhana 936a916e78 server: Add connection rate limiter for server. (#1977) 2016-06-26 03:18:07 -07:00
Harshavardhana 57146fbbb8 main: minio --help should print regardless of root. (#1985)
Remove root check entirely.

Fixes #1964
2016-06-26 03:03:52 -07:00
karthic rao 3d02f7471e Benchmarks for various object sizes for FS/XL GetObject (#1984) 2016-06-25 20:22:04 -07:00
karthic rao b2d9a46cbb Cleaning up/refactoring tests to be more extensible (#1970) 2016-06-25 19:07:44 -07:00
Harshavardhana 42286cba70 XL: Implement new ReadAll API for files which are read in single call. (#1974)
Add a unit test as well.
2016-06-25 14:51:06 -07:00
karthic rao ed2fdd90b0 fs: Fix GetObject failure to read large blocks. (#1982)
Add relevant test cases as well for verifying this
part of the codebase.

Fixes #1979
2016-06-25 03:03:27 -07:00
Krishna Srinivas cb1200a66d XL/erasure-read: read disks in parallel. (#1975)
On read failure, fallback to reads from other
disks also happen in parallel.
2016-06-24 18:02:10 -07:00
Krishnan Parthasarathi a3a310cde8 Moved tree-walk-fs to use tree-walk-pool (#1978) 2016-06-24 16:41:57 -07:00
Bala FA f625392211 tests: add unit test for posix.readDir() (#1967)
Fixes #1820
2016-06-24 14:32:08 -07:00
Harshavardhana e8990e42c2 XL: Make allocations simpler avoid redundant allocs. (#1961)
- Reduce 10MiB buffers for loopy calls to use 128KiB.
- start using 128KiB buffer where needed.
2016-06-24 02:06:23 -07:00
Harshavardhana ff9fc22c72 posix: Mkdir() and OpenFile() should honor umask. (#1972)
Adds two unit tests for validation as well.

Fixes #1965
2016-06-23 20:19:27 -07:00
Harshavardhana 41c089a7e0 XL: Add mis-aligned GetObject() test. (#1960) 2016-06-22 21:42:24 -07:00
Harshavardhana 5725f3c809 Merge pull request #1958 from krisis/unittest/unc-path
Added unit tests for UNC path handling in windows
2016-06-22 17:19:40 -07:00
Harshavardhana f4830162a4 XL: Format heal should re-allocate new UUIDs not reuse. (#1953)
This patch also supports writing to a temporary file and renaming
rather than appending to an existing file. This helps in avoiding
inconsistent files.
2016-06-22 17:18:31 -07:00
Harshavardhana e10934a88e bitrot: Start using blake2b algorithm and remove sha512 usage. (#1957)
Fixes #1952
2016-06-22 17:13:26 -07:00
Krishnan Parthasarathi a07751f61b Added tests to validate disk name length near MAX_PATH 2016-06-22 15:30:41 -07:00
Krishnan Parthasarathi 0766e903e3 Added unit tests for UNC path handling in windows 2016-06-22 15:30:41 -07:00
Harshavardhana 75dddfb2ae Merge pull request #1959 from krishnasrinivas/parallel-reads
Parallel reads in erasure-read
2016-06-22 15:05:35 -07:00
Harshavardhana 9b82e64a11 XL/erasure-read: Avoid memory copy, write to writer directly all the dataBlocks. 2016-06-23 02:06:57 +05:30
Harshavardhana bdf8738076 lock: Add unit tests for namespace lock (#1922)
Fixes #1821
2016-06-22 12:27:47 -07:00
Krishna Srinivas d4bea5fbf8 XL/erasure-read: Add Comments and enable bitrot detection. 2016-06-23 00:34:47 +05:30
Krishna Srinivas 17efaaa902 XL/erasure-read: Support parallel reads from disks. 2016-06-23 00:34:47 +05:30
Krishnan Parthasarathi 78ae696749 Added couple of unit-tests to xlObjects (#1950)
* Added couple of units to xlObjects

* Used test_utils for initialize/destroying xlObjects
2016-06-21 15:48:27 -07:00
Harshavardhana 3fa95f5263 docker: Remove unneeded docker files and makefile tags. 2016-06-21 15:31:30 -07:00
karthic rao ba5bb4a127 TestServer introduction and revamp of functional tests. (#1940)
Allows for easy creation of Test server with temp backend.

changes
2016-06-21 12:10:18 -07:00
Harshavardhana 409b4ddecb api: MakeBucket should set proper bucket location. (#1948)
Fixes #1942
2016-06-20 23:25:18 -07:00
Harshavardhana ad779a8ba4 XL: Enable tests for large GetObject. (#1947)
Ref #1946
2016-06-20 22:08:36 -07:00
Bala FA 7d757033f2 erasure-readfile: Use chunk size to read from each disk for a block. (#1949)
A block of data is split into data chunk and each data chunk is
written to each disk.  Previously block size was used to read data
chunk which returns corrupted data.

This patch fixes the issue by reading chunk sized data from each disk
and assembles a block.

Fixes #1939
2016-06-20 21:40:10 -07:00
Krishnan Parthasarathi 393c504de0 Renaming a part from tmp namespace needs to be handled different from… (#1944)
* Renaming a part from tmp namespace needs to be handled different from renaming of an object

* Made argument passing in xl.rename and xl.undoRename explicit
2016-06-20 19:11:55 -07:00
Krishnan Parthasarathi 6143c87c3a Make ioErrCount updates go-routine safe (#1943)
* Make ioErrCount updates go-routine safe

* Made ioErrCount int32 instead of *int32

... and implemented StorageAPI on *posix as opposed to posix type.
This is consistent with the thumb-rule that if a value of a type is
modified as part of the interface implementation then we implement the
interface on pointer to that type.
2016-06-20 16:57:14 -07:00
Bala FA 8559e89494 XL: fix getBlockInfo() to return correct end block (#1941)
If requested offset/length of an object is equal to
erasureInfo.BlockSize, getBlockInfo() returns one more block added to
actual end block.  This patch fixes the issue.

This patch also adds unit test for get objects with big files.
2016-06-20 14:23:25 -07:00
Krishna Srinivas 468ca4ccda XL/Unittest: Add testcase for xlMetaV1{} and its methods. (#1938)
fixes #1822
2016-06-20 07:35:41 -07:00
Aakash Muttineni 4ee2136b28 Unit tests for PUT object when object already exists (#1904)
* fs/xl tests for multiple put object requests
* xl fix for put object on directory
* Unit tests fix windows test issue.
2016-06-20 06:18:47 -07:00
Yurii Rashkovskii 80d83220ad INSTALLGO.md mentions Go 1.5+ for OS X (#1936)
However, current requirement is 1.6, so the file has been updated to reflect that.
2016-06-20 06:17:57 -07:00
Bala FA fb10c09da7 posix-utils: remove unused isValidPath() (#1937) 2016-06-20 06:17:36 -07:00
Bala FA 2f136e92f7 posix: cleanup usage of fmt.Println() (#1934) 2016-06-19 18:52:19 -07:00
Harshavardhana 50d25ca94a XL: Change AppendFile() to return only error (#1932)
AppendFile ensures that it appends the entire buffer. Returns
an error otherwise, this patch removes the necessity for the
caller to look for 'n' return on short writes.

Ref #1893
2016-06-19 15:31:13 -07:00
Harshavardhana e1aad066c6 XL: CompleteMultipart should ignore last part is 0bytes. (#1931)
Fixes #1917
2016-06-19 14:51:20 -07:00
Bala FA 1ea1dba528 erasure-readfile: write to given Writer than returning buffer. (#1910)
Fixes #1889
2016-06-19 13:35:26 -07:00
Krishna Srinivas c41bf26712 Unit tests: add unit tests for listv1/v2 for list bucket handler. (#1933)
fixes #1818
2016-06-19 13:33:00 -07:00
Harshavardhana 8c0942bf0d XL: Remove usage of reduceErr and make it isQuorum verification. (#1909)
Fixes #1908
2016-06-18 00:27:51 +05:30
Harshavardhana 7f38f46e20 vendor: update klauspost/reedsomon package with upstream changes. (#1912) 2016-06-17 15:16:26 +05:30
Krishna Srinivas 466a2e01f1 XL/Erasure: Blocksize for object-part should be derived from what was decided during xl.NewMultipartUpload which creates xl.json. (#1920)
fixes #1919
2016-06-17 12:47:15 +05:30
Krishna Srinivas d31b38aac8 XL/GetObject: pick the xl.json with highest version for metadata information. (#1914)
fixes #1913
2016-06-17 10:56:18 +05:30
Krishna Srinivas 365f80efa3 XL/DeleteObject: delete call on a prefix should not delete the entire tree structure. (#1916)
fixes #1915
2016-06-17 10:48:43 +05:30
Anand Babu (AB) Periasamy f51d34cedd Do not guess content-type for objects with no extension (#1918) 2016-06-17 10:12:02 +05:30
Krishnan Parthasarathi 129ebbd685 object layer: Send 200 OK and whitespace chars (#1897) 2016-06-16 09:01:06 +05:30
Krishna Srinivas e2743d05e8 FS: remove .minio directory if .minio/multipart is empty. (#1899)
fixes #1886
2016-06-16 08:50:38 +05:30
Krishna Srinivas de1c7d33eb XL: appendFile should return error if quorum is not met. (#1898)
Fixes #1890
2016-06-15 00:24:49 +05:30
karthic rao afc3102488 Adding format.json during FS initialization (#1896) 2016-06-14 14:09:40 +05:30
Harshavardhana 23c88ffb1d XL/format: Fix a bug in checkDisksConsistency. (#1894) 2016-06-14 01:12:15 -07:00
Harshavardhana ed4fe689b4 posix: Support UNC paths on windows. (#1887)
This allows us to now use 32K paths names on windows.

Fixes #1620
2016-06-13 02:53:09 -07:00
Harshavardhana 4ab57f7d60 server: terminal width should fallback to 80x25. (#1895)
Some environments might disable access to `/dev/tty`, fall
back to '80' in such scenarios.

Move to 'cheggaaa/pb' package for better cross platform
support on fetching terminal width.

Fixes #1891
2016-06-12 19:35:28 -07:00
karthic rao 276282957e Test for Complete Multipart Upload. (#1888) 2016-06-10 18:43:16 +05:30
Harshavardhana 71632b375e docs: Add comments for each data types. (#1881) 2016-06-09 06:24:11 -07:00
Aakash Muttineni 6f3bd76754 api: Add new bucket policy nesting error (#1883)
* Added ErrPolicyNesting which is returned when nesting of policies has occured
* Replaces ErrMalformedPolicy in the case of nesting
* Changed test case in bucket-policy-parser_test.go (ErrMalformedPolicy -> ErrPolicyNesting)
2016-06-09 01:53:56 -07:00
Bala FA f2765d98a8 XL: set write quorum (no. of disk / 2) + 2 (#1876)
Previously write quorum was set to (no. of disk / 2) + 3.  As per new
change, the write quorum is set to (no. of disk / 2) + 2.  This helps
to accommodate one more failure of disk.
2016-06-08 22:12:36 -07:00
Bala FA 61598ed02f posix: return errFaultyDisk on I/O errors. (#1885)
When I/O error is occured more than allowed limit, posix returns
errFaultyDisk.

Fixes #1884
2016-06-08 22:02:10 -07:00
Krishna Srinivas 1b9db9ee6c FS/PutObject: Read() data should be handled even in case of EOF. (#1864)
Fixes #1710
2016-06-08 22:00:31 -07:00
Harshavardhana 51f3d4e0ca XL/multipart: statPart should ignore errDiskNotFound. (#1862)
startPart should also take uploadId and partName as arguments.
2016-06-07 18:15:04 -07:00
Bala FA d13e6e7156 XL: return error if DeleteObject() fails. (#1878)
Previously DeleteObject() does not return any error if write quorum is
not available.  This patch fixes the issue by returning errors.
2016-06-07 11:35:03 -07:00
Bala FA d32f3288f8 XL: return false only if given prefix doesn't exist in all disks (#1877)
Previously xl.isObject() returns false if one of the disk doesn't have
the object.  Its possible that object may be present in another disk.

This patch fixes the issue by returning false only if given prefix
doesn't exist in all disks.

Fixes #1855
2016-06-07 11:02:12 -07:00
Harshavardhana c5b6cb2420 Merge pull request #1867 from balamurugana/devel
cleanup: remove unused waitCloser.
2016-06-06 20:32:33 -07:00
Bala.FA 2eb6fa3fce cleanup: remove unused waitCloser. 2016-06-07 07:38:18 +05:30
Krishna Srinivas acc393ba8b XL/tree-walk: Added comments, changed variable names and structure fields to improve code readability. (#1856) 2016-06-05 11:55:45 -07:00
Harshavardhana 37551a2ad3 Merge pull request #1857 from harshavardhana/erasure
erasure: Fix block index matching.
2016-06-05 09:35:31 -07:00
Harshavardhana c6ac3fa6db erasure: Fix block index matching.
This patch fixes an important issue of block reconstruction upon block corruption.
2016-06-05 06:19:58 -07:00
Harshavardhana 18b3871705 Add erasure code. 2016-06-03 12:50:36 -07:00
Harshavardhana 73ddb5be75 Merge pull request #1850 from harshavardhana/list-rewrite
XL: Implement ListObjects channel and pool management.
2016-06-03 12:30:06 -07:00
Krishna Srinivas 002c5bf7dd XL: Treewalk handle all the race conditions and blocking channels. 2016-06-03 12:17:54 -07:00
Harshavardhana 1cf1532ca3 XL: Implement ListObjects channel and pool management. 2016-06-03 12:17:54 -07:00
Harshavardhana 70a1231f02 Merge pull request #1849 from harshavardhana/multipart
XL/PutObject: Handle all pending cases of DiskNotFound.
2016-06-03 11:58:04 -07:00
Harshavardhana 82fd907933 XL/PutObject: Handle all pending cases of DiskNotFound. 2016-06-03 11:40:44 -07:00
Harshavardhana f39a6f8df7 Merge pull request #1852 from minio/harshavardhana-patch-1
Fix download link
2016-06-03 01:17:54 -07:00
Harshavardhana f6013c46ea Fix download link 2016-06-03 00:05:32 -07:00
Harshavardhana da069a18c4 Merge pull request #1847 from krisis/patch-1
Created ISSUE_TEMPLATE with basic information
2016-06-03 00:01:48 -07:00
Harshavardhana 5108ba6eb1 Merge pull request #1728 from minio/rewrite-xl
XL/FS: Rewrite in new format.
2016-06-02 23:19:17 -07:00
Krishnan Parthasarathi 1213bf9fa1 Created ISSUE_TEMPLATE with basic information
Added an issued template to gather as much useful information that we may need to resolve an issue while the user has access to the relevant details.
2016-06-03 10:31:08 +05:30
Krishna Srinivas b00ac40c35 XL/PutObject: Calculate size if not provided by the client and update xl.json with the correct size. (#1844) 2016-06-02 17:09:47 -07:00
Harshavardhana fb95c1fad3 XL: Bring in some modularity into format verification and healing. (#1832) 2016-06-02 16:34:15 -07:00
Krishna Srinivas aa1d769b1e FS/Multipart: remove uploads.json on complete-multipart if no more uploadIDs are present for the object. (#1843)
Fixes #1835
2016-06-02 15:54:00 -07:00
Krishna Srinivas 611c892f8f FS/Multipart: Lock() to avoid race during PutObjectPart. (#1842)
Fixes #1839
2016-06-02 15:19:13 -07:00
Harshavardhana 67bba270a0 FS: Cleanup and Fix all multipart related operations. (#1836) 2016-06-02 12:18:56 -07:00
Harshavardhana de21126f7e XL: Re-align the code again. 2016-06-02 01:54:06 -07:00
Harshavardhana ae311aa53b XL: Cleanup, comments and all the updated functions. (#1830) 2016-06-01 16:43:31 -07:00
Krishna Srinivas 9b79760dcf XL/heal: heal missing format.json on replaced drives. (#1828)
fixes #1817
2016-06-01 16:15:56 -07:00
Bala FA 116b5607d7 server: fix to have readable timeout value (#1823) 2016-06-01 09:14:50 -07:00
Krishna Srinivas 614c770b5d List Objects version 2. (#1815)
object: List Objects v2 support
2016-05-31 22:10:55 -07:00
Harshavardhana c493ab5d0d XL: Bring in sha512 checksum support. (#1797) 2016-05-31 20:23:31 -07:00
Bala FA db2fdbf38d erasure: allocate buffer only for non-nil disk (#1811) 2016-05-31 11:55:50 -07:00
Krishna Srinivas 89f65333fb XL/Multipart: Introduce "deleted" field for uploads.json (#1810)
To future proof backend in case #1805 becomes an issue.
2016-05-31 11:54:01 -07:00
Krishna Srinivas 22511dc4c7 XL/Multipart: During list-multipart-uploads ignore errFileNotFound and errDiskNotFound errors. (#1813)
Fixes #1795
2016-05-31 11:53:28 -07:00
karthic rao 1947ae198e Adding read nad write timeout for unresponsive client connectinos (#1809) 2016-05-31 11:53:21 -07:00
Harshavardhana 2e4ab71303 Web: Update with ui changes. (#1808) 2016-05-31 02:01:02 -07:00
Harshavardhana 445dc22118 XL: Cleanup and add more comments. (#1807) 2016-05-30 16:51:59 -07:00
karthic rao ffc2b3c304 Test for ListObjectParts. (#1802) 2016-05-30 14:36:33 -07:00
Krishnan Parthasarathi 967c2b2940 Handled possible short writes to httpResponseWriter (#1804)
* XL: Handled possible short writes to httpResponseWriter

* Added tests for Range Header combinations
2016-05-30 11:27:15 -07:00
Krishna Srinivas b466f27705 Nslock fixes (#1803)
* XL/Multipart: Support parallel upload of parts by doing NS locking appropriately.

* XL/Multipart: hold lock on the multipart upload while aborting.
2016-05-30 11:26:10 -07:00
Harshavardhana a4a0ea605b XL: Fix GetObject erasure decode issues. (#1793) 2016-05-29 15:38:14 -07:00
Harshavardhana 5e8de786b3 XL: Truly use unique id's in temp directory. (#1790)
This also helps in avoiding cleaning up directories after.

Additionally this patch also fixes the problem of Range offsets.
2016-05-29 00:42:09 -07:00
Harshavardhana feb337098d XL: bring in new storage API. (#1780)
Fixes #1771
2016-05-28 16:12:51 -07:00
Krishnan Parthasarathi c87f259820 Remove parts that are missing in CompleteMultipartUpload (#1786)
* Remove parts that are missing in CompleteMultipartUpload

* Moved isUploadIDExists under proper namespace locks

* Moved code that deletes part files to a function
2016-05-28 15:15:53 -07:00
karthic rao 7278b90fe1 Adding defer to the lock (#1785) 2016-05-28 15:15:53 -07:00
Krishna Srinivas 41a5b3908b XL/ListParts: take the size from xl.json instead of backend file size as it will be different. (#1781)
Fixes #1779
2016-05-28 15:15:53 -07:00
Krishna Srinivas 3fb0b5e455 XL/Multipart: check existance upload uploadID after lock. (#1778)
Fixes #1767
2016-05-28 15:15:53 -07:00
Harshavardhana ba8bdec077 XL: ListObjects should not list when delimiter and prefix are '/'. (#1777) 2016-05-28 15:15:53 -07:00
Harshavardhana 27cc8a6529 erasure: read only dataBlocks if we have enough. (#1776)
Reconstruct with parity blocks if we don't have enough data blocks.
2016-05-28 15:15:53 -07:00
Krishnan Parthasarathi 302ec27fa2 Fixed race during parallel PutObjectPart requests (#1775)
The race is between two parallel PutObjectPart requests updating partsInfo in xl.json.
Previously, it was being updated under a RLock().
2016-05-28 15:15:53 -07:00
Krishnan Parthasarathi 5f679d9d1e Rename back multipart objects if read/write Quorum was unavailable (#1773) 2016-05-28 15:15:53 -07:00
Bala FA 51bb613fdf pkg/safe: remove temporary file on failure (#1774) 2016-05-28 15:15:53 -07:00
Harshavardhana d65101a8c8 XL: Implement strided erasure distribution. (#1772)
Strided erasure distribution uses a new randomized
block distribution for each Put operation. This
information is captured inside `xl.json` for subsequent
Get operations.
2016-05-28 15:15:53 -07:00
Krishna Srinivas 6dc8323684 FS/ListMultipart: Fix FS list-multipart to work for unit test cases. 2016-05-28 15:15:53 -07:00
Krishna Srinivas 616a257bfa XL/Multipart: isMultipartUpload() checks for presence of uploads.json on a random disk. 2016-05-28 15:15:53 -07:00
Krishna Srinivas 3487b3c095 Multipart: Disable FS tests and certain test cases for list-incomplete-uploads. 2016-05-28 15:15:53 -07:00
Karthic Rao 1f51af6f37 Listmultipart tests. 2016-05-28 15:15:53 -07:00
Krishna Srinivas b1e2b7dea2 Fix list-incomplete uploads for XL. 2016-05-28 15:15:53 -07:00
Harshavardhana 34e9ad24aa XL: Introduce new API StorageInfo. (#1770)
This is necessary for calculating the total storage
capacity from object layer. This value is also needed for
browser UI.

Buckets used to carry this information, this patch
deprecates this feature.
2016-05-28 15:15:53 -07:00
Harshavardhana b2293c2bf4 XL: Rename, cleanup and add more comments. (#1769)
- xl-v1-bucket.go - removes a whole bunch of code.
- {xl-v1,fs-v1}-metadata.go - add a lot of comments and rename functions
   appropriately.
2016-05-28 15:15:53 -07:00
Harshavardhana 553fdb9211 XL: Bring in support for object versions written during writeQuorum. (#1762)
Erasure is initialized as needed depending on the quorum and onlineDisks.
This way we can manage the quorum at the object layer.
2016-05-28 15:15:53 -07:00
Harshavardhana cae4782973 XL: explicit deleteObject is not needed after rename failure. (#1760)
Reason is renameObject() does deleteObject() upon writeQuorum
failure if not keeps the successfully renamed parts if we have
reached readQuorum.
2016-05-28 15:15:53 -07:00
Krishnan Parthasarathi 3550660163 Return error for empty parts in multipartupload complete (#1758) 2016-05-28 15:15:53 -07:00
Harshavardhana a4771265cf XL: Abortmultipart should update uploads.json properly. (#1757) 2016-05-28 15:15:53 -07:00
Harshavardhana a9e778f460 XL/fs: initObjectLayer should cleanup tmpMetaPrefix in parallel. (#1752)
Fixes #1747
2016-05-28 15:15:53 -07:00
Harshavardhana ee6645f421 XL: Add additional PartNumber variable as part of xl.json (#1750)
This is needed for verification of incoming parts and to
support variadic part uploads. Which should be sorted
properly.

Fixes #1740
2016-05-28 15:15:53 -07:00
Harshavardhana a97230dd56 XL/erasure: Reset dataBlocks to reduce the memory usage. (#1749)
Fixes #1748
2016-05-28 15:15:53 -07:00
Harshavardhana 1e393c6c5b XL: Add new metadata for checksum. (#1743) 2016-05-28 15:15:53 -07:00
Krishna Srinivas b38b9fea79 XL/erasure: fix for skipping 0 padding. (#1737)
Fixes #1736
2016-05-28 15:15:53 -07:00
Krishna Srinivas 6d84e84b3c XL/mutltipart: fix partnumber to partname association. (#1739)
Fixes #1738
2016-05-28 15:15:53 -07:00
Harshavardhana a00a5c6e7e XL: Multipart update uploads.json properly. (#1741) 2016-05-28 15:15:53 -07:00
Harshavardhana ed43d5e02b No need to delete file inside erasure code (#1732) 2016-05-28 15:15:53 -07:00
Harshavardhana 293d246f95 XL/FS: Rewrite in new format. 2016-05-28 15:15:53 -07:00
Anand Babu (AB) Periasamy 63c65b4635 filter GOPATH from stack trace (#1755) 2016-05-25 02:32:35 -07:00
Harshavardhana 64b0976e1b Remove probe and tasker. (#1733)
Fixes #1717
2016-05-24 18:43:33 -07:00
Aakash Muttineni b48b2e7f7c Part ID check (#1730)
* Added check in PutObjectPartHandler to make sure part ID does not exceed 10000. ErrInvalidMaxParts written to response if part ID exceeds the maximum value.
2016-05-24 01:52:47 -07:00
Krishnan Parthasarathi 584813e214 Used MINIO_PROFILE_DIR for saving profile information of a minio server (#1722)
To specify the directory where profiling information should be saved
  ```
    export MINIO_PROFILE_DIR=/path/to/profile/dir
  ```
By default, profiling information would be saved in a directory created
using ioutil.TempDir, which would be displayed in stdout on starting the
minio server.
2016-05-22 22:11:39 -07:00
Krishnan Parthasarathi 3a980eac1a Fix shadowing of err variable (#1718) 2016-05-21 00:43:47 -07:00
Harshavardhana 5a4b074ca0 XL: PutObject incorrectly returned after deleting multipart object. (#1715)
Fixes #1714
2016-05-20 17:27:48 -07:00
Harshavardhana f76d975304 xl: StatVol and ListVols should handle cases when disks are missing. (#1703)
Remove unnecessary code, handle cases of missing disk.
2016-05-20 16:45:53 -07:00
Harshavardhana 6015a7a3cd Add mention-bot config 2016-05-20 13:53:15 -07:00
Krishna Srinivas b83b87a7f6 XL/Incompleteuploads: list should save the tree-walk go routine to the map if eof is not reached. (#1695) 2016-05-20 12:09:21 -07:00
Krishna Srinivas 5b95f097d4 multipart: listing does not skip uploadIDmarker. (#1708)
Fixes #1706
2016-05-20 11:48:28 -07:00
Harshavardhana e4240aa58f XL/objects: Initialize format.json outside of erasure. (#1640)
Fixes #1636

New format now generates a UUID and includes it along with
the order of disks. So that UUID is the real order of disks
and on command line user is able to specify disks in any order.

This pre-dominantly solves our dilemma.
```
{
   "format" : "xl",
   "xl" : {
      "version" : "1",
      "disk": "00e4cf06-5bf5-4bb5-b885-4b2fff4a7959",
      "jbod" : [
         "00e4cf06-5bf5-4bb5-b885-4b2fff4a7959",
         ....
         "c47d2608-5067-4ed7-b1e4-fb81bdbb549f",
         "a543293e-99f1-4310-b540-1e450878e844",
         "18f97cbe-529a-456a-b6d4-0feacf64534d"
      ]
   },
   "version" : "1"
}
```
2016-05-20 02:22:22 -07:00
Andreas Linz f5dfa895a5 Exit with error code if minio server fails to start (#1704)
This commit replaces the call to `errorIf` with `fatalIf`, so that the
minio server exits with an non-zero exit status if something fails, e.g.
the port was already openend by another process.
2016-05-20 02:05:52 -07:00
Harshavardhana 50c328ff19 XL: RenameFile should rename and cleanup on writeQuorum. (#1702)
Fixes #1683
2016-05-20 01:56:46 -07:00
Anand Babu (AB) Periasamy b8405ca172 simplify profiler cleanup 2016-05-19 19:19:32 -07:00
Harshavardhana f6d9e73548 posix: Do not lowercase names, return as is. Object layer will filter them out. (#1699) 2016-05-19 18:52:55 -07:00
Harshavardhana 7ae5470395 XL: simplify isMultipartObject not need to handle unknown errors. (#1686)
Unknown errors are just logged with errorIf.
2016-05-19 17:10:33 -07:00
Harshavardhana 9fdb69563d handler: CopyObject should save metadata. (#1698)
- Content-Type
- Content-Encoding
- ETag

Fixes #1682
2016-05-19 17:10:08 -07:00
Harshavardhana 00d0558131 XL: Enable tests for content-type for Head and Get
Fixes #1674
2016-05-19 15:34:18 -07:00
Bala.FA 13e4618309 XL/fs: Return saved content-type during GetObject
Fixes #1674
2016-05-19 15:34:18 -07:00
Krishnan Parthasarathi 2f05aacbf2 Stop profiling on exit of main goroutine (#1670)
* Stop profiling on exit of main goroutine

Previously, profiling was stopped since Stop() method was called on exit of cli.BeforeFunc.
This lead to profiling to be stopped prematurely.

* Moved profiling switch statement to a separate func
2016-05-19 14:50:54 -07:00
Krishna Srinivas dc36594ef4 XL/heal: Should skip healing if CreateFile() failed on the part which needed healing. (#1693)
Fixes #1684
2016-05-19 11:32:47 -07:00
Krishna Srinivas 537568f9f9 XL/ListVols: Fix panic. Skip if slice is nil. (#1694)
Fixes #1692
2016-05-19 11:32:19 -07:00
Harshavardhana 62b4fd6964 XL: Close the reader properly. 2016-05-18 20:16:19 -07:00
Harshavardhana 7d6ed50fc2 objects: Save all the incoming metadata properly. (#1688)
For both multipart and single put operation
2016-05-18 19:54:25 -07:00
Frank af85acf388 Added ETag as an exposed header (required for multi part uploads) (#1681)
* Added ETag as an exposed header (required for multi part uploads)

* Fix formatting on adding ETag for exposed headers
2016-05-18 19:17:32 -07:00
Frank a4fef436c8 Fix formatting for adding ETag for exposed headers (#1687) 2016-05-18 19:17:17 -07:00
Harshavardhana 404364ba73 XL/fs: ListMultipartUploads should list only requested entries. (#1668)
Fixes #1665
2016-05-18 15:06:29 -07:00
Krishna Srinivas 3c1ef3fee2 Locking: move locking code from xl-erasure to xl-objects. (#1673)
Fixes #1639 #1649 #1650 #1624
2016-05-18 15:05:23 -07:00
Bala FA a0865122a7 XL/objectLayer: Save additional meta data during PutObject (#1672)
Fixes  #1602
2016-05-18 13:56:11 -07:00
Krishna Srinivas 824c8a39f1 XL/Multipart: If the part is already uploaded ignore the newly uploaded part. (#1677)
Fixes #1644
2016-05-18 13:37:28 -07:00
Krishna Srinivas 71b9341fc7 XL/Multipart: Cleanup uploads.json after abort-multipart-upload. (#1678)
Fixes #1663
2016-05-18 13:30:58 -07:00
Harshavardhana b69a97aed4 server: Set rLimit properly to the max. (#1676)
4000 is too small to handle 500 go-routines.

Fixes #1666
2016-05-18 11:34:24 -07:00
karthic rao 2da34e4668 vendor changes to pkg/profile (#1671) 2016-05-18 09:22:06 -07:00
Harshavardhana 4bc923e63b XL/fs: Optimize calling isBucketExist() (#1656)
* posix: Avoid using getAllVolumeInfo() in getVolumeDir()

This is necessary compromise to avoid significant slowness this
causes under load. The compromise is also substantial in a way
so that to avoid penalizing common cases v/s special cases.

For buckets with Caps on Unixes, we filter buckets based on the
latest anyways, so this is completely acceptable.

* XL/fs: Change the usage of verification of existance of buckets.

Optimize calling isBucketExists, it is not needed for all call
paths. isBucketExist should be called only for calls which use
temporary volume location for operations, for the rest rely on
the errors returned on their original call path.

Remove usage of filtering as well across all volume names.
2016-05-17 21:22:27 -07:00
Harshavardhana 4214da65af XL/fs: MakeVol replies should be consistent. (#1667)
Fixes #1658
Fixes #1633
2016-05-17 18:41:17 -07:00
Krishnan Parthasarathi 596fe65e84 Write pprof output files under config dir supplied (#1660)
Since config dir, supplied as command line argument, is parsed after pprof
output directory is determined, pprof output files are  written in ~/.minio/profile
directory instead of <configDir>/profile/. This change fixes this behaviour.
2016-05-17 11:44:40 -07:00
Krishna Srinivas 39865c0d2e XL/Multipart: Fix list multipart output. (#1661)
Fixes #1541
2016-05-17 11:44:32 -07:00
Harshavardhana 1760687c83 XL: Make sure to create proper temporary files for renames to succeed. (#1654)
Renames work in a special manner, temporary location files should
be created properly.

Fixes #1653
Fixes #1651
2016-05-16 15:40:57 -07:00
Harshavardhana 9472299308 logging: Log only for unhandled errors, remove all the debug logging. (#1652)
This patch brings in the removal of debug logging altogether, instead
we bring in the functionality of being able to trace the errors properly
pointing back to the origination of the problem.

To enable tracing you need to enable "MINIO_TRACE" set to "1" or "true"
environment variable which would print back traces whenever there is an
error which is unhandled or at the handler layer.

By default this tracing is turned off and only user level logging is
provided.
2016-05-16 14:31:28 -07:00
Harshavardhana 8828fd1e5c vendor: Remove unused packages. 2016-05-15 00:04:56 -07:00
Harshavardhana 7de206cb85 XL: ListVols should provide consistent view. (#1648)
Additionally get list of all volumes in parallel for aggregation
and quorum verification.

Fixes #1647
2016-05-14 23:57:57 -07:00
Harshavardhana 498ce1e9bb handler: Add a waitgroup to avoid expect100Continue crash. (#1623)
This waitgroup allows for safe blocking operation where we can cleanly
control the flow of the writes and the underlying pipe altogether.

Fixes #1553
2016-05-14 17:18:00 -07:00
Harshavardhana 5b29cefd40 api: DeleteObject should always return 204. (#1645)
Fixes #1643
2016-05-14 15:47:19 -07:00
Hori Ryota e03ebfd13b Add default cmd (#1625) 2016-05-14 02:58:50 -07:00
Harshavardhana 74c23a3544 docs: Move developer docs from top-level to its own directory. (#1642) 2016-05-14 02:47:16 -07:00
Harshavardhana 025054fb36 XL: CreateFile/ReadFile should write and read from all disks in parallel. (#1612)
* XL: CreateFile should write to all disks in parallel.

* XL: ReadFile should read from all disks in parallel.
2016-05-14 01:57:04 -07:00
Bala FA 7264cd2ab3 Fix error message when wrong set of disks are passed (#1634)
Previously when wrong set of disks are given with last minio server
run, it throws unclear error message.  This is fixed by returning
appropriate errors.

Fixes #1591
2016-05-13 23:04:10 -07:00
Harshavardhana 0e4e9c4bc1 XL: ListDir should return each List from a random disk in the set. (#1613)
Fixes #1609
2016-05-13 18:12:26 -07:00
Krishna Srinivas 8099396ff0 xl/putObject: Should take care of the situation if an object already exists at the location. (#1606)
Fixes  #1598 #1594 #1595
2016-05-13 11:52:36 -07:00
Krishna Srinivas d267696110 Validation: Reject object names with trailing "/". (#1619)
Fixes #1616
2016-05-13 11:43:06 -07:00
Bala FA 43539a0c86 posix: parseDirents() should follow symlink and get values. (#1631)
Previously parseDirents() ignores symbolic links.  This patch fixes
the issue by following the symlink using os.Stat().

Fixes #1545
2016-05-13 11:39:48 -07:00
Krishnan Parthasarathi 9e45d138cc Closed readCloser for each multipart in xl.GetObject (#1629)
* Closed readCloser for each multipart in xl.GetObject
2016-05-13 04:50:13 -07:00
karthic rao ee8605e333 Make bucket failure fix with high concurrent load (#1630) 2016-05-13 04:03:38 -07:00
karthic rao e4958f9757 Removing regexp check and adding string based check, regexp check was unnecessary here (#1627) 2016-05-13 03:33:53 -07:00
Harshavardhana b62774d32f storage/xl: Return errVolumeAccessDenied if disks cannot be accessed. (#1621)
Fixes #1614
2016-05-12 21:01:11 -07:00
koolhead17 d6e0f3ab33 added awscli commands & minor typo fix. (#1578) 2016-05-12 16:35:11 -07:00
Bala FA 3ff0a56e62 XL: Ignore errDiskNotFound in certain situations (#1610)
When a disk is removed while an operation is going on
(eg. single/multipart put object, list/multipart list objects etc),
its required to ignore errDiskNotFound error and continue the
operation.

Fixes #1552
2016-05-11 23:42:14 -07:00
Harshavardhana 50431e91a6 erasure: Handle failed disks so that we initialize properly if they are missing. (#1607)
Fixes #1592
Fixes #1579
2016-05-11 18:58:32 -07:00
Harshavardhana d4745c7d6a object: PutObjectHandler should set the md5Sum properly. (#1604)
Additionally add a test case as well for validating for us
to reply BadDigest properly.

Fixes #1603
2016-05-11 16:13:37 -07:00
Bala FA adbcafefad xl/CreateFile: handle errFileNameTooLong error properly (#1523)
When errFileNameTooLong error is returned from posix, xl.CreateFile()
treats the error specially by returning the same error immediately.

Fixes #1501
2016-05-11 12:55:02 -07:00
Harshavardhana 86e5d71519 erasure: MakeVol, DeleteVol and StatVol should hold locks. (#1597)
Since there is a good amount of overlap, each code has to lock
properly for the operation they are going to perform.

- MakeVol create vols in a routine on all disks, hold locks.
- DeleteVol delete vols in a routine on all disks, hold locks.
- StatVol stat vols in a routine on all disks, hold locks.

Fixes #1588
2016-05-11 12:54:21 -07:00
Harshavardhana 72748d2073 erasure: healVolume err should be different from shadowed version. (#1590)
Multiple go-routines updating the same shadowed variable can
cause a data race, avoid it by using its own err variable.

Fixes #1589
2016-05-11 01:36:09 -07:00
Harshavardhana 49141eb3e4 http: Remove minhttp package and use standard Golang http. (#1587)
The functionality provided by minhttp will be implemented
cleanly through our own APIs. Since we are not going to
send SIGUSR2 and manage configuration in that manner, it
doesn't make sense to use minhttp.

Fixes #1586
2016-05-10 18:03:00 -07:00
Harshavardhana d1fa1d9352 Remove binary files from previous commit. 2016-05-10 15:49:17 -07:00
karthic rao 26e2c4bf4d Replacing fastsha256 with crypto/sha256 package from golang standard package (#1584) 2016-05-10 14:20:11 -07:00
Krishna Srinivas b044336329 XL/GetObject: If the offset does not fall in the first "dataBlock" it gives incorrect data. (#1583)
Fixes #1582
2016-05-10 11:38:49 -07:00
Krishna Srinivas e99cb05516 XL/GetObject: offset should be reset to 0 after reading first part. (#1580) (#1581) 2016-05-10 10:38:12 -07:00
Krishna Srinivas 409e09c1e5 XL/Selfheal: skip reading from disk if ReadFile had returned error. (#1575) 2016-05-10 01:24:58 -07:00
Krishna Srinivas c314a98c1a XL/list: fix panic on list when a disk is down. (#1562) 2016-05-10 00:35:29 -07:00
Harshavardhana 5f0ca64346 erasure: listOnlineDisks should return errFileNotFound for errReadQuorum. (#1573)
Fixes #1571
2016-05-10 00:10:34 -07:00
Harshavardhana 0db3218d5d xl: getPartsMetadata fetch parts and decode in go-routine. (#1569)
Ref #1516
2016-05-09 23:51:05 -07:00
Harshavardhana eec41c369c posix: Return diskNotFound error rather than errVolumeNotFound (#1568)
Fixes #1559
2016-05-09 18:57:39 -07:00
Harshavardhana b66c3bf35e server: Enable server profiling as needed. (#1565) 2016-05-09 16:18:56 -07:00
Harshavardhana f733120d3d xl: CompleteMultipartUpload make sure to delete uploads.json (#1539)
Fixes #1537

Ref #1540 - for missing functionality in this patch.
2016-05-09 12:09:48 -07:00
Krishna Srinivas 6627388dc3 posix: remove dead code related to posix reserved suffixes. (#1555) 2016-05-09 11:40:30 -07:00
Harshavardhana 9d41414fb5 posix: reserved files should be filtered out at posix not object layer. (#1554) 2016-05-09 02:53:08 -07:00
Harshavardhana 722abe2d0f xl/fs: pathJoin now takes variadic inputs. (#1550)
Retains slash for the last element.

Fixes #1546
2016-05-09 00:46:54 -07:00
Krishna Srinivas 04a5b25929 Multipart: Minimum part size limit does not apply to the last part during CompleteMultipartUpload. (#1518) (#1538) 2016-05-08 23:49:49 -07:00
Harshavardhana 90ea494338 erasure: waitCloser should implement CloseWithError. (#1543)
This is needed so that the other end of the pipe receives
and error, cleanups temporary files.
2016-05-08 16:26:10 -07:00
Harshavardhana a8fdd04e62 erasure: ReadFile should honor proper offsets. (#1542)
Fixes #1535
2016-05-08 15:39:24 -07:00
Harshavardhana 76c511c9fe api: Extend S3 errors with Minio errors. (#1533)
Fixes #1530
2016-05-08 12:36:16 -07:00
Krishna Srinivas 75320f70d0 multipart: reject part upload if size is less than 5MB. (#1518) 2016-05-08 12:06:05 -07:00
Krishna Srinivas 88e1c04259 XL/ListDir: break out of loop if list on one disk is a success. (#1534) 2016-05-08 12:05:19 -07:00
Krishna Srinivas a205aca6d2 init: Cleanup .minio/tmp directories recursively. Also takes care of cleaning up of parts directory during abortMultipartUpload. (#1532) 2016-05-08 10:15:34 -07:00
Harshavardhana 3f51dd4fd4 xl: CompleteMultipartUpload should rename files in a routine. (#1527)
This solves the client timeout while renaming 9000+ parts.

Fixes #1526
2016-05-08 02:38:35 -07:00
Harshavardhana 56b7df90e1 xl/fs: ListObjectParts should set nextPartNumberMarker properly. (#1528)
For list requests on parts more than 1000, would lead to an infinite
loop.

Fixes #1522
2016-05-08 02:21:12 -07:00
Harshavardhana a56d5ef415 xl/fs: isFunctions should only return boolean. (#1525)
log the unrecognize errors.
2016-05-08 01:58:05 -07:00
Harshavardhana 937d68202d server: Deadcode removal. (#1517) 2016-05-07 21:47:33 -07:00
Harshavardhana bf563afb80 xl: DeleteObject regression over errChannel. (#1521)
DeleteObject would hang indefinitely - fix it.
2016-05-07 12:48:12 -07:00
Harshavardhana 091c1e8456 copyObject: No need to verify md5sum. (#1520)
Multipart objects are kept in non hex md5sum format.
This format doesn't comply with hex, so decoding
would fail invariably.

This is not necessary to validate and its not expected
error during a CopyObject operation.

Fixes #1519
2016-05-07 03:43:08 -07:00
Harshavardhana 751fa972f5 xl/fs: Multipart re-org introduce "uploads.json" (#1505)
Fixes #1457
2016-05-07 02:08:03 -07:00
Harshavardhana 434423de89 xl: Move format detection inside xl objects. (#1515)
Fixes #1449
2016-05-07 00:59:43 -07:00
Harshavardhana a20ccb1e83 server: Print proper endpoint, along with https if configured. (#1514)
Fixes #1492
2016-05-06 21:18:29 -07:00
Harshavardhana 0625c050e6 xl/tests: Enable server handler tests over XL. (#1512)
Fixes #1513
2016-05-06 16:47:23 -07:00
Harshavardhana 0b74f5624e xl: Fix how we deal with read offsets at erasure layer. (#1511)
Requires skipping necessary parts of dataBlocks during
decoding phase and requires us to properly skip the
entries as needed.

Thanks to Karthic for reproducing this important issue.

Fixes #1503
2016-05-06 16:25:08 -07:00
Krishna Srinivas c06b9abc15 bucket-handlers: do not unescape marker as gorilla layer would have already done it. (#1495) (#1510) 2016-05-06 16:04:46 -07:00
Krishna Srinivas a5d31d4254 XL/ListObjects: use string.TrimSuffix instead of Trim. (#1498) (#1509) 2016-05-06 13:28:55 -07:00
karthic rao 20ca65c793 Cleanup: mispell fixes 2016-05-06 12:32:44 -07:00
karthic rao 0b4bbe6d9e Adding XL Object layer validation for existing unit tests of single node (#1499)
object layer.

Adding isBucketExist check for GetObjectInfo in the XL layer.
2016-05-06 11:57:04 -07:00
Krishna Srinivas 48d3be36da XL/ListObjects: Fix ordering issue during listing if the files were uploaded as multipart uploads. (#1498) (#1506)
i.e if two files "tmp" and "tmp.1" are uploaded as multipart we would list ""tmp.1"" before ""tmp"" as "tmp.1/" < "tmp/"
2016-05-06 10:19:09 -07:00
Harshavardhana 5133ea50bd xl/fs: Make i/o operations atomic. (#1496) 2016-05-05 20:28:22 -07:00
Harshavardhana 17868ccd7f handlers: overhaul entire writErrorResponse, simplify. (#1472) 2016-05-05 20:24:29 -07:00
Harshavardhana ba5805e60a bucketPolicy: Do not use regexes, just do prefix matches. (#1497)
AWS arn supports wildcards and this is flat namespace, simple
prefix matching is fine.

Fixes #1481
Fixes #1482
2016-05-05 19:58:48 -07:00
Harshavardhana ca097de96c xl/fs: Add initObjectLayer function. (#1494)
Fixes #1493
2016-05-05 15:00:03 -07:00
Bala FA 658a595d7a xl-erasure: RenameFile should support quorum. (#1487)
Fixes #1463
2016-05-05 13:03:53 -07:00
Krishna Srinivas 247e835d7b object: move go-routine listing from posix to objectLayer. (#1491) 2016-05-05 12:51:56 -07:00
Harshavardhana 46680788f9 xl/fs: cleanup '/.minio/tmp' directory on each initialization. (#1490) 2016-05-05 01:54:43 -07:00
Harshavardhana ad40036cba posix: filepath shouldn't be used anymore use path.Join (#1486) 2016-05-05 01:39:26 -07:00
Harshavardhana 82fbe908a3 object: DeleteBucket should return proper error for BucketNotEmpty. (#1489)
Fixes #1488
2016-05-05 01:35:21 -07:00
Harshavardhana f145e1042f quick/config: No need to use Data() with type assertion. (#1480)
Since input to quick.New() is a pointer the unmarshalled value
internally already has the value, subsequent type assertions
are not needed.

Thanks to Bala for finding this behavior.

Fixes #1475
2016-05-04 20:22:15 -07:00
Rajiv Makhijani 9dccbd6478 New Dockerfile for building & running minio inside Docker (inc. autobuild support) (#1473) (#1485) 2016-05-04 17:07:19 -07:00
karthic rao 82113b747c Resource matching fix to overcome issues with regular expression based match (#1476) 2016-05-04 16:56:57 -07:00
Rajiv Makhijani a5959789d5 Make minimum file space percent a constant (#1484) 2016-05-04 15:39:06 -07:00
Harshavardhana 6988ed9257 xl/getObjectInfo: Returns back proper size, modTime and md5Sum. (#1479)
Fixes #1469
2016-05-04 15:28:58 -07:00
Rajiv Makhijani 321aefa026 Add Response for PostPolicyBucketHandler (#1477) (#1483) 2016-05-04 15:24:10 -07:00
Harshavardhana dd417e5476 fs: Handle cases of PutObject for an existing prefix. (#1478) 2016-05-04 12:18:40 -07:00
Bala FA da3a53376c server: save and compare multiple disks are used (#1474)
When server is run with multiple disks which uses xl interface where
order and count of disks are important, this patch saves such disks
configuration and compares in next run if there is a mismatch.

Fixes #1458
2016-05-04 12:18:20 -07:00
Harshavardhana e4d89d8156 xl/deleteObject: Support deleting special multipart object. (#1470)
Fixes #1452
2016-05-03 17:52:54 -07:00
Harshavardhana d0e854afb7 xl/fs: Bring in ".minio/tmp" directory support. (#1464)
All transactions happen through this directory inside ".minio/temp".
Adding this allows us to remove any temporary files which were not
committed before.

Fixes #1462
Fixes #1444
2016-05-03 16:10:24 -07:00
Harshavardhana 6f1811ee4d config: Migration should save region properly. (#1468)
Fixes #1466
2016-05-03 15:17:58 -07:00
Yurii bba5468368 minio: Replace 'bucket already exists' error by 'bucket already owned by you'. (#1465)
S3 API returns BucketAlreadyExists error when some another user has such bucket.
If user that creates the bucket already has it, s3 returns BucketAlreadyOwnedByYou.
As minio has only one user, it should behave accordingly.
Otherwise it causes failures in the applications that ignore creation of already existing bucket in the account, but fail when bucket name is used by someone else.
2016-05-03 03:19:04 -07:00
Harshavardhana 7ae40eb1bb minhttp: Remove probe usage, move to golang error. (#1459)
Fixes #1454
2016-05-03 01:07:34 -07:00
Harshavardhana ad8e27a963 xl: Rename 'xl.json' to 'file.json' (#1461)
Fixes #1460
2016-05-02 17:42:01 -07:00
Harshavardhana ac7a7cec20 bucket-policy: Delete policy should remove policy properly. (#1456) 2016-05-02 16:58:10 -07:00
Harshavardhana afd59c45dc xl/fs: Move few functions into common code. (#1453)
- PutObject()
- PutObjectPart()
- NewMultipartUpload()
- AbortMultipartUpload()

Implementations across both FS and XL object layer
share common implementation.
2016-05-02 16:57:31 -07:00
Harshavardhana 3bf3d18f1f rpc/client: Implement RenameFile properly. (#1443) 2016-05-02 03:12:18 -07:00
Harshavardhana 8102a4712a xl/metadata: Keep the json erasure tag consistent. (#1447)
Currently the on-disk json has "Erasure" we should
keep it consistent name and move to lower case instead.
2016-05-02 01:13:07 -07:00
karthic rao 2393a3a0be XL non-recursive fix (#1450) 2016-05-01 23:16:44 -07:00
Harshavardhana d006129fb5 xl/vol: Add healing and quorum support for StatVol, MakeVol.
Fixes #1437
2016-05-01 18:42:00 -07:00
Harshavardhana 7caa82f32f xl/fs: Rename minioMetaVolume to minioMetaBucket. (#1442) 2016-05-01 18:13:10 -07:00
Krishna Srinivas 286de4de2c XL - fixes mostly related to multipart listing. (#1441)
* XL/Multipart: Use json.NewDecoder to decode read stream.

* XL/Multipart: fix recursive and non-recursive listing.

* XL/Multipart: Create object part with md5sum later using RenameFile.

* XL/Multipart: ListObjectParts should list parts in order.

previously: uploadID.10.md5sum < uploadID.2.md5sum
fix       : uploadID.00010.md5sum > uploadID.00002.md5sum

* XL/Multipart: Keep the size of each part in the multipart metadata file to avoid stats on the parts.

* XL/Multipart: fix listing bug which was showing size of the multipart uploaded objects as 0.
2016-05-01 17:52:16 -07:00
Harshavardhana ba7a55c321 xl: ReedSolomon code fix small file erasure bug. (#1431)
For files less than 'dataBlocks', erasure encoding would fail
with short data due to a bug in the implementation itself.

Relax the error return, even a single byte can be properly
erasure coded without issues.

Fixes #1413
2016-05-01 15:30:40 -07:00
Harshavardhana e05aa762a9 fs: Create object part with md5sum later using RenameFile. (#1440)
Fixes #1340.
2016-05-01 14:50:30 -07:00
Krishna Srinivas 0c27d8e5b1 XL/Multipart: maintain the parts info in multipart.json after complete-multipart-upload. (#1436) 2016-05-01 01:25:48 -07:00
Harshavardhana 443ec37765 xl: Add disk usages properly for ListVols() and StatVol(). (#1435) 2016-04-30 23:34:43 -07:00
Bala FA d5df8b8b8d xl: remove unused err return in listFileVersions() (#1434) 2016-04-30 03:21:54 -07:00
Harshavardhana ac2933c799 windows: Enable erasure test for windows. (#1432)
Fixes #1363
2016-04-30 02:52:23 -07:00
Bala FA 84afec9ae0 xl: fix DeleteFile() removing meta data files without updating it (#1433)
Fixes #1428 #1427
2016-04-30 02:52:15 -07:00
Harshavardhana 27c50a70cc obj: support object names with curly braces. (#1429)
Example files like

```
/usr/share/mozilla/extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}/ubufox@ubuntu.com.xpi
Song's Son.ogg
```

Should be supported.
2016-04-29 20:19:08 -07:00
Bala FA a978975eea xl: add quorum support for DeleteFile() (#1426)
Fixes #1396
2016-04-29 18:43:18 -07:00
Harshavardhana dc45ea3946 log: Fix file logging, enable it properly. (#1424)
Fixes #1419
2016-04-29 17:54:02 -07:00
Harshavardhana 9eb56f0676 xl/healFile: Handle errors and continue (#1425)
Fixes #1354
2016-04-29 17:52:49 -07:00
Harshavardhana 10a010c1ad xl/fs: Object layer - keep common functions into single place. (#1423) 2016-04-29 17:52:17 -07:00
Harshavardhana a9935f886c vendor: update reedsolomon package with new perm improvements. (#1422) 2016-04-29 14:59:03 -07:00
Harshavardhana 4e34e03dd4 xl/fs: Split object layer into interface. (#1415) 2016-04-29 14:24:10 -07:00
nomadlogic 4d1b3d5e9a docs: FreeBSD minio source intructions (#1421)
* Modifications of documentation for using and building minio server on FreeBSD.

- update example of enabling compression to use lz4 vs gzip and provide
  explanation of benefits of lz4

- provide walkthrough of building minio server on FreeBSD with binary
  golang and gmake

* Fixing markdown syntax for code blocks so we render correctly.

* typo fix

* reword compression enablement docs for easier reading
2016-04-29 13:35:20 -07:00
Krishna Srinivas 7066ce5160 XL/Multipart: rename the parts instead of concatenating. (#1416) 2016-04-29 12:17:48 -07:00
Krishna Srinivas 39df425b2a lock: bug fixes. (#1420)
* release Lock on map before trying to Lock NS
* delete NS lock from map if no more refs.
* refactor to avoid repetition of code.
2016-04-29 11:39:20 -07:00
Harshavardhana 984903cce1 server: Add global namespace lock. (#1398)
Fixes #1393
2016-04-29 01:29:09 -07:00
karthic rao 8deddb82f4 Cleanup: Moving IsValidLocationContraint to handler utils 2016-04-28 20:01:11 -07:00
Harshavardhana a1a667ae5d xl: Change fileMetadata to xlMetadata. (#1404)
Finalized backend format

```
{
    "version": "1.0.0",
    "stat": {
        "size": 24256,
        "modTime": "2016-04-28T00:11:37.843Z"
    },
    "erasure": {
        "data": 5,
        "parity": 5,
        "blockSize": 4194304
    ],
    "minio": {
        "release": "RELEASE.2016-04-28T00-09-47Z"
    }
}
```
2016-04-28 19:27:02 -07:00
Harshavardhana 41b35cff7b xl: Fixes a bug in read quorum ListFiles() (#1412)
Fixes a bug in #1406
2016-04-28 17:32:46 -07:00
Harshavardhana eed756777b object: Allow '[' and ']' as part of object names. 2016-04-28 13:29:32 -07:00
Harshavardhana 2ac10209cc xl: ListFiles - return sorted files. (#1408)
Fixes #1407
2016-04-28 01:48:57 -07:00
Bala FA 5bd6b0b510 xl: check read quorum for ListFiles() (#1406)
Fixes #1364
2016-04-27 21:09:26 -07:00
karthic rao 1813e9c070 Cleanup - Comments and readability fixes (#1386) 2016-04-27 19:28:13 -07:00
Scott McClellan f87a19a15c Minor changes to CONTIRBUTING.md instructions (#1403) 2016-04-27 16:57:16 -07:00
Harshavardhana 5fffd558d0 xl/heal: Make healFile non-blocking for StatFile and ReadFile. (#1399)
Fixes #1355
2016-04-27 15:10:19 -07:00
Harshavardhana b51bef85e6 objectapi: ListMultipart now lists more than 1000 entries in non-recursive. (#1397)
Having keyMarker with "/" is a valid marker.

Fixes #1394
2016-04-27 13:19:48 -07:00
Krishna Srinivas d0e5470050 ListMultipart fixes (#1392)
* ListMultipart: listLeafEntries() - return earlier if a directory is found.
* ListMultipart: do listLeafEntries() only for directories.
2016-04-27 00:15:40 -07:00
Harshavardhana 90987df9b4 objectapi: Simplify ListMultipart combine recursive and non-recursive. (#1390)
Fixes #1365
2016-04-26 17:57:16 -07:00
Harshavardhana ad1abc4486 xl-v1/Cleanup: use listOnlineDisks instead of getReadableDisks. (#1389)
Remove usage of getFileVersionQuorumMap, instead use listFileVersions
to get the version list and extract higherVersion.

Fixes #1379
Fixes #1378
Fixes #1377
2016-04-26 13:03:37 -07:00
Krishna Srinivas 4333e529e6 xl/ListFiles: return as many objects as requested. (#1383)
* xl/ListFiles: return as many objects as requested and take care of eof (#1361)

* xl/ListFiles: fix review comments.

* xl/ListFiles: Add windows filepath translation.

* xl/ListFiles: Use slashSeparator instead of "/". Remove filepath.FromSlash() as golang-windows takes care of it automatically.
2016-04-26 10:35:39 -07:00
koolhead17 9685f88b84 Added FreeBSD installation steps with ZFS. (#1388) 2016-04-26 10:17:02 -07:00
Harshavardhana 5f80edf232 routers: Fix a crash while initializing network fs. (#1382)
Crash happens when 'minio server filename' a file name is
provided instead of a directory on command line argument.

```
panic: runtime error: slice bounds out of range

goroutine 1 [running]:
panic(0x5eb460, 0xc82000e0b0)
	/usr/local/opt/go/libexec/src/runtime/panic.go:464 +0x3e6
main.splitNetPath(0x7fff5fbff9bd, 0x7, 0x0, 0x0, 0x0, 0x0)
	/Users/harsha/mygo/src/github.com/minio/minio/network-fs.go:49 +0xb7
main.newNetworkFS(0x7fff5fbff9bd, 0x7, 0x0, 0x0, 0x0, 0x0)
	/Users/harsha/mygo/src/github.com/minio/minio/network-fs.go:90 +0x20a
main.configureServerHandler(0xc82024e1c8, 0x5, 0xc8200640e0, 0x1, 0x1, 0x0, 0x0)
	/Users/harsha/mygo/src/github.com/minio/minio/routers.go:43 +0x6ce
main.configureServer(0xc82024e1c8, 0x5, 0xc8200640e0, 0x1, 0x1, 0x5)
	/Users/harsha/mygo/src/github.com/minio/minio/server-main.go:86 +0x67
```
2016-04-25 18:10:40 -07:00
Harshavardhana 42254b5c4d xl: Rename blockingWriteCloser to waitCloser. (#1376) 2016-04-25 16:00:58 -07:00
Harshavardhana 00c697393a Merge pull request #1381 from minio/xl-layer
Implement XL layer
2016-04-25 15:59:30 -07:00
Harshavardhana 55032ffdf9 xl: Simplify blockingWriter and its usage. (#1373)
This removes odd races since we don't need to
track errors and avoids locking. All we need
is a Wait() and Done() waitgroup.
2016-04-25 12:47:31 -07:00
Harshavardhana 8bce699dae xl: Add logging. (#1372) 2016-04-25 12:47:31 -07:00
Harshavardhana 57f35c2bcc xl: Introduce new blocking writer to make CreateFile atomic. (#1362)
Creates a new write closer that must be released
by the read consumer. This is necessary so that
while commiting the underlying writers in erasure
coding we need to make sure we reply success only if
we have committed to disk.

This in turn also fixes plethora of bugs related to
subsequent PutObject() races with namespace locking.

This patch also enables most of the tests, other than
ListObjects paging which has some issues still.

Fixes #1358, #1360
2016-04-25 12:47:31 -07:00
Harshavardhana cab6805f09 xl: Enable a subset of tests for XL branch. (#1359) 2016-04-25 12:47:31 -07:00
Krishna Srinivas 8c85815106 xl: refactor functions to xl-v1-common.go xl-v1-utils.go. (#1357) 2016-04-25 12:47:31 -07:00
Krishna Srinivas becc814531 Xl layer selfheal quorum2
* xl/selfheal: selfheal based on read quorum on GET

* xl: getReadableDisks() also returns whether self-heal is needed so that this info can be used by ReadFile/SelfHeal/StatFile.

* xl: trigger selfheal from StatFile.
2016-04-25 12:47:31 -07:00
Harshavardhana 9bd9441107 xl: Simplify reading metadata and add a new fileMetadata type. (#1346) 2016-04-25 12:47:31 -07:00
Harshavardhana f3784d1087 xl: Handle read quorum for StatVol, ListVols 2016-04-25 12:47:31 -07:00
Harshavardhana 91588209fa obj: Object api handle all errors in common location. (#1343) 2016-04-25 12:47:31 -07:00
Krishna Srinivas 5c33b68318 xl: code refactor, cleanup ReadFile and CreateFile. 2016-04-25 12:47:31 -07:00
Bala FA 45b3d3e21f xl: add quorum support for create file 2016-04-25 12:47:31 -07:00
Harshavardhana 141a44bfbf xl: Fix ReadFile to keep the order always for reading the data back. (#1339)
Also fixes a stackoverflow bug in namespace locking.
2016-04-25 12:47:31 -07:00
Harshavardhana c7bf471c9e list/xl: Fix the way marker is handled in leafDirectory verification. 2016-04-25 12:47:31 -07:00
Krishna Srinivas c302875774 selfheal: implement self-heal. Heals the missing parts. (#1335) 2016-04-25 12:47:31 -07:00
Harshavardhana b76f3f1d62 xl: Add more fixes and cleanup.
Simplify cleanup of temporary files during createFile operations.
2016-04-25 12:47:31 -07:00
Bala FA ada0f82b9a xl: add quorum support for read file and name space locking. (#1333) 2016-04-25 12:47:31 -07:00
Harshavardhana a98a7fb1ad Implement XL layer - preliminary work. 2016-04-25 12:47:31 -07:00
Harshavardhana bf8a9702a4 tests: Fix a bug in TestObjectAPIIsUploadIDExists. (#1375)
The following code crashes when upload ID does not
exist, since we are setting err == nil when we find
err == errFileNotFound.

```
if e == nil {
   t.Fatal(e.Error())
```

Fix it.
2016-04-25 12:47:08 -07:00
karthic rao 6e372f83b4 Tests: object api multipart tests and bug fixes. 2016-04-25 10:39:28 -07:00
Harshavardhana e9fba04b36 logging: Enable logging across storage fs layer. (#1367)
Adds log.Debugf at all the layer - fixes #1074
2016-04-24 00:36:00 -07:00
Harshavardhana d63d17012d tests: Add API suite tests back for object api. (#1352) 2016-04-21 23:40:01 -07:00
Harshavardhana 444d1f8a65 miniobrowser: Vendorize to new changes in miniobrowser. 2016-04-21 20:35:48 -07:00
karthic rao 560c3bd153 Adding return statement after error response in the lastest commit to verify location constraint (#1348) 2016-04-21 20:08:08 -07:00
Harshavardhana 4cf73caf02 api: Add diskInfo as part of StatVol and ListVols. (#1349)
It is the bucket and volumes which needs to have this
value rather than the DiskInfo API itself. Eventually
this can be extended to show disk usage per
Buckets/Volumes whenever we have that functionality.

For now since buckets/volumes are thinly provisioned
this is the right approach.
2016-04-21 20:07:47 -07:00
Harshavardhana 1284ecc6f2 api: Fix verification of checkLeafDirectory. (#1347)
This fixes a problem where leaf directory has more than 1000
entries, also resulting in listing issues, leading to an infinite
loop.

Fixes #1334
2016-04-21 18:05:26 -07:00
karthic rao cb1116725b api: verify Location constraint for make bucket. (#1342) 2016-04-20 17:35:38 -07:00
koolhead17 c3d0a3d51e Update README.md
Our community contributor bought this to our attention so we have to add region as well in s3cmd config file.
2016-04-19 10:22:40 -07:00
Harshavardhana e0f8fed011 object: handle Error responses and handle errDiskFull. (#1331) 2016-04-19 02:42:10 -07:00
Harshavardhana 6bc17a3aea server: Attempt to increase max open files. (#1328) 2016-04-18 22:05:32 -07:00
Dave Henderson ff9a6b00cc isContainerized should look for /.dockerenv not /.dockerinit
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
2016-04-18 19:26:56 -07:00
matt robinson af907a35a9 add environment var to explicitly indicate containerized and allow running as root (#1327) 2016-04-18 16:30:11 -07:00
Harshavardhana b47d722d8e fs: Fix filtering out valid paths from previous #1321 fix (#1323)
Fixes #1324
2016-04-17 12:00:23 -07:00
Harshavardhana 33633fd15d fs: Filter out valid paths and volnames. (#1321) 2016-04-16 19:33:29 -07:00
Harshavardhana b2ec7da9f9 Merge pull request #1314 from minio/split-fs
Split fs code into storage layer and object layer.
2016-04-16 18:09:31 -07:00
Harshavardhana be002ac01e fs/object: Fix issues from review comments. 2016-04-16 17:57:14 -07:00
Krishna Srinivas 149c6ca094 listMultipart: bugfixes. (#1318) 2016-04-16 16:25:53 -07:00
Harshavardhana 8457af5708 fs: Add proper volume and path validation. 2016-04-16 16:25:53 -07:00
Krishna Srinivas caa35f68fa listMultipart: implement support for marker. (#1313) 2016-04-16 16:25:53 -07:00
Harshavardhana 30b0b4deba storage/server/client: Enable storage server, enable client storage. 2016-04-16 16:25:53 -07:00
Krishna Srinivas 01a439f95b refactor: add multipart code to the object layer. 2016-04-16 16:25:53 -07:00
Krishna Srinivas 3c48537f20 refactor: refactor code to separate fs into object-layer and fs layer. (#1305) 2016-04-16 16:25:53 -07:00
651 changed files with 94312 additions and 18449 deletions
+13
View File
@@ -0,0 +1,13 @@
## Expected behaviour
## Actual behaviour
## Steps to reproduce the behaviour
## Minio version
- (paste output of `minio version`)
## System information
+3
View File
@@ -13,3 +13,6 @@ build
vendor/**/*.js
vendor/**/*.json
release
.DS_Store
*.syso
coverage.txt
+4
View File
@@ -0,0 +1,4 @@
{
"numFilesToCheck": 10,
"requiredOrgs": ["minio"]
}
+5 -6
View File
@@ -13,12 +13,11 @@ env:
- ARCH=i686
script:
- make test
- make test GOFLAGS="-race"
- make coverage
after_success:
- bash <(curl -s https://codecov.io/bash)
go:
- 1.6
notifications:
slack:
secure: K9tsn5MvrCAxuEZTxn+m3Kq1K2NG2xMEJFSv/sTp+RQBW7TslPHzv859GsIvrm8mU1y1btOU9RlOzqrRUczI5cJpE8IL1oljPZbXrIXgetE0kbsw0Wpy99g27UQ2VGp933WDu8tfj7zU4cZv+BI0RltNLwqYO6GWXmcWP0IueCU=
- 1.6.2
-76
View File
@@ -1,76 +0,0 @@
## Using aws-sdk-go with Minio
aws-sdk-go is the official AWS SDK for the Go programming language. This document covers
how to use aws-sdk-go with Minio server.
### Install AWS SDK S3 service
```sh
$ go get github.com/aws/aws-sdk-go/service/s3
```
### List all buckets on Minio
```go
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
func main() {
newSession := session.New()
s3Config := &aws.Config{
Credentials: credentials.NewStaticCredentials("<YOUR-ACCESS-KEY-ID>", "<YOUR-SECRET-ACCESS-KEY", ""),
Endpoint: aws.String("http://localhost:9000"),
Region: aws.String("us-east-1"),
DisableSSL: aws.Bool(true),
S3ForcePathStyle: aws.Bool(true),
}
// Create an S3 service object in the default region.
s3Client := s3.New(newSession, s3Config)
cparams := &s3.CreateBucketInput{
Bucket: aws.String("newbucket"), // Required
}
_, err := s3Client.CreateBucket(cparams)
if err != nil {
// Message from an error.
fmt.Println(err.Error())
return
}
var lparams *s3.ListBucketsInput
// Call the ListBuckets() Operation
resp, err := s3Client.ListBuckets(lparams)
if err != nil {
// Message from an error.
fmt.Println(err.Error())
return
}
// Pretty-print the response data.
fmt.Println(resp)
}
```
Populate your AccessKeyId and SecretAccessKey credentials and run the program as shown below.
```sh
$ go run aws-sdk-minio.go
{
Buckets: [{
CreationDate: 2015-10-22 01:46:04 +0000 UTC,
Name: "newbucket"
}],
Owner: {
DisplayName: "minio",
ID: "minio"
}
}
```
+3 -3
View File
@@ -3,11 +3,11 @@
If you do not have a working Golang environment setup please follow [Golang Installation Guide](./INSTALLGO.md).
### Setup your Minio Github Repository
Fork [Minio upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL and pass it to ``go get`` command. Go uses git to clone a copy into your project workspace folder.
Fork [Minio upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL for minio from your personal github repo (you will need it for the `git clone` command below).
```sh
$ mkdir -p $GOPATH/src/github.com/minio
$ cd $GOPATH/src/github.com/minio
$ git clone https://github.com/$USER_ID/minio
$ git clone <paste saved URL for personal forked minio repo>
$ cd minio
```
@@ -51,7 +51,7 @@ Building Libraries
- Push to the branch (git push origin my-new-feature)
- Create new Pull Request
* If you have additional dependencies for ``Minio``, ``Minio`` manages its depedencies using [govendor](https://github.com/kardianos/govendor)
* If you have additional dependencies for ``Minio``, ``Minio`` manages its dependencies using [govendor](https://github.com/kardianos/govendor)
- Run `go get foo/bar`
- Edit your code to import foo/bar
- Run `make pkg-add PKG=foo/bar` from top-level directory
-27
View File
@@ -1,27 +0,0 @@
## Setting up Proxy using Caddy.
Please download [Caddy Server](https://caddyserver.com/download)
Create a caddy configuration file as below, change the ip addresses according to your local
minio and DNS configuration.
```bash
$ ./minio --address localhost:9000 server <your_export_dir>
```
```bash
your.public.com {
proxy / localhost:9000 {
proxy_header Host {host}
proxy_header X-Real-IP {remote}
proxy_header X-Forwarded-Proto {scheme}
}
}
```
```bash
$ ./caddy
Activating privacy features... done.
your.public.com:443
your.public.com:80
```
+15
View File
@@ -0,0 +1,15 @@
FROM golang:1.6
RUN mkdir -p /go/src/app
WORKDIR /go/src/app
COPY . /go/src/app
RUN go-wrapper download
RUN go-wrapper install
ENV ALLOW_CONTAINER_ROOT=1
RUN mkdir -p /export/docker && cp /go/src/app/docs/Docker.md /export/docker/
EXPOSE 9000
ENTRYPOINT ["go-wrapper", "run", "server"]
CMD ["/export"]
-84
View File
@@ -1,84 +0,0 @@
## Ubuntu (Kylin) 14.04
### Build Dependencies
This installation document assumes Ubuntu 14.04+ on x86-64 platform.
##### Install Git, GCC
```sh
$ sudo apt-get install git build-essential
```
##### Install Go 1.6+
Download Go 1.6+ from [https://golang.org/dl/](https://golang.org/dl/).
```sh
$ wget https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz
$ mkdir -p ${HOME}/bin/
$ mkdir -p ${HOME}/go/
$ tar -C ${HOME}/bin/ -xzf go1.6.linux-amd64.tar.gz
```
##### Setup GOROOT and GOPATH
Add the following exports to your ``~/.bashrc``. Environment variable GOROOT specifies the location of your golang binaries
and GOPATH specifies the location of your project workspace.
```sh
export GOROOT=${HOME}/bin/go
export GOPATH=${HOME}/go
export PATH=${HOME}/bin/go/bin:${GOPATH}/bin:$PATH
```
##### Source the new enviornment
```sh
$ source ~/.bashrc
```
##### Testing it all
```sh
$ go env
```
## OS X (Yosemite) 10.10
### Build Dependencies
This installation document assumes OS X Yosemite 10.10+ on x86-64 platform.
##### Install brew
```sh
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
```
##### Install Git, Python
```sh
$ brew install git python
```
##### Install Go 1.5+
Install golang binaries using `brew`
```sh
$ brew install go
$ mkdir -p $HOME/go
```
##### Setup GOROOT and GOPATH
Add the following exports to your ``~/.bash_profile``. Environment variable GOROOT specifies the location of your golang binaries
and GOPATH specifies the location of your project workspace.
```sh
export GOPATH=${HOME}/go
export GOVERSION=$(brew list go | head -n 1 | cut -d '/' -f 6)
export GOROOT=$(brew --prefix)/Cellar/go/${GOVERSION}/libexec
export PATH=${GOPATH}/bin:$PATH
```
##### Source the new enviornment
```sh
$ source ~/.bash_profile
```
##### Testing it all
```sh
$ go env
```
+11 -15
View File
@@ -1,8 +1,6 @@
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
DOCKER_BIN := $(shell which docker)
PWD := $(shell pwd)
GOPATH := $(shell go env GOPATH)
DOCKER_LDFLAGS := '$(LDFLAGS) -extldflags "-static"'
BUILD_LDFLAGS := '$(LDFLAGS)'
TAG := latest
@@ -62,15 +60,12 @@ checks:
@(env bash $(PWD)/buildscripts/checkdeps.sh)
@(env bash $(PWD)/buildscripts/checkgopath.sh)
checkdocker:
@echo "Checking if docker is installed.. "
@if [ -z ${DOCKER_BIN} ]; then echo "Docker not installed, cannot build docker image. Please install 'sudo apt-get install docker.io'" && exit 1; else echo "Docker installed at ${DOCKER_BIN}."; fi;
getdeps: checks
@go get -u github.com/golang/lint/golint && echo "Installed golint:"
@go get -u github.com/fzipp/gocyclo && echo "Installed gocyclo:"
@go get -u github.com/remyoudompheng/go-misc/deadcode && echo "Installed deadcode:"
@go get -u github.com/client9/misspell/cmd/misspell && echo "Installed misspell:"
@go get -u github.com/gordonklaus/ineffassign && echo "Installed ineffassign:"
verifiers: vet fmt lint cyclo spelling
@@ -91,6 +86,10 @@ lint:
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/golint *.go
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/golint github.com/minio/minio/pkg...
ineffassign:
@echo "Running $@:"
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/ineffassign .
cyclo:
@echo "Running $@:"
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/gocyclo -over 65 *.go
@@ -102,14 +101,18 @@ deadcode:
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/deadcode
spelling:
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell *.go
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell pkg/**/*
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell -error *.go
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell -error pkg/**/*
test: build
@echo "Running all minio testing:"
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) .
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) github.com/minio/minio/pkg...
coverage: build
@echo "Running all coverage for minio:"
@GO15VENDOREXPERIMENT=1 ./buildscripts/go-coverage.sh
gomake-all: build
@echo "Installing minio:"
@GO15VENDOREXPERIMENT=1 go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
@@ -128,13 +131,6 @@ pkg-list:
install: gomake-all
dockerimage: checkdocker getdeps verifiers $(UI_ASSETS)
@echo "Building docker image:" minio:$(TAG)
@GO15VENDOREXPERIMENT=1 GOOS=linux GOARCH=amd64 go build --ldflags $(DOCKER_LDFLAGS) -o docker/minio.dockerimage
@cd docker; mkdir -p export; sudo docker build --rm --tag=minio/minio:$(TAG) .
@rmdir docker/export
@rm docker/minio.dockerimage
release: verifiers
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
-10
View File
@@ -1,10 +0,0 @@
## Multipart backend format
When multipart upload is used for objects, below meta-data/staging files are created
- New multipart upload call creates file ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.uploadid```
- Put object part call creates file ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.PART_NUMBER.MD5SUM_STRING```
- Abort multipart call removes all files matching ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.*```
- Complete multipart call does
1. Create a staging file ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.complete.TEMP_NAME``` then rename to ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.complete```
2. Rename staging file ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.complete``` to ```EXPORT_DIR/BUCKET/PATH/TO/OBJECT```
+152 -193
View File
@@ -1,232 +1,191 @@
## Minio [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
# Minio Quickstart Guide [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![codecov](https://codecov.io/gh/minio/minio/branch/master/graph/badge.svg)](https://codecov.io/gh/minio/minio)
Minio is an object storage server compatible with Amazon S3 and licensed under [Apache license 2.0](./LICENSE).
Minio is an object storage server released under Apache License v2.0. It is compatible with Amazon S3 cloud storage service. It is best suited for storing unstructured data such as photos, videos, log files, backups and container / VM images. Size of an object can range from a few KBs to a maximum of 5TB.
## Description
## 1. Download
Minio is an open source object storage server released under Apache License V2. It is compatible with Amazon S3 cloud storage service. Minio follows a minimalist design philosophy.
Minio is light enough to be bundled with the application stack. It sits on the side of NodeJS, Redis, MySQL and the likes. Unlike databases, Minio stores objects such as photos, videos, log files, backups, container / VM images and so on. Minio is best suited for storing blobs of information ranging from KBs to 5 TBs each. In a simplistic sense, it is like a FTP server with a simple get / put API over HTTP.
Minio server is light enough to be bundled with the application stack, similar to NodeJS, Redis and MySQL.
Minio currently implements two backends
| Platform| Architecture | URL|
| ----------| -------- | ------|
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio|
||32-bit Intel|https://dl.minio.io/server/minio/release/linux-386/minio|
||32-bit ARM|https://dl.minio.io/server/minio/release/linux-arm/minio|
|Apple OS X|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio|
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe|
||32-bit|https://dl.minio.io/server/minio/release/windows-386/minio.exe|
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio|
- Filesystem (FS) - is available and ready for general purpose use. This version of the Minio binary is built using Filesystem storage backend for magnetic and solid state disks.
- ErasureCoded (XL) - is work in progress and not ready for general purpose use.
### Install from Source
## Minio Client
Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://docs.minio.io/docs/how-to-install-golang).
[Minio Client (mc)](https://github.com/minio/mc#minio-client-mc-) provides a modern alternative to Unix commands like ``ls``, ``cat``, ``cp``, ``sync``, and ``diff``. It supports POSIX compatible filesystems and Amazon S3 compatible cloud storage systems. It is entirely written in Golang.
## Amazon S3 Compatible Client Libraries
- [Golang Library](https://github.com/minio/minio-go)
- [Java Library](https://github.com/minio/minio-java)
- [Nodejs Library](https://github.com/minio/minio-js)
- [Python Library](https://github.com/minio/minio-py)
- [.Net Library](https://github.com/minio/minio-dotnet)
```sh
### Install [![Build Status](https://travis-ci.org/minio/minio.svg?branch=master)](https://travis-ci.org/minio/minio)[![Build status](https://ci.appveyor.com/api/projects/status/royh137dni8yevep/branch/master?svg=true)](https://ci.appveyor.com/project/harshavardhana/minio-qxbjq/branch/master)
#### GNU/Linux
Download ``minio`` for:
- ``64-bit Intel`` from https://dl.minio.io/server/minio/release/linux-amd64/minio
- ``32-bit Intel`` from https://dl.minio.io/server/minio/release/linux-386/minio
- ``32-bit ARM`` from https://dl.minio.io/server/minio/release/linux-arm/minio
~~~
$ chmod +x minio
$ ./minio --help
~~~
#### OS X
Download ``minio`` from https://dl.minio.io/server/minio/release/darwin-amd64/minio
~~~
$ chmod 755 minio
$ ./minio --help
~~~
#### Microsoft Windows
Download ``minio`` for:
- ``64-bit`` from https://dl.minio.io/server/minio/release/windows-amd64/minio.exe
- ``32-bit`` from https://dl.minio.io/server/minio/release/windows-386/minio.exe
~~~
C:\Users\Username\Downloads> minio.exe --help
~~~
#### FreeBSD
Download ``minio`` from https://dl.minio.io/server/minio/release/freebsd-amd64/minio
~~~
$ chmod 755 minio
$ ./minio --help
~~~
#### Docker container
Download ``minio`` for docker.
~~~
$ docker pull minio/minio
~~~
Read more here on [How to configure data volume containers for Minio?](./Docker.md)
#### Source
<blockquote>
NOTE: Source installation is intended for only developers and advanced users. For general use, please download official releases from https://minio.io/download.
</blockquote>
If you do not have a working Golang environment, please follow [Install Golang](./INSTALLGO.md).
~~~
$ go get -d github.com/minio/minio
$ cd $GOPATH/src/github.com/minio/minio
$ make
~~~
### How to use Minio?
Start minio server.
~~~
$ minio server ~/Photos
AccessKey: WLGDGYAQYIGI833EV05A SecretKey: BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF Region: us-east-1
Minio Object Storage:
http://127.0.0.1:9000
http://10.1.10.177:9000
Minio Browser:
http://127.0.0.1:9000
http://10.1.10.177:9000
To configure Minio Client:
$ wget https://dl.minio.io/client/mc/release/darwin-amd64/mc
$ chmod 755 mc
$ ./mc config host add myminio http://localhost:9000 WLGDGYAQYIGI833EV05A BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF
~~~
#### How to use AWS CLI with Minio?
<blockquote>
This section assumes that you have already installed aws-cli, if not please visit https://aws.amazon.com/cli/
</blockquote>
To configure `aws-cli`, type `aws configure` and follow below steps.
```
$ aws configure
AWS Access Key ID [None]: YOUR_ACCESS_KEY_HERE
AWS Secret Access Key [None]: YOUR_SECRET_KEY_HERE
Default region name [None]: us-east-1
Default output format [None]: ENTER
```
Additionally enable `aws-cli` to use AWS Signature Version '4' for Minio server.
## 2. Run Minio Server
### GNU/Linux
```sh
$ chmod +x minio
$ ./minio --help
$ ./minio server ~/Photos
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
AccessKey: USWUXHGYZQYFYFFIT3RE
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Region: us-east-1
Browser Access:
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Object API (Amazon S3 compatible):
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
Java: https://docs.minio.io/docs/java-client-quickstart-guide
Python: https://docs.minio.io/docs/python-client-quickstart-guide
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
```
$ aws configure set default.s3.signature_version s3v4
```
To list your buckets.
```
$ aws --endpoint-url http://localhost:9000 s3 ls
2016-01-07 16:38:23 testbucket
```
### OS X
To list contents inside bucket.
```
$ aws --endpoint-url http://localhost:9000 s3 ls s3://testbucket
PRE test/
2015-12-17 08:46:41 12232928 vim
2016-01-07 16:38:23 32232928 emacs
2015-12-09 08:05:24 138504 s3cmd
```
#### How to use AWS SDK with Minio?
```sh
Please follow the documentation here - [Using aws-sdk-go with Minio server](./AWS-SDK-GO.md)
$ chmod 755 minio
$ ./minio --help
$ ./minio server ~/Photos
#### How to use s3cmd with Minio?
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
AccessKey: USWUXHGYZQYFYFFIT3RE
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Region: us-east-1
<blockquote>
This section assumes that you have already installed s3cmd, if not please visit http://s3tools.org/s3cmd
</blockquote>
Browser Access:
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
Edit the following fields in your s3cmd configuration file `~/.s3cfg` .
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Object API (Amazon S3 compatible):
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
Java: https://docs.minio.io/docs/java-client-quickstart-guide
Python: https://docs.minio.io/docs/python-client-quickstart-guide
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
```
host_base = localhost:9000
host_bucket = localhost:9000
access_key = YOUR_ACCESS_KEY_HERE
secret_key = YOUR_SECRET_KEY_HERE
signature_v2 = False
### Microsoft Windows
```sh
C:\Users\Username\Downloads> minio.exe --help
C:\Users\Username\Downloads> minio.exe server D:\Photos
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
AccessKey: USWUXHGYZQYFYFFIT3RE
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Region: us-east-1
Browser Access:
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
$ mc.exe config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Object API (Amazon S3 compatible):
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
Java: https://docs.minio.io/docs/java-client-quickstart-guide
Python: https://docs.minio.io/docs/python-client-quickstart-guide
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
```
To make a bucket
```
$ s3cmd mb s3://mybucket
Bucket 's3://mybucket/' created
### Docker Container
```sh
$ docker pull minio/minio
$ docker run -p 9000:9000 minio/minio
```
To copy an object to bucket
```
$ s3cmd put newfile.txt s3://testbucket
upload: 'newfile' -> 's3://testbucket/newfile'
### FreeBSD
```sh
$ chmod 755 minio
$ ./minio --help
$ ./minio server ~/Photos
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
AccessKey: USWUXHGYZQYFYFFIT3RE
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Region: us-east-1
Browser Access:
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Object API (Amazon S3 compatible):
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
Java: https://docs.minio.io/docs/java-client-quickstart-guide
Python: https://docs.minio.io/docs/python-client-quickstart-guide
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
```
To copy an object to local system
```
$ s3cmd get s3://testbucket/newfile
download: 's3://testbucket/newfile' -> './newfile'
## 3. Test Minio Server using Minio Browser
Open a web browser and navigate to http://127.0.0.1:9000 to view your buckets on minio server.
![Screenshot](https://github.com/minio/minio/blob/master/docs/screenshots/minio-browser.jpg?raw=true)
## 4. Test Minio Server using `mc`
Install mc from [here](https://docs.minio.io/docs/minio-client-quickstart-guide). Use `mc ls` command to list all the buckets on your minio server.
```sh
$ mc ls myminio/
[2015-08-05 08:13:22 IST] 0B andoria/
[2015-08-05 06:14:26 IST] 0B deflector/
[2015-08-05 08:13:11 IST] 0B ferenginar/
[2016-03-08 14:56:35 IST] 0B jarjarbing/
[2016-01-20 16:07:41 IST] 0B my.minio.io/
```
To sync local file/directory to a bucket
```
$ s3cmd sync newdemo s3://testbucket
upload: 'newdemo/newdemofile.txt' -> 's3://testbucket/newdemo/newdemofile.txt'
```
For more examples please navigate to [Minio Client Complete Guide](https://docs.minio.io/docs/minio-client-complete-guide).
To sync bucket or object with local filesystem
```
$ s3cmd sync s3://otherbucket otherlocalbucket
download: 's3://otherbucket/cat.jpg' -> 'otherlocalbucket/cat.jpg'
```
To list buckets.
```
$ s3cmd ls s3://
2015-12-09 16:12 s3://testbbucket
```
## 5. Explore Further
To list contents inside bucket.
```
$ s3cmd ls s3://testbucket/
DIR s3://testbucket/test/
2015-12-09 16:05 138504 s3://testbucket/newfile
```
- [Minio Erasure Code QuickStart Guide](https://docs.minio.io/docs/minio-erasure-code-quickstart-guide)
- [Minio Docker Quickstart Guide](https://docs.minio.io/docs/minio-docker-quickstart-guide)
- [Use `mc` with Minio Server](https://docs.minio.io/docs/minio-client-quickstart-guide)
- [Use `aws-cli` with Minio Server](https://docs.minio.io/docs/aws-cli-with-minio)
- [Use `s3cmd` with Minio Server](https://docs.minio.io/docs/s3cmd-with-minio)
- [Use `minio-go` SDK with Minio Server](https://docs.minio.io/docs/golang-client-quickstart-guide)
Delete an object from bucket
```
$ s3cmd del s3://testbucket/newfile
delete: 's3://testbucket/newfile'
```
Delete a bucket
```
$ s3cmd rb s3://testbucket
Bucket 's3://testbucket/' removed
```
## Contribute to Minio Project
Please follow Minio [Contributor's Guide](./CONTRIBUTING.md)
### Jobs
If you think in Lisp or Haskell and hack in go, you would blend right in. Send your github link to callhome@minio.io.
## 6. Contribute to Minio Project
Please follow Minio [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
+10 -20
View File
@@ -19,10 +19,7 @@ package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"regexp"
"github.com/minio/minio/pkg/probe"
)
// credential container for access and secret keys.
@@ -31,13 +28,6 @@ type credential struct {
SecretAccessKey string `json:"secretKey"`
}
// stringer colorized access keys.
func (a credential) String() string {
accessStr := colorMagenta("AccessKey: ") + colorWhite(a.AccessKeyID)
secretStr := colorMagenta("SecretKey: ") + colorWhite(a.SecretAccessKey)
return fmt.Sprint(accessStr + " " + secretStr)
}
const (
minioAccessID = 20
minioSecretID = 40
@@ -52,19 +42,19 @@ var isValidAccessKey = regexp.MustCompile(`^[a-zA-Z0-9\\-\\.\\_\\~]{5,20}$`)
// mustGenAccessKeys - must generate access credentials.
func mustGenAccessKeys() (creds credential) {
creds, err := genAccessKeys()
fatalIf(err.Trace(), "Unable to generate access keys.", nil)
fatalIf(err, "Unable to generate access keys.")
return creds
}
// genAccessKeys - generate access credentials.
func genAccessKeys() (credential, *probe.Error) {
func genAccessKeys() (credential, error) {
accessKeyID, err := genAccessKeyID()
if err != nil {
return credential{}, err.Trace()
return credential{}, err
}
secretAccessKey, err := genSecretAccessKey()
if err != nil {
return credential{}, err.Trace()
return credential{}, err
}
creds := credential{
AccessKeyID: string(accessKeyID),
@@ -75,10 +65,10 @@ func genAccessKeys() (credential, *probe.Error) {
// genAccessKeyID - generate random alpha numeric value using only uppercase characters
// takes input as size in integer
func genAccessKeyID() ([]byte, *probe.Error) {
func genAccessKeyID() ([]byte, error) {
alpha := make([]byte, minioAccessID)
if _, e := rand.Read(alpha); e != nil {
return nil, probe.NewError(e)
if _, err := rand.Read(alpha); err != nil {
return nil, err
}
for i := 0; i < minioAccessID; i++ {
alpha[i] = alphaNumericTable[alpha[i]%byte(len(alphaNumericTable))]
@@ -87,10 +77,10 @@ func genAccessKeyID() ([]byte, *probe.Error) {
}
// genSecretAccessKey - generate random base64 numeric value from a random seed.
func genSecretAccessKey() ([]byte, *probe.Error) {
func genSecretAccessKey() ([]byte, error) {
rb := make([]byte, minioSecretID)
if _, e := rand.Read(rb); e != nil {
return nil, probe.NewError(e)
if _, err := rand.Read(rb); err != nil {
return nil, err
}
return []byte(base64.StdEncoding.EncodeToString(rb))[:minioSecretID], nil
}
-120
View File
@@ -1,120 +0,0 @@
/*
* Minimalist Object Storage, (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"encoding/json"
"net/http"
"net/url"
"os"
"time"
"github.com/minio/minio/pkg/probe"
)
type accessLogHandler struct {
http.Handler
accessLogFile *os.File
}
// LogMessage is a serializable json log message
type LogMessage struct {
StartTime time.Time
Duration time.Duration
StatusMessage string // human readable http status message
ContentLength string // human readable content length
// HTTP detailed message
HTTP struct {
ResponseHeaders http.Header
Request struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header http.Header
Host string
Form url.Values
PostForm url.Values
Trailer http.Header
RemoteAddr string
RequestURI string
}
}
}
func (h *accessLogHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
message, err := getLogMessage(w, req)
fatalIf(err.Trace(), "Unable to extract http message.", nil)
_, e := h.accessLogFile.Write(message)
fatalIf(probe.NewError(e), "Writing to log file failed.", nil)
h.Handler.ServeHTTP(w, req)
}
func getLogMessage(w http.ResponseWriter, req *http.Request) ([]byte, *probe.Error) {
logMessage := &LogMessage{
StartTime: time.Now().UTC(),
}
// store lower level details
logMessage.HTTP.ResponseHeaders = w.Header()
logMessage.HTTP.Request = struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header http.Header
Host string
Form url.Values
PostForm url.Values
Trailer http.Header
RemoteAddr string
RequestURI string
}{
Method: req.Method,
URL: req.URL,
Proto: req.Proto,
ProtoMajor: req.ProtoMajor,
ProtoMinor: req.ProtoMinor,
Header: req.Header,
Host: req.Host,
Form: req.Form,
PostForm: req.PostForm,
Trailer: req.Header,
RemoteAddr: req.RemoteAddr,
RequestURI: req.RequestURI,
}
// logMessage.HTTP.Request = req
logMessage.Duration = time.Now().UTC().Sub(logMessage.StartTime)
js, e := json.Marshal(logMessage)
if e != nil {
return nil, probe.NewError(e)
}
js = append(js, byte('\n')) // append a new line
return js, nil
}
// setAccessLogHandler logs requests
func setAccessLogHandler(h http.Handler) http.Handler {
file, e := os.OpenFile("access.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
fatalIf(probe.NewError(e), "Unable to open access log.", nil)
return &accessLogHandler{Handler: h, accessLogFile: file}
}
+11
View File
@@ -16,11 +16,22 @@
package main
import (
"encoding/xml"
)
// ObjectIdentifier carries key name for the object to delete.
type ObjectIdentifier struct {
ObjectName string `xml:"Key"`
}
// createBucketConfiguration container for bucket configuration request from client.
// Used for parsing the location from the request body for MakeBucketbucket.
type createBucketLocationConfiguration struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CreateBucketConfiguration" json:"-"`
Location string `xml:"LocationConstraint"`
}
// DeleteObjectsRequest - xml carrying the object key names which needs to be deleted.
type DeleteObjectsRequest struct {
// Element to enable quiet mode for the request
+238 -30
View File
@@ -74,6 +74,7 @@ const (
ErrNoSuchKey
ErrNoSuchUpload
ErrNotImplemented
ErrPreconditionFailed
ErrRequestTimeTooSkewed
ErrSignatureDoesNotMatch
ErrMethodNotAllowed
@@ -83,8 +84,6 @@ const (
ErrMalformedPOSTRequest
ErrSignatureVersionNotSupported
ErrBucketNotEmpty
ErrRootPathFull
ErrObjectExistsAsPrefix
ErrAllAccessDisabled
ErrMalformedPolicy
ErrMissingFields
@@ -97,14 +96,45 @@ const (
ErrMissingSignHeadersTag
ErrPolicyAlreadyExpired
ErrMalformedDate
ErrMalformedPresignedDate
ErrMalformedCredentialDate
ErrMalformedCredentialRegion
ErrMalformedExpires
ErrNegativeExpires
ErrAuthHeaderEmpty
ErrDateHeaderMissing
ErrExpiredPresignRequest
ErrUnsignedHeaders
ErrMissingDateHeader
ErrInvalidQuerySignatureAlgo
ErrInvalidQueryParams
ErrBucketAlreadyOwnedByYou
// Add new error codes here.
// Bucket notification related errors.
ErrEventNotification
ErrARNNotification
ErrRegionNotification
ErrOverlappingFilterNotification
ErrFilterNameInvalid
ErrFilterNamePrefix
ErrFilterNameSuffix
ErrFilterPrefixValueInvalid
// S3 extended errors.
ErrContentSHA256Mismatch
// Add new extended error codes here.
// Minio extended errors.
ErrReadQuorum
ErrWriteQuorum
ErrStorageFull
ErrObjectExistsAsDirectory
ErrPolicyNesting
ErrInvalidObjectName
// Add new extended error codes here.
// Please open a https://github.com/minio/minio/issues before adding
// new error codes here.
)
// error code to APIError structure, these fields carry respective
@@ -127,17 +157,17 @@ var errorCodeResponse = map[APIErrorCode]APIError{
},
ErrInvalidMaxUploads: {
Code: "InvalidArgument",
Description: "Argument maxUploads must be an integer between 0 and 2147483647.",
Description: "Argument max-uploads must be an integer between 0 and 2147483647",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidMaxKeys: {
Code: "InvalidArgument",
Description: "Argument maxKeys must be an integer between 0 and 2147483647.",
Description: "Argument maxKeys must be an integer between 0 and 2147483647",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidMaxParts: {
Code: "InvalidArgument",
Description: "Argument maxParts must be an integer between 1 and 10000.",
Description: "Argument max-parts must be an integer between 0 and 2147483647",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidPartNumberMarker: {
@@ -202,7 +232,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
},
ErrInvalidRange: {
Code: "InvalidRange",
Description: "The requested range cannot be satisfied.",
Description: "The requested range is not satisfiable",
HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
},
ErrMalformedXML: {
@@ -227,7 +257,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
},
ErrNoSuchBucket: {
Code: "NoSuchBucket",
Description: "The specified bucket does not exist.",
Description: "The specified bucket does not exist",
HTTPStatusCode: http.StatusNotFound,
},
ErrNoSuchBucketPolicy: {
@@ -242,14 +272,19 @@ var errorCodeResponse = map[APIErrorCode]APIError{
},
ErrNoSuchUpload: {
Code: "NoSuchUpload",
Description: "The specified multipart upload does not exist.",
Description: "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
HTTPStatusCode: http.StatusNotFound,
},
ErrNotImplemented: {
Code: "NotImplemented",
Description: "A header you provided implies functionality that is not implemented.",
Description: "A header you provided implies functionality that is not implemented",
HTTPStatusCode: http.StatusNotImplemented,
},
ErrPreconditionFailed: {
Code: "PreconditionFailed",
Description: "At least one of the pre-conditions you specified did not hold",
HTTPStatusCode: http.StatusPreconditionFailed,
},
ErrRequestTimeTooSkewed: {
Code: "RequestTimeTooSkewed",
Description: "The difference between the request time and the server's time is too large.",
@@ -257,7 +292,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
},
ErrSignatureDoesNotMatch: {
Code: "SignatureDoesNotMatch",
Description: "The request signature we calculated does not match the signature you provided.",
Description: "The request signature we calculated does not match the signature you provided. Check your key and signing method.",
HTTPStatusCode: http.StatusForbidden,
},
ErrMethodNotAllowed: {
@@ -295,16 +330,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "The bucket you tried to delete is not empty.",
HTTPStatusCode: http.StatusConflict,
},
ErrRootPathFull: {
Code: "RootPathFull",
Description: "Root path has reached its minimum free disk threshold. Please delete few objects to proceed.",
HTTPStatusCode: http.StatusInternalServerError,
},
ErrObjectExistsAsPrefix: {
Code: "ObjectExistsAsPrefix",
Description: "An object already exists as your prefix, choose a different prefix to proceed.",
HTTPStatusCode: http.StatusConflict,
},
ErrAllAccessDisabled: {
Code: "AllAccessDisabled",
Description: "All access to this bucket has been disabled.",
@@ -326,8 +351,8 @@ var errorCodeResponse = map[APIErrorCode]APIError{
HTTPStatusCode: http.StatusBadRequest,
},
ErrCredMalformed: {
Code: "CredentialMalformed",
Description: "Credential field malformed does not follow accessKeyID/credScope.",
Code: "AuthorizationQueryParametersError",
Description: "Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting \"<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request\".",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMalformedDate: {
@@ -335,19 +360,46 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "Invalid date format header, expected to be in ISO8601, RFC1123 or RFC1123Z time format.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMalformedPresignedDate: {
Code: "AuthorizationQueryParametersError",
Description: "X-Amz-Date must be in the ISO8601 Long Format \"yyyyMMdd'T'HHmmss'Z'\"",
HTTPStatusCode: http.StatusBadRequest,
},
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
// right Description: "Error parsing the X-Amz-Credential parameter; incorrect date format \"%s\". This date in the credential must be in the format \"yyyyMMdd\".",
// Need changes to make sure variable messages can be constructed.
ErrMalformedCredentialDate: {
Code: "AuthorizationQueryParametersError",
Description: "Error parsing the X-Amz-Credential parameter; incorrect date format \"%s\". This date in the credential must be in the format \"yyyyMMdd\".",
HTTPStatusCode: http.StatusBadRequest,
},
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
// right Description: "Error parsing the X-Amz-Credential parameter; the region 'us-east-' is wrong; expecting 'us-east-1'".
// Need changes to make sure variable messages can be constructed.
ErrMalformedCredentialRegion: {
Code: "AuthorizationQueryParametersError",
Description: "Error parsing the X-Amz-Credential parameter; the region is wrong;",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidRegion: {
Code: "InvalidRegion",
Description: "Region does not match.",
HTTPStatusCode: http.StatusBadRequest,
},
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
// right Description: "Error parsing the X-Amz-Credential parameter; incorrect service \"s4\". This endpoint belongs to \"s3\".".
// Need changes to make sure variable messages can be constructed.
ErrInvalidService: {
Code: "AccessDenied",
Description: "Service scope should be of value 's3'.",
Code: "AuthorizationQueryParametersError",
Description: "Error parsing the X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".",
HTTPStatusCode: http.StatusBadRequest,
},
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
// Description: "Error parsing the X-Amz-Credential parameter; incorrect terminal "aws4_reque". This endpoint uses "aws4_request".
// Need changes to make sure variable messages can be constructed.
ErrInvalidRequestVersion: {
Code: "AccessDenied",
Description: "Request scope should be of value 'aws4_request'.",
Code: "AuthorizationQueryParametersError",
Description: "Error parsing the X-Amz-Credential parameter; incorrect terminal. This endpoint uses \"aws4_request\".",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMissingSignTag: {
@@ -366,8 +418,13 @@ var errorCodeResponse = map[APIErrorCode]APIError{
HTTPStatusCode: http.StatusBadRequest,
},
ErrMalformedExpires: {
Code: "MalformedExpires",
Description: "Malformed expires header, expected non-zero number.",
Code: "AuthorizationQueryParametersError",
Description: "X-Amz-Expires should be a number",
HTTPStatusCode: http.StatusBadRequest,
},
ErrNegativeExpires: {
Code: "AuthorizationQueryParametersError",
Description: "X-Amz-Expires must be non-negative",
HTTPStatusCode: http.StatusBadRequest,
},
ErrAuthHeaderEmpty: {
@@ -387,7 +444,13 @@ var errorCodeResponse = map[APIErrorCode]APIError{
},
ErrExpiredPresignRequest: {
Code: "AccessDenied",
Description: "Request has expired.",
Description: "Request has expired",
HTTPStatusCode: http.StatusBadRequest,
},
// FIXME: Actual XML error response also contains the header which missed in lsit of signed header parameters.
ErrUnsignedHeaders: {
Code: "AccessDenied",
Description: "There were headers present in the request which were not signed",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidQueryParams: {
@@ -395,9 +458,150 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "Query-string authentication version 4 requires the X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature, X-Amz-Date, X-Amz-SignedHeaders, and X-Amz-Expires parameters.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrBucketAlreadyOwnedByYou: {
Code: "BucketAlreadyOwnedByYou",
Description: "Your previous request to create the named bucket succeeded and you already own it.",
HTTPStatusCode: http.StatusConflict,
},
/// Bucket notification related errors.
ErrEventNotification: {
Code: "InvalidArgument",
Description: "A specified event is not supported for notifications.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrARNNotification: {
Code: "InvalidArgument",
Description: "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrRegionNotification: {
Code: "InvalidArgument",
Description: "A specified destination is in a different region than the bucket. You must use a destination that resides in the same region as the bucket.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrOverlappingFilterNotification: {
Code: "InvalidArgument",
Description: "An object key name filtering rule defined with overlapping prefixes, overlapping suffixes, or overlapping combinations of prefixes and suffixes for the same event types.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrFilterNameInvalid: {
Code: "InvalidArgument",
Description: "filter rule name must be either prefix or suffix",
HTTPStatusCode: http.StatusBadRequest,
},
ErrFilterNamePrefix: {
Code: "InvalidArgument",
Description: "Cannot specify more than one prefix rule in a filter.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrFilterNameSuffix: {
Code: "InvalidArgument",
Description: "Cannot specify more than one suffix rule in a filter.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrFilterPrefixValueInvalid: {
Code: "InvalidArgument",
Description: "prefix rule value cannot exceed 1024 characters",
HTTPStatusCode: http.StatusBadRequest,
},
/// S3 extensions.
ErrContentSHA256Mismatch: {
Code: "XAmzContentSHA256Mismatch",
Description: "The provided 'x-amz-content-sha256' header does not match what was computed.",
HTTPStatusCode: http.StatusBadRequest,
},
/// Minio extensions.
ErrStorageFull: {
Code: "XMinioStorageFull",
Description: "Storage backend has reached its minimum free disk threshold. Please delete few objects to proceed.",
HTTPStatusCode: http.StatusInternalServerError,
},
ErrObjectExistsAsDirectory: {
Code: "XMinioObjectExistsAsDirectory",
Description: "Object name already exists as a directory.",
HTTPStatusCode: http.StatusConflict,
},
ErrReadQuorum: {
Code: "XMinioReadQuorum",
Description: "Multiple disk failures, unable to reconstruct data.",
HTTPStatusCode: http.StatusServiceUnavailable,
},
ErrWriteQuorum: {
Code: "XMinioWriteQuorum",
Description: "Multiple disks failures, unable to write data.",
HTTPStatusCode: http.StatusServiceUnavailable,
},
ErrPolicyNesting: {
Code: "XMinioPolicyNesting",
Description: "Policy nesting conflict has occurred.",
HTTPStatusCode: http.StatusConflict,
},
ErrInvalidObjectName: {
Code: "XMinioInvalidObjectName",
Description: "Object name contains unsupported characters. Unsupported characters are `^*|\\\"",
HTTPStatusCode: http.StatusBadRequest,
},
// Add your error structure here.
}
// toAPIErrorCode - Converts embedded errors. Convenience
// function written to handle all cases where we have known types of
// errors returned by underlying layers.
func toAPIErrorCode(err error) (apiErr APIErrorCode) {
if err == nil {
return ErrNone
}
// Verify if the underlying error is signature mismatch.
switch err {
case errSignatureMismatch:
apiErr = ErrSignatureDoesNotMatch
case errContentSHA256Mismatch:
apiErr = ErrContentSHA256Mismatch
}
if apiErr != ErrNone {
// If there was a match in the above switch case.
return apiErr
}
switch err.(type) {
case StorageFull:
apiErr = ErrStorageFull
case BadDigest:
apiErr = ErrBadDigest
case IncompleteBody:
apiErr = ErrIncompleteBody
case ObjectExistsAsDirectory:
apiErr = ErrObjectExistsAsDirectory
case BucketNameInvalid:
apiErr = ErrInvalidBucketName
case BucketNotFound:
apiErr = ErrNoSuchBucket
case BucketNotEmpty:
apiErr = ErrBucketNotEmpty
case BucketExists:
apiErr = ErrBucketAlreadyOwnedByYou
case ObjectNotFound:
apiErr = ErrNoSuchKey
case ObjectNameInvalid:
apiErr = ErrInvalidObjectName
case InvalidUploadID:
apiErr = ErrNoSuchUpload
case InvalidPart:
apiErr = ErrInvalidPart
case InsufficientWriteQuorum:
apiErr = ErrWriteQuorum
case InsufficientReadQuorum:
apiErr = ErrReadQuorum
case PartTooSmall:
apiErr = ErrEntityTooSmall
default:
apiErr = ErrInternalError
}
return apiErr
}
// getAPIError provides API Error for input API error code.
func getAPIError(code APIErrorCode) APIError {
return errorCodeResponse[code]
@@ -418,3 +622,7 @@ func getAPIErrorResponse(err APIError, resource string) APIErrorResponse {
return data
}
func getErrMalformedCredentialDate(malformedDateStr string) {
}
+15 -11
View File
@@ -62,24 +62,28 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
// set common headers
setCommonHeaders(w)
// set object-related metadata headers
lastModified := objInfo.ModifiedTime.UTC().Format(http.TimeFormat)
// Set content length.
w.Header().Set("Content-Length", strconv.FormatInt(objInfo.Size, 10))
// Set last modified time.
lastModified := objInfo.ModTime.UTC().Format(http.TimeFormat)
w.Header().Set("Last-Modified", lastModified)
w.Header().Set("Content-Type", objInfo.ContentType)
// Set Etag if available.
if objInfo.MD5Sum != "" {
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
}
w.Header().Set("Content-Length", strconv.FormatInt(objInfo.Size, 10))
// Set all other user defined metadata.
for k, v := range objInfo.UserDefined {
w.Header().Set(k, v)
}
// for providing ranged content
if contentRange != nil {
if contentRange.start > 0 || contentRange.length > 0 {
// override content-length
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.length, 10))
w.Header().Set("Content-Range", contentRange.String())
w.WriteHeader(http.StatusPartialContent)
}
if contentRange != nil && contentRange.offsetBegin > -1 {
// Override content-length
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.getLength(), 10))
w.Header().Set("Content-Range", contentRange.String())
w.WriteHeader(http.StatusPartialContent)
}
}
+16 -7
View File
@@ -22,7 +22,7 @@ import (
)
// Parse bucket url queries
func getBucketResources(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string) {
func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string) {
prefix = values.Get("prefix")
marker = values.Get("marker")
delimiter = values.Get("delimiter")
@@ -35,6 +35,21 @@ func getBucketResources(values url.Values) (prefix, marker, delimiter string, ma
return
}
// Parse bucket url queries for ListObjects V2.
func getListObjectsV2Args(values url.Values) (prefix, token, startAfter, delimiter string, maxkeys int, encodingType string) {
prefix = values.Get("prefix")
startAfter = values.Get("start-after")
delimiter = values.Get("delimiter")
if values.Get("max-keys") != "" {
maxkeys, _ = strconv.Atoi(values.Get("max-keys"))
} else {
maxkeys = maxObjectList
}
encodingType = values.Get("encoding-type")
token = values.Get("continuation-token")
return
}
// Parse bucket url queries for ?uploads
func getBucketMultipartResources(values url.Values) (prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int, encodingType string) {
prefix = values.Get("prefix")
@@ -62,9 +77,3 @@ func getObjectResources(values url.Values) (uploadID string, partNumberMarker, m
encodingType = values.Get("encoding-type")
return
}
// Get upload id.
func getUploadID(values url.Values) (uploadID string) {
uploadID, _, _, _ = getObjectResources(values)
return
}
+54
View File
@@ -0,0 +1,54 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// File carries any specific responses constructed/necessary in
// multipart operations.
package main
import "net/http"
// writeErrorResponsePartTooSmall - function is used specifically to
// construct a proper error response during CompleteMultipartUpload
// when one of the parts is < 5MB.
// The requirement comes due to the fact that generic ErrorResponse
// XML doesn't carry the additional fields required to send this
// error. So we construct a new type which lies well within the scope
// of this function.
func writePartSmallErrorResponse(w http.ResponseWriter, r *http.Request, err PartTooSmall) {
// Represents additional fields necessary for ErrPartTooSmall S3 error.
type completeMultipartAPIError struct {
// Proposed size represents uploaded size of the part.
ProposedSize int64
// Minimum size allowed epresents the minimum size allowed per
// part. Defaults to 5MB.
MinSizeAllowed int64
// Part number of the part which is incorrect.
PartNumber int
// ETag of the part which is incorrect.
PartETag string
// Other default XML error responses.
APIErrorResponse
}
// Generate complete multipart error response.
errorResponse := getAPIErrorResponse(getAPIError(toAPIErrorCode(err)), r.URL.Path)
cmpErrResp := completeMultipartAPIError{err.PartSize, int64(5242880), err.PartNumber, err.PartETag, errorResponse}
encodedErrorResponse := encodeResponse(cmpErrResp)
// Write error body
w.Write(encodedErrorResponse)
w.(http.Flusher).Flush()
}
// Add any other multipart specific responses here.
+104 -7
View File
@@ -19,6 +19,7 @@ package main
import (
"encoding/xml"
"net/http"
"path"
"time"
)
@@ -65,6 +66,37 @@ type ListObjectsResponse struct {
Prefix string
}
// ListObjectsV2Response - format for list objects response.
type ListObjectsV2Response struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult" json:"-"`
CommonPrefixes []CommonPrefix
Contents []Object
Delimiter string
// Encoding type used to encode object keys in the response.
EncodingType string
// A flag that indicates whether or not ListObjects returned all of the results
// that satisfied the search criteria.
IsTruncated bool
StartAfter string
MaxKeys int
Name string
// When response is truncated (the IsTruncated element value in the response
// is true), you can use the key name in this field as marker in the subsequent
// request to get next set of objects. Server lists objects in alphabetical
// order Note: This element is returned only if you have delimiter request parameter
// specified. If response does not include the NextMaker and it is truncated,
// you can use the value of the last Key in the response as the marker in the
// subsequent request to get the next set of object keys.
ContinuationToken string
NextContinuationToken string
Prefix string
}
// Part container for part metadata.
type Part struct {
PartNumber int
@@ -194,6 +226,16 @@ type CompleteMultipartUploadResponse struct {
ETag string
}
// PostResponse container for completed post upload response
type PostResponse struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ PostResponse" json:"-"`
Location string
Bucket string
Key string
ETag string
}
// DeleteError structure.
type DeleteError struct {
Code string
@@ -214,7 +256,12 @@ type DeleteObjectsResponse struct {
// getLocation get URL location.
func getLocation(r *http.Request) string {
return r.URL.Path
return path.Clean(r.URL.Path) // Clean any trailing slashes.
}
// getObjectLocation gets the relative URL for an object
func getObjectLocation(bucketName string, key string) string {
return "/" + bucketName + "/" + key
}
// takes an array of Bucketmetadata information for serialization
@@ -244,8 +291,8 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
return data
}
// generates an ListObjects response for the said bucket with other enumerated options.
func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
// generates an ListObjectsV1 response for the said bucket with other enumerated options.
func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
var contents []Object
var prefixes []CommonPrefix
var owner = Owner{}
@@ -260,7 +307,7 @@ func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKe
continue
}
content.Key = object.Name
content.LastModified = object.ModifiedTime.UTC().Format(timeFormatAMZ)
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZ)
if object.MD5Sum != "" {
content.ETag = "\"" + object.MD5Sum + "\""
}
@@ -289,6 +336,51 @@ func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKe
return data
}
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter string, maxKeys int, resp ListObjectsInfo) ListObjectsV2Response {
var contents []Object
var prefixes []CommonPrefix
var owner = Owner{}
var data = ListObjectsV2Response{}
owner.ID = "minio"
owner.DisplayName = "minio"
for _, object := range resp.Objects {
var content = Object{}
if object.Name == "" {
continue
}
content.Key = object.Name
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZ)
if object.MD5Sum != "" {
content.ETag = "\"" + object.MD5Sum + "\""
}
content.Size = object.Size
content.StorageClass = "STANDARD"
content.Owner = owner
contents = append(contents, content)
}
// TODO - support EncodingType in xml decoding
data.Name = bucket
data.Contents = contents
data.StartAfter = startAfter
data.Delimiter = delimiter
data.Prefix = prefix
data.MaxKeys = maxKeys
data.ContinuationToken = token
data.NextContinuationToken = resp.NextMarker
data.IsTruncated = resp.IsTruncated
for _, prefix := range resp.Prefixes {
var prefixItem = CommonPrefix{}
prefixItem.Prefix = prefix
prefixes = append(prefixes, prefixItem)
}
data.CommonPrefixes = prefixes
return data
}
// generateCopyObjectResponse
func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse {
return CopyObjectResponse{
@@ -406,13 +498,18 @@ func writeSuccessNoContent(w http.ResponseWriter) {
// writeErrorRespone write error headers
func writeErrorResponse(w http.ResponseWriter, req *http.Request, errorCode APIErrorCode, resource string) {
error := getAPIError(errorCode)
// generate error response
errorResponse := getAPIErrorResponse(error, resource)
encodedErrorResponse := encodeResponse(errorResponse)
// set common headers
setCommonHeaders(w)
// write Header
w.WriteHeader(error.HTTPStatusCode)
writeErrorResponseNoHeader(w, req, errorCode, resource)
}
func writeErrorResponseNoHeader(w http.ResponseWriter, req *http.Request, errorCode APIErrorCode, resource string) {
error := getAPIError(errorCode)
// Generate error response.
errorResponse := getAPIErrorResponse(error, resource)
encodedErrorResponse := encodeResponse(errorResponse)
// HEAD should have no body, do not attempt to write to it
if req.Method != "HEAD" {
// write error body
+15 -7
View File
@@ -18,13 +18,13 @@ package main
import router "github.com/gorilla/mux"
// objectStorageAPI container for S3 compatible API.
type objectStorageAPI struct {
ObjectAPI ObjectAPI
// objectAPIHandler implements and provides http handlers for S3 API.
type objectAPIHandlers struct {
ObjectAPI ObjectLayer
}
// registerAPIRouter - registers S3 compatible APIs.
func registerAPIRouter(mux *router.Router, api objectStorageAPI) {
func registerAPIRouter(mux *router.Router, api objectAPIHandlers) {
// API Router
apiRouter := mux.NewRoute().PathPrefix("/").Subrouter()
@@ -48,7 +48,7 @@ func registerAPIRouter(mux *router.Router, api objectStorageAPI) {
// GetObject
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(api.GetObjectHandler)
// CopyObject
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/).*?").HandlerFunc(api.CopyObjectHandler)
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectHandler)
// PutObject
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(api.PutObjectHandler)
// DeleteObject
@@ -60,12 +60,20 @@ func registerAPIRouter(mux *router.Router, api objectStorageAPI) {
bucket.Methods("GET").HandlerFunc(api.GetBucketLocationHandler).Queries("location", "")
// GetBucketPolicy
bucket.Methods("GET").HandlerFunc(api.GetBucketPolicyHandler).Queries("policy", "")
// GetBucketNotification
bucket.Methods("GET").HandlerFunc(api.GetBucketNotificationHandler).Queries("notification", "")
// ListenBucketNotification
bucket.Methods("GET").HandlerFunc(api.ListenBucketNotificationHandler).Queries("notificationARN", "{notificationARN:.*}")
// ListMultipartUploads
bucket.Methods("GET").HandlerFunc(api.ListMultipartUploadsHandler).Queries("uploads", "")
// ListObjects
bucket.Methods("GET").HandlerFunc(api.ListObjectsHandler)
// ListObjectsV2
bucket.Methods("GET").HandlerFunc(api.ListObjectsV2Handler).Queries("list-type", "2")
// ListObjectsV1 (Legacy)
bucket.Methods("GET").HandlerFunc(api.ListObjectsV1Handler)
// PutBucketPolicy
bucket.Methods("PUT").HandlerFunc(api.PutBucketPolicyHandler).Queries("policy", "")
// PutBucketNotification
bucket.Methods("PUT").HandlerFunc(api.PutBucketNotificationHandler).Queries("notification", "")
// PutBucket
bucket.Methods("PUT").HandlerFunc(api.PutBucketHandler)
// HeadBucket
+10 -10
View File
@@ -1,6 +1,10 @@
# Operating system (build VM template)
os: Visual Studio 2015
# version format
version: "{build}"
# Operating system (build VM template)
os: Windows Server 2012 R2
# Platform.
platform: x64
clone_folder: c:\gopath\src\github.com\minio\minio
@@ -8,30 +12,26 @@ clone_folder: c:\gopath\src\github.com\minio\minio
# environment variables
environment:
GOPATH: c:\gopath
GO_EXTLINK_ENABLED: 0
GO15VENDOREXPERIMENT: 1
# scripts that run after cloning repository
install:
- '"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64'
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
- rd C:\Go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.6.windows-amd64.zip
- 7z x go1.6.windows-amd64.zip -oC:\ >nul
- go version
- go env
- cd %GOPATH%\src\github.com\minio\minio
# to run your custom scripts instead of automatic MSBuild
build_script:
- go test .
- go test -race .
- go test github.com/minio/minio/pkg...
- go test -race github.com/minio/minio/pkg...
- go test -coverprofile=coverage.txt -covermode=atomic
- go run buildscripts/gen-ldflags.go > temp.txt
- set /p BUILD_LDFLAGS=<temp.txt
- go build -ldflags="%BUILD_LDFLAGS%" -o %GOPATH%\bin\minio.exe
after_test:
- bash <(curl -s https://codecov.io/bash)
# to disable automatic tests
test: off
+76 -44
View File
@@ -1,5 +1,5 @@
/*
* Minio Cloud Storage, (C) 2015 Minio, Inc.
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,52 +19,43 @@ package main
import (
"bytes"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"io/ioutil"
"net/http"
"strings"
fastSha256 "github.com/minio/minio/pkg/crypto/sha256"
"github.com/minio/minio/pkg/probe"
)
// Verify if the request http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD"
func isRequestUnsignedPayload(r *http.Request) bool {
return r.Header.Get("x-amz-content-sha256") == unsignedPayload
}
// Verify if request has JWT.
func isRequestJWT(r *http.Request) bool {
if _, ok := r.Header["Authorization"]; ok {
if strings.HasPrefix(r.Header.Get("Authorization"), jwtAlgorithm) {
return true
}
}
return false
return strings.HasPrefix(r.Header.Get("Authorization"), jwtAlgorithm)
}
// Verify if request has AWS Signature Version '4'.
func isRequestSignatureV4(r *http.Request) bool {
if _, ok := r.Header["Authorization"]; ok {
if strings.HasPrefix(r.Header.Get("Authorization"), signV4Algorithm) {
return true
}
}
return false
return strings.HasPrefix(r.Header.Get("Authorization"), signV4Algorithm)
}
// Verify if request has AWS Presignature Version '4'.
// Verify if request has AWS PreSign Version '4'.
func isRequestPresignedSignatureV4(r *http.Request) bool {
if _, ok := r.URL.Query()["X-Amz-Credential"]; ok {
return true
}
return false
_, ok := r.URL.Query()["X-Amz-Credential"]
return ok
}
// Verify if request has AWS Post policy Signature Version '4'.
func isRequestPostPolicySignatureV4(r *http.Request) bool {
if _, ok := r.Header["Content-Type"]; ok {
if strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") && r.Method == "POST" {
return true
}
}
return false
return strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") && r.Method == "POST"
}
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
func isRequestSignStreamingV4(r *http.Request) bool {
return r.Header.Get("x-amz-content-sha256") == streamingContentSHA256 && r.Method == "PUT"
}
// Authorization type.
@@ -76,13 +67,16 @@ const (
authTypeAnonymous
authTypePresigned
authTypePostPolicy
authTypeStreamingSigned
authTypeSigned
authTypeJWT
)
// Get request authentication type.
func getRequestAuthType(r *http.Request) authType {
if isRequestSignatureV4(r) {
if isRequestSignStreamingV4(r) {
return authTypeStreamingSigned
} else if isRequestSignatureV4(r) {
return authTypeSigned
} else if isRequestPresignedSignatureV4(r) {
return authTypePresigned
@@ -98,7 +92,7 @@ func getRequestAuthType(r *http.Request) authType {
// sum256 calculate sha256 sum for an input byte array
func sum256(data []byte) []byte {
hash := fastSha256.New()
hash := sha256.New()
hash.Write(data)
return hash.Sum(nil)
}
@@ -113,12 +107,10 @@ func sumMD5(data []byte) []byte {
// Verify if request has valid AWS Signature Version '4'.
func isReqAuthenticated(r *http.Request) (s3Error APIErrorCode) {
if r == nil {
errorIf(probe.NewError(errInvalidArgument), "HTTP request cannot be empty.", nil)
return ErrInternalError
}
payload, e := ioutil.ReadAll(r.Body)
if e != nil {
errorIf(probe.NewError(e), "Unable to read HTTP body.", nil)
payload, err := ioutil.ReadAll(r.Body)
if err != nil {
return ErrInternalError
}
// Verify Content-Md5, if payload is set.
@@ -130,16 +122,39 @@ func isReqAuthenticated(r *http.Request) (s3Error APIErrorCode) {
// Populate back the payload.
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
validateRegion := true // Validate region.
var sha256sum string
// Skips calculating sha256 on the payload on server,
// if client requested for it.
if skipContentSha256Cksum(r) {
sha256sum = unsignedPayload
} else {
sha256sum = hex.EncodeToString(sum256(payload))
}
if isRequestSignatureV4(r) {
return doesSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
return doesSignatureMatch(sha256sum, r, validateRegion)
} else if isRequestPresignedSignatureV4(r) {
return doesPresignedSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
return doesPresignedSignatureMatch(sha256sum, r, validateRegion)
}
return ErrAccessDenied
}
// authHandler - handles all the incoming authorization headers and
// validates them if possible.
// checkAuth - checks for conditions satisfying the authorization of
// the incoming request. Request should be either Presigned or Signed
// in accordance with AWS S3 Signature V4 requirements. ErrAccessDenied
// is returned for unhandled auth type. Once the auth type is indentified
// request headers and body are used to calculate the signature validating
// the client signature present in request.
func checkAuth(r *http.Request) APIErrorCode {
aType := getRequestAuthType(r)
if aType != authTypePresigned && aType != authTypeSigned {
// For all unhandled auth types return error AccessDenied.
return ErrAccessDenied
}
// Validates the request for both Presigned and Signed.
return isReqAuthenticated(r)
}
// authHandler - handles all the incoming authorization headers and validates them if possible.
type authHandler struct {
handler http.Handler
}
@@ -149,23 +164,40 @@ func setAuthHandler(h http.Handler) http.Handler {
return authHandler{h}
}
// List of all support S3 auth types.
var supportedS3AuthTypes = []authType{
authTypeAnonymous,
authTypePresigned,
authTypeSigned,
authTypePostPolicy,
authTypeStreamingSigned,
}
// Validate if the authType is valid and supported.
func isSupportedS3AuthType(aType authType) bool {
for _, a := range supportedS3AuthTypes {
if a == aType {
return true
}
}
return false
}
// handler for validating incoming authorization headers.
func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch getRequestAuthType(r) {
case authTypeAnonymous, authTypePresigned, authTypeSigned, authTypePostPolicy:
// Let top level caller validate for anonymous and known
// signed requests.
aType := getRequestAuthType(r)
if isSupportedS3AuthType(aType) {
// Let top level caller validate for anonymous and known signed requests.
a.handler.ServeHTTP(w, r)
return
case authTypeJWT:
} else if aType == authTypeJWT {
// Validate Authorization header if its valid for JWT request.
if !isJWTReqAuthenticated(r) {
w.WriteHeader(http.StatusUnauthorized)
return
}
a.handler.ServeHTTP(w, r)
default:
writeErrorResponse(w, r, ErrSignatureVersionNotSupported, r.URL.Path)
return
}
writeErrorResponse(w, r, ErrSignatureVersionNotSupported, r.URL.Path)
}
+206
View File
@@ -0,0 +1,206 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"io"
"net/http"
"testing"
)
// Test all s3 supported auth types.
func TestS3SupportedAuthType(t *testing.T) {
type testCase struct {
authT authType
pass bool
}
// List of all valid and invalid test cases.
testCases := []testCase{
// Test 1 - supported s3 type anonymous.
{
authT: authTypeAnonymous,
pass: true,
},
// Test 2 - supported s3 type presigned.
{
authT: authTypePresigned,
pass: true,
},
// Test 3 - supported s3 type signed.
{
authT: authTypeSigned,
pass: true,
},
// Test 4 - supported s3 type with post policy.
{
authT: authTypePostPolicy,
pass: true,
},
// Test 5 - supported s3 type with streaming signed.
{
authT: authTypeStreamingSigned,
pass: true,
},
// Test 6 - JWT is not supported s3 type.
{
authT: authTypeJWT,
pass: false,
},
// Test 7 - unknown auth header is not supported s3 type.
{
authT: authTypeUnknown,
pass: false,
},
// Test 8 - some new auth type is not supported s3 type.
{
authT: authType(7),
pass: false,
},
}
// Validate all the test cases.
for i, tt := range testCases {
ok := isSupportedS3AuthType(tt.authT)
if ok != tt.pass {
t.Errorf("Test %d:, Expected %t, got %t", i+1, tt.pass, ok)
}
}
}
// TestIsRequestUnsignedPayload - Test validates the Unsigned payload detection logic.
func TestIsRequestUnsignedPayload(t *testing.T) {
testCases := []struct {
inputAmzContentHeader string
expectedResult bool
}{
// Test case - 1.
// Test case with "X-Amz-Content-Sha256" header set to empty value.
{"", false},
// Test case - 2.
// Test case with "X-Amz-Content-Sha256" header set to "UNSIGNED-PAYLOAD"
// The payload is flagged as unsigned When "X-Amz-Content-Sha256" header is set to "UNSIGNED-PAYLOAD".
{"UNSIGNED-PAYLOAD", true},
// Test case - 3.
// set to a random value.
{"abcd", false},
}
// creating an input HTTP request.
// Only the headers are relevant for this particular test.
inputReq, err := http.NewRequest("GET", "http://example.com", nil)
if err != nil {
t.Fatalf("Error initializing input HTTP request: %v", err)
}
for i, testCase := range testCases {
inputReq.Header.Set("X-Amz-Content-Sha256", testCase.inputAmzContentHeader)
actualResult := isRequestUnsignedPayload(inputReq)
if testCase.expectedResult != actualResult {
t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult)
}
}
}
// TestIsRequestPresignedSignatureV4 - Test validates the logic for presign signature verision v4 detection.
func TestIsRequestPresignedSignatureV4(t *testing.T) {
testCases := []struct {
inputQueryKey string
inputQueryValue string
expectedResult bool
}{
// Test case - 1.
// Test case with query key ""X-Amz-Credential" set.
{"", "", false},
// Test case - 2.
{"X-Amz-Credential", "", true},
// Test case - 3.
{"X-Amz-Content-Sha256", "", false},
}
for i, testCase := range testCases {
// creating an input HTTP request.
// Only the query parameters are relevant for this particular test.
inputReq, err := http.NewRequest("GET", "http://example.com", nil)
if err != nil {
t.Fatalf("Error initializing input HTTP request: %v", err)
}
q := inputReq.URL.Query()
q.Add(testCase.inputQueryKey, testCase.inputQueryValue)
inputReq.URL.RawQuery = q.Encode()
actualResult := isRequestPresignedSignatureV4(inputReq)
if testCase.expectedResult != actualResult {
t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult)
}
}
}
// Provides a fully populated http request instance, fails otherwise.
func mustNewRequest(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
req, err := newTestRequest(method, urlStr, contentLength, body)
if err != nil {
t.Fatalf("Unable to initialize new http request %s", err)
}
return req
}
// This is similar to mustNewRequest but additionally the request
// is signed with AWS Signature V4, fails if not able to do so.
func mustNewSignedRequest(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
req := mustNewRequest(method, urlStr, contentLength, body, t)
cred := serverConfig.GetCredential()
if err := signRequest(req, cred.AccessKeyID, cred.SecretAccessKey); err != nil {
t.Fatalf("Unable to inititalized new signed http request %s", err)
}
return req
}
// Tests is requested authenticated function, tests replies for s3 errors.
func TestIsReqAuthenticated(t *testing.T) {
path, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("unable initialize config file, %s", err)
}
defer removeAll(path)
serverConfig.SetCredential(credential{"myuser", "mypassword"})
// List of test cases for validating http request authentication.
testCases := []struct {
req *http.Request
s3Error APIErrorCode
}{
// When request is nil, internal error is returned.
{nil, ErrInternalError},
// When request is unsigned, access denied is returned.
{mustNewRequest("GET", "http://localhost:9000", 0, nil, t), ErrAccessDenied},
// When request is properly signed, but has bad Content-MD5 header.
{mustNewSignedRequest("PUT", "http://localhost:9000", 5, bytes.NewReader([]byte("hello")), t), ErrBadDigest},
// When request is properly signed, error is none.
{mustNewSignedRequest("GET", "http://localhost:9000", 0, nil, t), ErrNone},
}
// Validates all testcases.
for _, testCase := range testCases {
if testCase.s3Error == ErrBadDigest {
testCase.req.Header.Set("Content-Md5", "garbage")
}
if s3Error := isReqAuthenticated(testCase.req); s3Error != testCase.s3Error {
t.Fatalf("Unexpected s3error returned wanted %d, got %d", testCase.s3Error, s3Error)
}
}
}
+370
View File
@@ -0,0 +1,370 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"io/ioutil"
"math"
"math/rand"
"strconv"
"testing"
"time"
)
// Benchmark utility functions for ObjectLayer.PutObject().
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
var err error
// obtains random bucket name.
bucket := getRandomBucketName()
// create bucket.
err = obj.MakeBucket(bucket)
if err != nil {
b.Fatal(err)
}
// PutObject returns md5Sum of the object inserted.
// md5Sum variable is assigned with that value.
var md5Sum string
// get text data generated for number of bytes equal to object size.
textData := generateBytesData(objSize)
// generate md5sum for the generated data.
// md5sum of the data to written is required as input for PutObject.
hasher := md5.New()
hasher.Write([]byte(textData))
metadata := make(map[string]string)
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
b.ReportAllocs()
// the actual benchmark for PutObject starts here. Reset the benchmark timer.
b.ResetTimer()
for i := 0; i < b.N; i++ {
// insert the object.
md5Sum, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata)
if err != nil {
b.Fatal(err)
}
if md5Sum != metadata["md5Sum"] {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
}
}
// Benchmark ends here. Stop timer.
b.StopTimer()
}
// Benchmark utility functions for ObjectLayer.PutObjectPart().
// Creates Object layer setup ( MakeBucket ) and then runs the PutObjectPart benchmark.
func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
var err error
// obtains random bucket name.
bucket := getRandomBucketName()
object := getRandomObjectName()
// create bucket.
err = obj.MakeBucket(bucket)
if err != nil {
b.Fatal(err)
}
objSize := 128 * 1024 * 1024
// PutObjectPart returns md5Sum of the object inserted.
// md5Sum variable is assigned with that value.
var md5Sum, uploadID string
// get text data generated for number of bytes equal to object size.
textData := generateBytesData(objSize)
// generate md5sum for the generated data.
// md5sum of the data to written is required as input for NewMultipartUpload.
hasher := md5.New()
hasher.Write([]byte(textData))
metadata := make(map[string]string)
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
uploadID, err = obj.NewMultipartUpload(bucket, object, metadata)
if err != nil {
b.Fatal(err)
}
var textPartData []byte
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
b.ReportAllocs()
// the actual benchmark for PutObjectPart starts here. Reset the benchmark timer.
b.ResetTimer()
for i := 0; i < b.N; i++ {
// insert the object.
totalPartsNR := int(math.Ceil(float64(objSize) / float64(partSize)))
for j := 0; j < totalPartsNR; j++ {
hasher.Reset()
if j < totalPartsNR-1 {
textPartData = textData[j*partSize : (j+1)*partSize-1]
} else {
textPartData = textData[j*partSize:]
}
hasher.Write([]byte(textPartData))
metadata := make(map[string]string)
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
md5Sum, err = obj.PutObjectPart(bucket, object, uploadID, j, int64(len(textPartData)), bytes.NewBuffer(textPartData), metadata["md5Sum"])
if err != nil {
b.Fatal(err)
}
if md5Sum != metadata["md5Sum"] {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
}
}
}
// Benchmark ends here. Stop timer.
b.StopTimer()
}
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function.
func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
// create a temp XL/FS backend.
objLayer, disks, err := makeTestBackend(instanceType)
if err != nil {
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
}
// cleaning up the backend by removing all the directories and files created on function return.
defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark.
runPutObjectPartBenchmark(b, objLayer, objSize)
}
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function.
func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
// create a temp XL/FS backend.
objLayer, disks, err := makeTestBackend(instanceType)
if err != nil {
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
}
// cleaning up the backend by removing all the directories and files created on function return.
defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark.
runPutObjectBenchmark(b, objLayer, objSize)
}
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for put object.
func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) {
// create a temp XL/FS backend.
objLayer, disks, err := makeTestBackend(instanceType)
if err != nil {
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
}
// cleaning up the backend by removing all the directories and files created on function return.
defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark.
runPutObjectBenchmarkParallel(b, objLayer, objSize)
}
// Benchmark utility functions for ObjectLayer.GetObject().
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
var err error
// obtains random bucket name.
bucket := getRandomBucketName()
// create bucket.
err = obj.MakeBucket(bucket)
if err != nil {
b.Fatal(err)
}
// PutObject returns md5Sum of the object inserted.
// md5Sum variable is assigned with that value.
var md5Sum string
for i := 0; i < 10; i++ {
// get text data generated for number of bytes equal to object size.
textData := generateBytesData(objSize)
// generate md5sum for the generated data.
// md5sum of the data to written is required as input for PutObject.
// PutObject is the functions which writes the data onto the FS/XL backend.
hasher := md5.New()
hasher.Write([]byte(textData))
metadata := make(map[string]string)
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
// insert the object.
md5Sum, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata)
if err != nil {
b.Fatal(err)
}
if md5Sum != metadata["md5Sum"] {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
}
}
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
b.ReportAllocs()
// the actual benchmark for GetObject starts here. Reset the benchmark timer.
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buffer = new(bytes.Buffer)
err = obj.GetObject(bucket, "object"+strconv.Itoa(i%10), 0, int64(objSize), buffer)
if err != nil {
b.Error(err)
}
}
// Benchmark ends here. Stop timer.
b.StopTimer()
}
// randomly picks a character and returns its equivalent byte array.
func getRandomByte() []byte {
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// seeding the random number generator.
rand.Seed(time.Now().UnixNano())
var b byte
// pick a character randomly.
b = letterBytes[rand.Intn(len(letterBytes))]
return []byte{b}
}
// picks a random byte and repeats it to size bytes.
func generateBytesData(size int) []byte {
// repeat the random character chosen size
return bytes.Repeat(getRandomByte(), size)
}
// creates XL/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function.
func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
// create a temp XL/FS backend.
objLayer, disks, err := makeTestBackend(instanceType)
if err != nil {
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
}
// cleaning up the backend by removing all the directories and files created.
defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark.
runGetObjectBenchmark(b, objLayer, objSize)
}
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() .
func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) {
// create a temp XL/FS backend.
objLayer, disks, err := makeTestBackend(instanceType)
if err != nil {
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
}
// cleaning up the backend by removing all the directories and files created.
defer removeRoots(disks)
// uses *testing.B and the object Layer to run the benchmark.
runGetObjectBenchmarkParallel(b, objLayer, objSize)
}
// Parallel benchmark utility functions for ObjectLayer.PutObject().
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
var err error
// obtains random bucket name.
bucket := getRandomBucketName()
// create bucket.
err = obj.MakeBucket(bucket)
if err != nil {
b.Fatal(err)
}
// PutObject returns md5Sum of the object inserted.
// md5Sum variable is assigned with that value.
var md5Sum string
// get text data generated for number of bytes equal to object size.
textData := generateBytesData(objSize)
// generate md5sum for the generated data.
// md5sum of the data to written is required as input for PutObject.
hasher := md5.New()
hasher.Write([]byte(textData))
metadata := make(map[string]string)
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
b.ReportAllocs()
// the actual benchmark for PutObject starts here. Reset the benchmark timer.
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
// insert the object.
md5Sum, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata)
if err != nil {
b.Fatal(err)
}
if md5Sum != metadata["md5Sum"] {
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", md5Sum, metadata["md5Sum"])
}
i++
}
})
// Benchmark ends here. Stop timer.
b.StopTimer()
}
// Parallel benchmark utility functions for ObjectLayer.GetObject().
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
var err error
// obtains random bucket name.
bucket := getRandomBucketName()
// create bucket.
err = obj.MakeBucket(bucket)
if err != nil {
b.Fatal(err)
}
// PutObject returns md5Sum of the object inserted.
// md5Sum variable is assigned with that value.
var md5Sum string
for i := 0; i < 10; i++ {
// get text data generated for number of bytes equal to object size.
textData := generateBytesData(objSize)
// generate md5sum for the generated data.
// md5sum of the data to written is required as input for PutObject.
// PutObject is the functions which writes the data onto the FS/XL backend.
hasher := md5.New()
hasher.Write([]byte(textData))
metadata := make(map[string]string)
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
// insert the object.
md5Sum, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata)
if err != nil {
b.Fatal(err)
}
if md5Sum != metadata["md5Sum"] {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
}
}
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
b.ReportAllocs()
// the actual benchmark for GetObject starts here. Reset the benchmark timer.
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
err = obj.GetObject(bucket, "object"+strconv.Itoa(i), 0, int64(objSize), ioutil.Discard)
if err != nil {
b.Error(err)
}
i++
if i == 10 {
i = 0
}
}
})
// Benchmark ends here. Stop timer.
b.StopTimer()
}
+167
View File
@@ -0,0 +1,167 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"net/http"
"strings"
"github.com/gorilla/mux"
)
// Validate all the ListObjects query arguments, returns an APIErrorCode
// if one of the args do not meet the required conditions.
// Special conditions required by Minio server are as below
// - delimiter if set should be equal to '/', otherwise the request is rejected.
// - marker if set should have a common prefix with 'prefix' param, otherwise
// the request is rejected.
func listObjectsValidateArgs(prefix, marker, delimiter string, maxKeys int) APIErrorCode {
// Max keys cannot be negative.
if maxKeys < 0 {
return ErrInvalidMaxKeys
}
/// Minio special conditions for ListObjects.
// Verify if delimiter is anything other than '/', which we do not support.
if delimiter != "" && delimiter != "/" {
return ErrNotImplemented
}
// Marker is set validate pre-condition.
if marker != "" {
// Marker not common with prefix is not implemented.
if !strings.HasPrefix(marker, prefix) {
return ErrNotImplemented
}
}
// Success.
return ErrNone
}
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
// --------------------------
// This implementation of the GET operation returns some or all (up to 1000)
// of the objects in a bucket. You can use the request parameters as selection
// criteria to return a subset of the objects in a bucket.
//
// NOTE: It is recommended that this API to be used for application development.
// Minio continues to support ListObjectsV1 for supporting legacy tools.
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
switch getRequestAuthType(r) {
default:
// For all unknown auth types return error.
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
return
case authTypeAnonymous:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucket", r.URL); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
case authTypeSigned, authTypePresigned:
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
}
// Extract all the listObjectsV2 query params to their native values.
prefix, token, startAfter, delimiter, maxKeys, _ := getListObjectsV2Args(r.URL.Query())
// In ListObjectsV2 'continuation-token' is the marker.
marker := token
// Check if 'continuation-token' is empty.
if token == "" {
// Then we need to use 'start-after' as marker instead.
marker = startAfter
}
// Validate all the query params before beginning to serve the request.
if s3Error := listObjectsValidateArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
// Inititate a list objects operation based on the input params.
// On success would return back ListObjectsInfo object to be
// marshalled into S3 compatible XML header.
listObjectsInfo, err := api.ObjectAPI.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
if err != nil {
errorIf(err, "Unable to list objects.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
response := generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter, maxKeys, listObjectsInfo)
// Write headers
setCommonHeaders(w)
// Write success response.
writeSuccessResponse(w, encodeResponse(response))
}
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
// --------------------------
// This implementation of the GET operation returns some or all (up to 1000)
// of the objects in a bucket. You can use the request parameters as selection
// criteria to return a subset of the objects in a bucket.
//
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
switch getRequestAuthType(r) {
default:
// For all unknown auth types return error.
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
return
case authTypeAnonymous:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucket", r.URL); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
case authTypeSigned, authTypePresigned:
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
}
// Extract all the litsObjectsV1 query params to their native values.
prefix, marker, delimiter, maxKeys, _ := getListObjectsV1Args(r.URL.Query())
// Validate all the query params before beginning to serve the request.
if s3Error := listObjectsValidateArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
// Inititate a list objects operation based on the input params.
// On success would return back ListObjectsInfo object to be
// marshalled into S3 compatible XML header.
listObjectsInfo, err := api.ObjectAPI.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
if err != nil {
errorIf(err, "Unable to list objects.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
// Write headers
setCommonHeaders(w)
// Write success response.
writeSuccessResponse(w, encodeResponse(response))
}
+136 -357
View File
@@ -17,45 +17,29 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"path"
"strings"
mux "github.com/gorilla/mux"
"github.com/minio/minio/pkg/probe"
)
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
func enforceBucketPolicy(action string, bucket string, reqURL *url.URL) (s3Error APIErrorCode) {
// Read saved bucket policy.
policy, err := readBucketPolicy(bucket)
if err != nil {
errorIf(err.Trace(bucket), "GetBucketPolicy failed.", nil)
switch err.ToGoError().(type) {
case BucketNotFound:
return ErrNoSuchBucket
case BucketNameInvalid:
return ErrInvalidBucketName
default:
// For any other error just return AccessDenied.
return ErrAccessDenied
}
// Enforces bucket policies for a bucket for a given tatusaction.
func enforceBucketPolicy(bucket string, action string, reqURL *url.URL) (s3Error APIErrorCode) {
if !IsValidBucketName(bucket) {
return ErrInvalidBucketName
}
// Parse the saved policy.
bucketPolicy, e := parseBucketPolicy(policy)
if e != nil {
errorIf(probe.NewError(e), "Parse policy failed.", nil)
// Fetch bucket policy, if policy is not set return access denied.
policy := globalBucketPolicies.GetBucketPolicy(bucket)
if policy == nil {
return ErrAccessDenied
}
// Construct resource in 'arn:aws:s3:::examplebucket' format.
// Construct resource in 'arn:aws:s3:::examplebucket/object' format.
resource := AWSResourcePrefix + strings.TrimPrefix(reqURL.Path, "/")
// Get conditions for policy verification.
@@ -65,7 +49,7 @@ func enforceBucketPolicy(action string, bucket string, reqURL *url.URL) (s3Error
}
// Validate action, resource and conditions with current policy statements.
if !bucketPolicyEvalStatements(action, resource, conditions, bucketPolicy.Statements) {
if !bucketPolicyEvalStatements(action, resource, conditions, policy.Statements) {
return ErrAccessDenied
}
return ErrNone
@@ -74,7 +58,7 @@ func enforceBucketPolicy(action string, bucket string, reqURL *url.URL) (s3Error
// GetBucketLocationHandler - GET Bucket location.
// -------------------------
// This operation returns bucket location.
func (api objectStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
@@ -85,49 +69,20 @@ func (api objectStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, r *h
return
case authTypeAnonymous:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
if s3Error := enforceBucketPolicy("s3:GetBucketLocation", bucket, r.URL); s3Error != ErrNone {
if s3Error := enforceBucketPolicy(bucket, "s3:GetBucketLocation", r.URL); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
case authTypeSigned, authTypePresigned:
payload, e := ioutil.ReadAll(r.Body)
if e != nil {
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
return
}
// Verify Content-Md5, if payload is set.
if r.Header.Get("Content-Md5") != "" {
if r.Header.Get("Content-Md5") != base64.StdEncoding.EncodeToString(sumMD5(payload)) {
writeErrorResponse(w, r, ErrBadDigest, r.URL.Path)
return
}
}
// Populate back the payload.
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
var s3Error APIErrorCode // API error code.
validateRegion := false // Validate region.
if isRequestSignatureV4(r) {
s3Error = doesSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
} else if isRequestPresignedSignatureV4(r) {
s3Error = doesPresignedSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
}
if s3Error != ErrNone {
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
}
_, err := api.ObjectAPI.GetBucketInfo(bucket)
if err != nil {
errorIf(err.Trace(), "GetBucketInfo failed.", nil)
switch err.ToGoError().(type) {
case BucketNotFound:
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
case BucketNameInvalid:
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
default:
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
}
if _, err := api.ObjectAPI.GetBucketInfo(bucket); err != nil {
errorIf(err, "Unable to fetch bucket info.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
@@ -152,7 +107,7 @@ func (api objectStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, r *h
// completed or aborted. This operation returns at most 1,000 multipart
// uploads in the response.
//
func (api objectStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
@@ -163,7 +118,7 @@ func (api objectStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, r
return
case authTypeAnonymous:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/mpuAndPermissions.html
if s3Error := enforceBucketPolicy("s3:ListBucketMultipartUploads", bucket, r.URL); s3Error != ErrNone {
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucketMultipartUploads", r.URL); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
@@ -180,32 +135,17 @@ func (api objectStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, r
return
}
if keyMarker != "" {
// Unescape keyMarker string
keyMarkerUnescaped, e := url.QueryUnescape(keyMarker)
if e != nil {
if e != nil {
// Return 'NoSuchKey' to indicate invalid marker key.
writeErrorResponse(w, r, ErrNoSuchKey, r.URL.Path)
return
}
keyMarker = keyMarkerUnescaped
// Marker not common with prefix is not implemented.
if !strings.HasPrefix(keyMarker, prefix) {
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
return
}
// Marker not common with prefix is not implemented.
if !strings.HasPrefix(keyMarker, prefix) {
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
return
}
}
listMultipartsInfo, err := api.ObjectAPI.ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
if err != nil {
errorIf(err.Trace(), "ListMultipartUploads failed.", nil)
switch err.ToGoError().(type) {
case BucketNotFound:
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
default:
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
}
errorIf(err, "Unable to list multipart uploads.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
// generate response
@@ -217,144 +157,35 @@ func (api objectStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, r
writeSuccessResponse(w, encodedSuccessResponse)
}
// ListObjectsHandler - GET Bucket (List Objects)
// -- -----------------------
// This implementation of the GET operation returns some or all (up to 1000)
// of the objects in a bucket. You can use the request parameters as selection
// criteria to return a subset of the objects in a bucket.
//
func (api objectStorageAPI) ListObjectsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
switch getRequestAuthType(r) {
default:
// For all unknown auth types return error.
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
return
case authTypeAnonymous:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
if s3Error := enforceBucketPolicy("s3:ListBucket", bucket, r.URL); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
case authTypeSigned, authTypePresigned:
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
}
// TODO handle encoding type.
prefix, marker, delimiter, maxkeys, _ := getBucketResources(r.URL.Query())
if maxkeys < 0 {
writeErrorResponse(w, r, ErrInvalidMaxKeys, r.URL.Path)
return
}
// Verify if delimiter is anything other than '/', which we do not support.
if delimiter != "" && delimiter != "/" {
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
return
}
// If marker is set unescape.
if marker != "" {
// Try to unescape marker.
markerUnescaped, e := url.QueryUnescape(marker)
if e != nil {
// Return 'NoSuchKey' to indicate invalid marker key.
writeErrorResponse(w, r, ErrNoSuchKey, r.URL.Path)
return
}
marker = markerUnescaped
// Marker not common with prefix is not implemented.
if !strings.HasPrefix(marker, prefix) {
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
return
}
}
listObjectsInfo, err := api.ObjectAPI.ListObjects(bucket, prefix, marker, delimiter, maxkeys)
if err == nil {
// generate response
response := generateListObjectsResponse(bucket, prefix, marker, delimiter, maxkeys, listObjectsInfo)
encodedSuccessResponse := encodeResponse(response)
// Write headers
setCommonHeaders(w)
// Write success response.
writeSuccessResponse(w, encodedSuccessResponse)
return
}
switch err.ToGoError().(type) {
case BucketNameInvalid:
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
case BucketNotFound:
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
case ObjectNotFound:
writeErrorResponse(w, r, ErrNoSuchKey, r.URL.Path)
case ObjectNameInvalid:
writeErrorResponse(w, r, ErrNoSuchKey, r.URL.Path)
default:
errorIf(err.Trace(), "ListObjects failed.", nil)
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
}
}
// ListBucketsHandler - GET Service
// -----------
// This implementation of the GET operation returns a list of all buckets
// owned by the authenticated sender of the request.
func (api objectStorageAPI) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
// List buckets does not support bucket policies.
switch getRequestAuthType(r) {
default:
// For all unknown auth types return error.
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
// List buckets does not support bucket policies, no need to enforce it.
if s3Error := checkAuth(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
case authTypeSigned, authTypePresigned:
payload, e := ioutil.ReadAll(r.Body)
if e != nil {
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
return
}
// Verify Content-Md5, if payload is set.
if r.Header.Get("Content-Md5") != "" {
if r.Header.Get("Content-Md5") != base64.StdEncoding.EncodeToString(sumMD5(payload)) {
writeErrorResponse(w, r, ErrBadDigest, r.URL.Path)
return
}
}
// Populate back the payload.
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
var s3Error APIErrorCode // API error code.
validateRegion := false // Validate region.
if isRequestSignatureV4(r) {
s3Error = doesSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
} else if isRequestPresignedSignatureV4(r) {
s3Error = doesPresignedSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
}
if s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
}
bucketsInfo, err := api.ObjectAPI.ListBuckets()
if err == nil {
// generate response
response := generateListBucketsResponse(bucketsInfo)
encodedSuccessResponse := encodeResponse(response)
// write headers
setCommonHeaders(w)
// write response
writeSuccessResponse(w, encodedSuccessResponse)
if err != nil {
errorIf(err, "Unable to list buckets.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
errorIf(err.Trace(), "ListBuckets failed.", nil)
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
// Generate response.
response := generateListBucketsResponse(bucketsInfo)
encodedSuccessResponse := encodeResponse(response)
// Write headers.
setCommonHeaders(w)
// Write response.
writeSuccessResponse(w, encodedSuccessResponse)
}
// DeleteMultipleObjectsHandler - deletes multiple objects.
func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
@@ -365,7 +196,7 @@ func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter,
return
case authTypeAnonymous:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
if s3Error := enforceBucketPolicy("s3:DeleteObject", bucket, r.URL); s3Error != ErrNone {
if s3Error := enforceBucketPolicy(bucket, "s3:DeleteObject", r.URL); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
@@ -394,16 +225,16 @@ func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter,
deleteXMLBytes := make([]byte, r.ContentLength)
// Read incoming body XML bytes.
_, e := io.ReadFull(r.Body, deleteXMLBytes)
if e != nil {
errorIf(probe.NewError(e), "DeleteMultipleObjects failed.", nil)
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
errorIf(err, "Unable to read HTTP body.")
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
return
}
// Unmarshal list of keys to be deleted.
deleteObjects := &DeleteObjectsRequest{}
if e := xml.Unmarshal(deleteXMLBytes, deleteObjects); e != nil {
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
errorIf(err, "Unable to unmarshal delete objects request XML.")
writeErrorResponse(w, r, ErrMalformedXML, r.URL.Path)
return
}
@@ -418,39 +249,12 @@ func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter,
ObjectName: object.ObjectName,
})
} else {
errorIf(err.Trace(object.ObjectName), "DeleteObject failed.", nil)
switch err.ToGoError().(type) {
case BucketNameInvalid:
deleteErrors = append(deleteErrors, DeleteError{
Code: errorCodeResponse[ErrInvalidBucketName].Code,
Message: errorCodeResponse[ErrInvalidBucketName].Description,
Key: object.ObjectName,
})
case BucketNotFound:
deleteErrors = append(deleteErrors, DeleteError{
Code: errorCodeResponse[ErrNoSuchBucket].Code,
Message: errorCodeResponse[ErrNoSuchBucket].Description,
Key: object.ObjectName,
})
case ObjectNotFound:
deleteErrors = append(deleteErrors, DeleteError{
Code: errorCodeResponse[ErrNoSuchKey].Code,
Message: errorCodeResponse[ErrNoSuchKey].Description,
Key: object.ObjectName,
})
case ObjectNameInvalid:
deleteErrors = append(deleteErrors, DeleteError{
Code: errorCodeResponse[ErrNoSuchKey].Code,
Message: errorCodeResponse[ErrNoSuchKey].Description,
Key: object.ObjectName,
})
default:
deleteErrors = append(deleteErrors, DeleteError{
Code: errorCodeResponse[ErrInternalError].Code,
Message: errorCodeResponse[ErrInternalError].Description,
Key: object.ObjectName,
})
}
errorIf(err, "Unable to delete object.")
deleteErrors = append(deleteErrors, DeleteError{
Code: errorCodeResponse[toAPIErrorCode(err)].Code,
Message: errorCodeResponse[toAPIErrorCode(err)].Description,
Key: object.ObjectName,
})
}
}
// Generate response
@@ -465,35 +269,27 @@ func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter,
// PutBucketHandler - PUT Bucket
// ----------
// This implementation of the PUT operation creates a new bucket for authenticated request
func (api objectStorageAPI) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
// PutBucket does not support policies, use checkAuth to validate signature.
if s3Error := checkAuth(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
// Set http request for signature.
switch getRequestAuthType(r) {
default:
// For all unknown auth types return error.
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
return
case authTypePresigned, authTypeSigned:
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
// Validate if incoming location constraint is valid, reject
// requests which do not follow valid region requirements.
if s3Error := isValidLocationConstraint(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
}
// Make bucket.
// Proceed to creating a bucket.
err := api.ObjectAPI.MakeBucket(bucket)
if err != nil {
errorIf(err.Trace(), "MakeBucket failed.", nil)
switch err.ToGoError().(type) {
case BucketNameInvalid:
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
case BucketExists:
writeErrorResponse(w, r, ErrBucketAlreadyExists, r.URL.Path)
default:
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
}
errorIf(err, "Unable to create a bucket.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
// Make sure to add Location information here only for bucket
@@ -501,48 +297,23 @@ func (api objectStorageAPI) PutBucketHandler(w http.ResponseWriter, r *http.Requ
writeSuccessResponse(w, nil)
}
func extractHTTPFormValues(reader *multipart.Reader) (io.Reader, map[string]string, *probe.Error) {
/// HTML Form values
formValues := make(map[string]string)
filePart := new(bytes.Buffer)
var e error
for e == nil {
var part *multipart.Part
part, e = reader.NextPart()
if part != nil {
if part.FileName() == "" {
buffer, e := ioutil.ReadAll(part)
if e != nil {
return nil, nil, probe.NewError(e)
}
formValues[http.CanonicalHeaderKey(part.FormName())] = string(buffer)
} else {
if _, e := io.Copy(filePart, part); e != nil {
return nil, nil, probe.NewError(e)
}
}
}
}
return filePart, formValues, nil
}
// PostPolicyBucketHandler - POST policy
// ----------
// This implementation of the POST operation handles object creation with a specified
// signature policy in multipart/form-data
func (api objectStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
// Here the parameter is the size of the form data that should
// be loaded in memory, the remaining being put in temporary files.
reader, e := r.MultipartReader()
if e != nil {
errorIf(probe.NewError(e), "Unable to initialize multipart reader.", nil)
reader, err := r.MultipartReader()
if err != nil {
errorIf(err, "Unable to initialize multipart reader.")
writeErrorResponse(w, r, ErrMalformedPOSTRequest, r.URL.Path)
return
}
fileBody, formValues, err := extractHTTPFormValues(reader)
fileBody, fileName, formValues, err := extractPostPolicyFormValues(reader)
if err != nil {
errorIf(err.Trace(), "Unable to parse form values.", nil)
errorIf(err, "Unable to parse form values.")
writeErrorResponse(w, r, ErrMalformedPOSTRequest, r.URL.Path)
return
}
@@ -550,6 +321,12 @@ func (api objectStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, r *ht
formValues["Bucket"] = bucket
object := formValues["Key"]
if fileName != "" && strings.Contains(object, "${filename}") {
// S3 feature to replace ${filename} found in Key form field
// by the filename attribute passed in multipart
object = strings.Replace(object, "${filename}", fileName, -1)
}
// Verify policy signature.
apiErr := doesPolicySignatureMatch(formValues)
if apiErr != ErrNone {
@@ -560,29 +337,49 @@ func (api objectStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, r *ht
writeErrorResponse(w, r, apiErr, r.URL.Path)
return
}
objInfo, err := api.ObjectAPI.PutObject(bucket, object, -1, fileBody, nil)
// Save metadata.
metadata := make(map[string]string)
// Nothing to store right now.
md5Sum, err := api.ObjectAPI.PutObject(bucket, object, -1, fileBody, metadata)
if err != nil {
errorIf(err.Trace(), "PutObject failed.", nil)
switch err.ToGoError().(type) {
case RootPathFull:
writeErrorResponse(w, r, ErrRootPathFull, r.URL.Path)
case BucketNotFound:
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
case BucketNameInvalid:
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
case BadDigest:
writeErrorResponse(w, r, ErrBadDigest, r.URL.Path)
case IncompleteBody:
writeErrorResponse(w, r, ErrIncompleteBody, r.URL.Path)
default:
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
}
errorIf(err, "Unable to create object.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
if objInfo.MD5Sum != "" {
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
if md5Sum != "" {
w.Header().Set("ETag", "\""+md5Sum+"\"")
}
encodedSuccessResponse := encodeResponse(PostResponse{
Location: getObjectLocation(bucket, object), // TODO Full URL is preferred
Bucket: bucket,
Key: object,
ETag: md5Sum,
})
setCommonHeaders(w)
writeSuccessResponse(w, encodedSuccessResponse)
// Fetch object info for notifications.
objInfo, err := api.ObjectAPI.GetObjectInfo(bucket, object)
if err != nil {
errorIf(err, "Unable to fetch object info for \"%s\"", path.Join(bucket, object))
return
}
if eventN.IsBucketNotificationSet(bucket) {
// Notify object created event.
eventNotify(eventData{
Type: ObjectCreatedPost,
Bucket: bucket,
ObjInfo: objInfo,
ReqParams: map[string]string{
"sourceIPAddress": r.RemoteAddr,
},
})
}
writeSuccessResponse(w, nil)
}
// HeadBucketHandler - HEAD Bucket
@@ -591,7 +388,7 @@ func (api objectStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, r *ht
// The operation returns a 200 OK if the bucket exists and you
// have permission to access it. Otherwise, the operation might
// return responses such as 404 Not Found and 403 Forbidden.
func (api objectStorageAPI) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
@@ -602,7 +399,7 @@ func (api objectStorageAPI) HeadBucketHandler(w http.ResponseWriter, r *http.Req
return
case authTypeAnonymous:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
if s3Error := enforceBucketPolicy("s3:ListBucket", bucket, r.URL); s3Error != ErrNone {
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucket", r.URL); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
@@ -613,55 +410,37 @@ func (api objectStorageAPI) HeadBucketHandler(w http.ResponseWriter, r *http.Req
}
}
_, err := api.ObjectAPI.GetBucketInfo(bucket)
if err != nil {
errorIf(err.Trace(), "GetBucketInfo failed.", nil)
switch err.ToGoError().(type) {
case BucketNotFound:
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
case BucketNameInvalid:
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
default:
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
}
if _, err := api.ObjectAPI.GetBucketInfo(bucket); err != nil {
errorIf(err, "Unable to fetch bucket info.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
writeSuccessResponse(w, nil)
}
// DeleteBucketHandler - Delete bucket
func (api objectStorageAPI) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
// DeleteBucket does not support bucket policies, use checkAuth to validate signature.
if s3Error := checkAuth(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
switch getRequestAuthType(r) {
default:
// For all unknown auth types return error.
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
return
case authTypePresigned, authTypeSigned:
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
}
err := api.ObjectAPI.DeleteBucket(bucket)
if err != nil {
errorIf(err.Trace(), "DeleteBucket failed.", nil)
switch err.ToGoError().(type) {
case BucketNotFound:
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
case BucketNotEmpty:
writeErrorResponse(w, r, ErrBucketNotEmpty, r.URL.Path)
default:
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
}
// Attempt to delete bucket.
if err := api.ObjectAPI.DeleteBucket(bucket); err != nil {
errorIf(err, "Unable to delete a bucket.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
// Delete bucket access policy, if present - ignore any errors.
removeBucketPolicy(bucket)
removeBucketPolicy(bucket, api.ObjectAPI)
// Delete notification config, if present - ignore any errors.
removeNotificationConfig(bucket, api.ObjectAPI)
// Write success response.
writeSuccessNoContent(w)
+178
View File
@@ -0,0 +1,178 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"encoding/xml"
"errors"
)
// Represents the criteria for the filter rule.
type filterRule struct {
Name string `xml:"Name"`
Value string `xml:"Value"`
}
// Collection of filter rules per service config.
type keyFilter struct {
FilterRules []filterRule `xml:"FilterRule,omitempty"`
}
// Queue SQS configuration.
type queueConfig struct {
Events []string `xml:"Event"`
Filter struct {
Key keyFilter `xml:"S3Key,omitempty"`
}
ID string `xml:"Id"`
QueueARN string `xml:"Queue"`
}
// Topic SNS configuration, this is a compliance field not used by minio yet.
type topicConfig struct {
Events []string `xml:"Event"`
Filter struct {
Key keyFilter `xml:"S3Key"`
}
ID string `xml:"Id"`
TopicARN string `xml:"Topic"`
}
// Lambda function configuration, this is a compliance field not used by minio yet.
type lambdaConfig struct {
Events []string `xml:"Event"`
Filter struct {
Key keyFilter `xml:"S3Key,omitempty"`
}
ID string `xml:"Id"`
LambdaARN string `xml:"CloudFunction"`
}
// Notification configuration structure represents the XML format of
// notification configuration of buckets.
type notificationConfig struct {
XMLName xml.Name `xml:"NotificationConfiguration"`
QueueConfigs []queueConfig `xml:"QueueConfiguration"`
TopicConfigs []topicConfig `xml:"TopicConfiguration"`
LambdaConfigs []lambdaConfig `xml:"CloudFunctionConfiguration"`
}
// Internal error used to signal notifications not set.
var errNoSuchNotifications = errors.New("The specified bucket does not have bucket notifications")
// EventName is AWS S3 event type:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
type EventName int
const (
// ObjectCreatedPut is s3:ObjectCreated:Put
ObjectCreatedPut EventName = iota
// ObjectCreatedPost is s3:ObjectCreated:Post
ObjectCreatedPost
// ObjectCreatedCopy is s3:ObjectCreated:Copy
ObjectCreatedCopy
// ObjectCreatedCompleteMultipartUpload is s3:ObjectCreated:CompleteMultipartUpload
ObjectCreatedCompleteMultipartUpload
// ObjectRemovedDelete is s3:ObjectRemoved:Delete
ObjectRemovedDelete
)
// Stringer interface for event name.
func (eventName EventName) String() string {
switch eventName {
case ObjectCreatedPut:
return "s3:ObjectCreated:Put"
case ObjectCreatedPost:
return "s3:ObjectCreated:Post"
case ObjectCreatedCopy:
return "s3:ObjectCreated:Copy"
case ObjectCreatedCompleteMultipartUpload:
return "s3:ObjectCreated:CompleteMultipartUpload"
case ObjectRemovedDelete:
return "s3:ObjectRemoved:Delete"
default:
return "s3:Unknown"
}
}
// Indentity represents the user id, this is a compliance field.
type identity struct {
PrincipalID string `json:"principalId"`
}
func defaultIdentity() identity {
return identity{"minio"}
}
// Notification event bucket metadata.
type bucketMeta struct {
Name string `json:"name"`
OwnerIdentity identity `json:"ownerIdentity"`
ARN string `json:"arn"`
}
// Notification event object metadata.
type objectMeta struct {
Key string `json:"key"`
Size int64 `json:"size,omitempty"`
ETag string `json:"eTag,omitempty"`
VersionID string `json:"versionId,omitempty"`
Sequencer string `json:"sequencer"`
}
// Notification event server specific metadata.
type eventMeta struct {
SchemaVersion string `json:"s3SchemaVersion"`
ConfigurationID string `json:"configurationId"`
Bucket bucketMeta `json:"bucket"`
Object objectMeta `json:"object"`
}
// NotificationEvent represents an Amazon an S3 bucket notification event.
type NotificationEvent struct {
EventVersion string `json:"eventVersion"`
EventSource string `json:"eventSource"`
AwsRegion string `json:"awsRegion"`
EventTime string `json:"eventTime"`
EventName string `json:"eventName"`
UserIdentity identity `json:"userIdentity"`
RequestParameters map[string]string `json:"requestParameters"`
ResponseElements map[string]string `json:"responseElements"`
S3 eventMeta `json:"s3"`
}
// Represents the minio topic type and account id's.
type arnTopic struct {
Type string
AccountID string
}
// Stringer for constructing AWS ARN compatible string.
func (m arnTopic) String() string {
return minioTopic + serverConfig.GetRegion() + ":" + m.AccountID + ":" + m.Type
}
// Represents the minio sqs type and account id's.
type arnSQS struct {
Type string
AccountID string
}
// Stringer for constructing AWS ARN compatible string.
func (m arnSQS) String() string {
return minioSqs + serverConfig.GetRegion() + ":" + m.AccountID + ":" + m.Type
}
+269
View File
@@ -0,0 +1,269 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"encoding/json"
"encoding/xml"
"io"
"net/http"
"path"
"time"
"github.com/gorilla/mux"
)
const (
bucketConfigPrefix = "buckets"
bucketNotificationConfig = "notification.xml"
)
// GetBucketNotificationHandler - This implementation of the GET
// operation uses the notification subresource to return the
// notification configuration of a bucket. If notifications are
// not enabled on the bucket, the operation returns an empty
// NotificationConfiguration element.
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
// Validate request authorization.
if s3Error := checkAuth(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
// Attempt to successfully load notification config.
nConfig, err := loadNotificationConfig(bucket, api.ObjectAPI)
if err != nil && err != errNoSuchNotifications {
errorIf(err, "Unable to read notification configuration.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
// For no notifications we write a dummy XML.
if err == errNoSuchNotifications {
// Complies with the s3 behavior in this regard.
nConfig = &notificationConfig{}
}
notificationBytes, err := xml.Marshal(nConfig)
if err != nil {
// For any marshalling failure.
errorIf(err, "Unable to marshal notification configuration into XML.", err)
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
// Success.
writeSuccessResponse(w, notificationBytes)
}
// PutBucketNotificationHandler - Minio notification feature enables
// you to receive notifications when certain events happen in your bucket.
// Using this API, you can replace an existing notification configuration.
// The configuration is an XML file that defines the event types that you
// want Minio to publish and the destination where you want Minio to publish
// an event notification when it detects an event of the specified type.
// By default, your bucket has no event notifications configured. That is,
// the notification configuration will be an empty NotificationConfiguration.
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
// Validate request authorization.
if s3Error := checkAuth(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
_, err := api.ObjectAPI.GetBucketInfo(bucket)
if err != nil {
errorIf(err, "Unable to bucket info.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
// If Content-Length is unknown or zero, deny the request. PutBucketNotification
// always needs a Content-Length if incoming request is not chunked.
if !contains(r.TransferEncoding, "chunked") {
if r.ContentLength == -1 {
writeErrorResponse(w, r, ErrMissingContentLength, r.URL.Path)
return
}
}
// Reads the incoming notification configuration.
var buffer bytes.Buffer
var bufferSize int64
if r.ContentLength >= 0 {
bufferSize, err = io.CopyN(&buffer, r.Body, r.ContentLength)
} else {
bufferSize, err = io.Copy(&buffer, r.Body)
}
if err != nil {
errorIf(err, "Unable to read incoming body.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
var notificationCfg notificationConfig
// Unmarshal notification bytes.
notificationConfigBytes := buffer.Bytes()
if err = xml.Unmarshal(notificationConfigBytes, &notificationCfg); err != nil {
errorIf(err, "Unable to parse notification configuration XML.")
writeErrorResponse(w, r, ErrMalformedXML, r.URL.Path)
return
} // Successfully marshalled notification configuration.
// Validate unmarshalled bucket notification configuration.
if s3Error := validateNotificationConfig(notificationCfg); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
// Proceed to save notification configuration.
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
_, err = api.ObjectAPI.PutObject(minioMetaBucket, notificationConfigPath, bufferSize, bytes.NewReader(buffer.Bytes()), nil)
if err != nil {
errorIf(err, "Unable to write bucket notification configuration.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
// Set bucket notification config.
eventN.SetBucketNotificationConfig(bucket, &notificationCfg)
// Success.
writeSuccessResponse(w, nil)
}
// writeNotification marshals notification message before writing to client.
func writeNotification(w http.ResponseWriter, notification map[string][]NotificationEvent) error {
// Invalid response writer.
if w == nil {
return errInvalidArgument
}
// Invalid notification input.
if notification == nil {
return errInvalidArgument
}
// Marshal notification data into XML and write to client.
notificationBytes, err := json.Marshal(&notification)
if err != nil {
return err
}
// Add additional CRLF characters for client to
// differentiate the individual events properly.
_, err = w.Write(append(notificationBytes, crlf...))
// Make sure we have flushed, this would set Transfer-Encoding: chunked.
w.(http.Flusher).Flush()
if err != nil {
return err
}
return nil
}
// CRLF character used for chunked transfer in accordance with HTTP standards.
var crlf = []byte("\r\n")
// sendBucketNotification - writes notification back to client on the response writer
// for each notification input, otherwise writes whitespace characters periodically
// to keep the connection active. Each notification messages are terminated by CRLF
// character. Upon any error received on response writer the for loop exits.
//
// TODO - do not log for all errors.
func sendBucketNotification(w http.ResponseWriter, arnListenerCh <-chan []NotificationEvent) {
var dummyEvents = map[string][]NotificationEvent{"Records": nil}
// Continuously write to client either timely empty structures
// every 5 seconds, or return back the notifications.
for {
select {
case events := <-arnListenerCh:
if err := writeNotification(w, map[string][]NotificationEvent{"Records": events}); err != nil {
errorIf(err, "Unable to write notification to client.")
return
}
case <-time.After(5 * time.Second):
if err := writeNotification(w, dummyEvents); err != nil {
errorIf(err, "Unable to write notification to client.")
return
}
}
}
}
// ListenBucketNotificationHandler - list bucket notifications.
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
// Validate request authorization.
if s3Error := checkAuth(r); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
// Get notification ARN.
topicARN := r.URL.Query().Get("notificationARN")
if topicARN == "" {
writeErrorResponse(w, r, ErrARNNotification, r.URL.Path)
return
}
// Validate if bucket exists.
_, err := api.ObjectAPI.GetBucketInfo(bucket)
if err != nil {
errorIf(err, "Unable to bucket info.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
notificationCfg := eventN.GetBucketNotificationConfig(bucket)
if notificationCfg == nil {
writeErrorResponse(w, r, ErrARNNotification, r.URL.Path)
return
}
// Set SNS notifications only if special "listen" sns is set in bucket
// notification configs.
if !isMinioSNSConfigured(topicARN, notificationCfg.TopicConfigs) {
writeErrorResponse(w, r, ErrARNNotification, r.URL.Path)
return
}
// Add all common headers.
setCommonHeaders(w)
// Create a new notification event channel.
nEventCh := make(chan []NotificationEvent)
// Close the listener channel.
defer close(nEventCh)
// Set sns target.
eventN.SetSNSTarget(topicARN, nEventCh)
// Remove sns listener after the writer has closed or the client disconnected.
defer eventN.RemoveSNSTarget(topicARN, nEventCh)
// Start sending bucket notifications.
sendBucketNotification(w, nEventCh)
}
// Removes notification.xml for a given bucket, only used during DeleteBucket.
func removeNotificationConfig(bucket string, objAPI ObjectLayer) error {
// Verify bucket is valid.
if !IsValidBucketName(bucket) {
return BucketNameInvalid{Bucket: bucket}
}
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
return objAPI.DeleteObject(minioMetaBucket, notificationConfigPath)
}
+90
View File
@@ -0,0 +1,90 @@
package main
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"testing"
)
// Implement a dummy flush writer.
type flushWriter struct {
io.Writer
}
// Flush writer is a dummy writer compatible with http.Flusher and http.ResponseWriter.
func (f *flushWriter) Flush() {}
func (f *flushWriter) Write(b []byte) (n int, err error) { return f.Writer.Write(b) }
func (f *flushWriter) Header() http.Header { return http.Header{} }
func (f *flushWriter) WriteHeader(code int) {}
func newFlushWriter(writer io.Writer) *flushWriter {
return &flushWriter{writer}
}
// Tests write notification code.
func TestWriteNotification(t *testing.T) {
// Initialize a new test config.
root, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("Unable to initialize test config %s", err)
}
defer removeAll(root)
var buffer bytes.Buffer
// Collection of test cases for each event writer.
testCases := []struct {
writer *flushWriter
event map[string][]NotificationEvent
err error
}{
// Invalid input argument with writer `nil` - Test - 1
{
writer: nil,
event: nil,
err: errInvalidArgument,
},
// Invalid input argument with event `nil` - Test - 2
{
writer: newFlushWriter(ioutil.Discard),
event: nil,
err: errInvalidArgument,
},
// Unmarshal and write, validate last 5 bytes. - Test - 3
{
writer: newFlushWriter(&buffer),
event: map[string][]NotificationEvent{
"Records": {newNotificationEvent(eventData{
Type: ObjectCreatedPut,
Bucket: "testbucket",
ObjInfo: ObjectInfo{
Name: "key",
},
ReqParams: map[string]string{
"ip": "10.1.10.1",
}}),
},
},
err: nil,
},
}
// Validates all the testcases for writing notification.
for _, testCase := range testCases {
err := writeNotification(testCase.writer, testCase.event)
if err != testCase.err {
t.Errorf("Unable to write notification %s", err)
}
// Validates if the ending string has 'crlf'
if err == nil && !bytes.HasSuffix(buffer.Bytes(), crlf) {
buf := buffer.Bytes()[buffer.Len()-5 : 0]
t.Errorf("Invalid suffix found from the writer last 5 bytes %s, expected `\r\n`", string(buf))
}
// Not printing 'buf' on purpose, validates look for string '10.1.10.1'.
if err == nil && !bytes.Contains(buffer.Bytes(), []byte("10.1.10.1")) {
// Enable when debugging)
// fmt.Println(string(buffer.Bytes()))
t.Errorf("Requested content couldn't be found, expected `10.1.10.1`")
}
}
}
+313
View File
@@ -0,0 +1,313 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import "strings"
// List of valid event types.
var suppportedEventTypes = map[string]struct{}{
// Object created event types.
"s3:ObjectCreated:*": {},
"s3:ObjectCreated:Put": {},
"s3:ObjectCreated:Post": {},
"s3:ObjectCreated:Copy": {},
"s3:ObjectCreated:CompleteMultipartUpload": {},
// Object removed event types.
"s3:ObjectRemoved:*": {},
"s3:ObjectRemoved:Delete": {},
}
// checkEvent - checks if an event is supported.
func checkEvent(event string) APIErrorCode {
_, ok := suppportedEventTypes[event]
if !ok {
return ErrEventNotification
}
return ErrNone
}
// checkEvents - checks given list of events if all of them are valid.
// given if one of them is invalid, this function returns an error.
func checkEvents(events []string) APIErrorCode {
for _, event := range events {
if s3Error := checkEvent(event); s3Error != ErrNone {
return s3Error
}
}
return ErrNone
}
// Valid if filterName is 'prefix'.
func isValidFilterNamePrefix(filterName string) bool {
return "prefix" == filterName
}
// Valid if filterName is 'suffix'.
func isValidFilterNameSuffix(filterName string) bool {
return "suffix" == filterName
}
// Is this a valid filterName? - returns true if valid.
func isValidFilterName(filterName string) bool {
return isValidFilterNamePrefix(filterName) || isValidFilterNameSuffix(filterName)
}
// checkFilterRules - checks given list of filter rules if all of them are valid.
func checkFilterRules(filterRules []filterRule) APIErrorCode {
ruleSetMap := make(map[string]string)
// Validate all filter rules.
for _, filterRule := range filterRules {
// Unknown filter rule name found, returns an appropriate error.
if !isValidFilterName(filterRule.Name) {
return ErrFilterNameInvalid
}
// Filter names should not be set twice per notification service
// configuration, if found return an appropriate error.
if _, ok := ruleSetMap[filterRule.Name]; ok {
if isValidFilterNamePrefix(filterRule.Name) {
return ErrFilterNamePrefix
} else if isValidFilterNameSuffix(filterRule.Name) {
return ErrFilterNameSuffix
} else {
return ErrFilterNameInvalid
}
}
// Maximum prefix length can be up to 1,024 characters, validate.
if !IsValidObjectPrefix(filterRule.Value) {
return ErrFilterPrefixValueInvalid
}
// Set the new rule name to keep track of duplicates.
ruleSetMap[filterRule.Name] = filterRule.Value
}
// Success all prefixes validated.
return ErrNone
}
// checkQueueARN - check if the queue arn is valid.
func checkQueueARN(queueARN string) APIErrorCode {
if !strings.HasPrefix(queueARN, minioSqs) {
return ErrARNNotification
}
if !strings.HasPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":") {
return ErrRegionNotification
}
return ErrNone
}
// checkTopicARN - check if the topic arn is valid.
func checkTopicARN(topicARN string) APIErrorCode {
if !strings.HasPrefix(topicARN, minioTopic) {
return ErrARNNotification
}
if !strings.HasPrefix(topicARN, minioTopic+serverConfig.GetRegion()+":") {
return ErrRegionNotification
}
return ErrNone
}
// Returns true if the topicARN is for an Minio sns listen type.
func isMinioSNS(topicARN arnTopic) bool {
return strings.HasSuffix(topicARN.Type, snsTypeMinio)
}
// isMinioSNSConfigured - verifies if one topic ARN is valid and is enabled.
func isMinioSNSConfigured(topicARN string, topicConfigs []topicConfig) bool {
for _, topicConfig := range topicConfigs {
// Validate if topic ARN is already enabled.
if topicARN == topicConfig.TopicARN {
return true
}
}
return false
}
// Validate if we recognize the queue type.
func isValidQueue(sqsARN arnSQS) bool {
amqpQ := isAMQPQueue(sqsARN) // Is amqp queue?.
elasticQ := isElasticQueue(sqsARN) // Is elastic queue?.
redisQ := isRedisQueue(sqsARN) // Is redis queue?.
return amqpQ || elasticQ || redisQ
}
// Validate if we recognize the topic type.
func isValidTopic(topicARN arnTopic) bool {
return isMinioSNS(topicARN) // Is minio topic?.
}
// Validates account id for input queue ARN.
func isValidQueueID(queueARN string) bool {
// Unmarshals QueueARN into structured object.
sqsARN := unmarshalSqsARN(queueARN)
// AMQP queue.
if isAMQPQueue(sqsARN) {
amqpN := serverConfig.GetAMQPNotifyByID(sqsARN.AccountID)
return amqpN.Enable && amqpN.URL != ""
} else if isElasticQueue(sqsARN) { // Elastic queue.
elasticN := serverConfig.GetElasticSearchNotifyByID(sqsARN.AccountID)
return elasticN.Enable && elasticN.URL != ""
} else if isRedisQueue(sqsARN) { // Redis queue.
redisN := serverConfig.GetRedisNotifyByID(sqsARN.AccountID)
return redisN.Enable && redisN.Addr != ""
}
return false
}
// Check - validates queue configuration and returns error if any.
func checkQueueConfig(qConfig queueConfig) APIErrorCode {
// Check queue arn is valid.
if s3Error := checkQueueARN(qConfig.QueueARN); s3Error != ErrNone {
return s3Error
}
// Unmarshals QueueARN into structured object.
sqsARN := unmarshalSqsARN(qConfig.QueueARN)
// Validate if sqsARN requested any of the known supported queues.
if !isValidQueue(sqsARN) {
return ErrARNNotification
}
// Validate if the account ID is correct.
if !isValidQueueID(qConfig.QueueARN) {
return ErrARNNotification
}
// Check if valid events are set in queue config.
if s3Error := checkEvents(qConfig.Events); s3Error != ErrNone {
return s3Error
}
// Check if valid filters are set in queue config.
if s3Error := checkFilterRules(qConfig.Filter.Key.FilterRules); s3Error != ErrNone {
return s3Error
}
// Success.
return ErrNone
}
// Check - validates queue configuration and returns error if any.
func checkTopicConfig(tConfig topicConfig) APIErrorCode {
// Check queue arn is valid.
if s3Error := checkTopicARN(tConfig.TopicARN); s3Error != ErrNone {
return s3Error
}
// Unmarshals QueueARN into structured object.
topicARN := unmarshalTopicARN(tConfig.TopicARN)
// Validate if topicARN requested any of the known supported queues.
if !isValidTopic(topicARN) {
return ErrARNNotification
}
// Check if valid events are set in queue config.
if s3Error := checkEvents(tConfig.Events); s3Error != ErrNone {
return s3Error
}
// Check if valid filters are set in queue config.
if s3Error := checkFilterRules(tConfig.Filter.Key.FilterRules); s3Error != ErrNone {
return s3Error
}
// Success.
return ErrNone
}
// Validates all incoming queue configs, checkQueueConfig validates if the
// input fields for each queues is not malformed and has valid configuration
// information. If validation fails bucket notifications are not enabled.
func validateQueueConfigs(queueConfigs []queueConfig) APIErrorCode {
for _, qConfig := range queueConfigs {
if s3Error := checkQueueConfig(qConfig); s3Error != ErrNone {
return s3Error
}
}
// Success.
return ErrNone
}
// Validates all incoming topic configs, checkTopicConfig validates if the
// input fields for each queues is not malformed and has valid configuration
// information. If validation fails bucket notifications are not enabled.
func validateTopicConfigs(topicConfigs []topicConfig) APIErrorCode {
for _, tConfig := range topicConfigs {
if s3Error := checkTopicConfig(tConfig); s3Error != ErrNone {
return s3Error
}
}
// Success.
return ErrNone
}
// Validates all the bucket notification configuration for their validity,
// if one of the config is malformed or has invalid data it is rejected.
// Configuration is never applied partially.
func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
if s3Error := validateQueueConfigs(nConfig.QueueConfigs); s3Error != ErrNone {
return s3Error
}
if s3Error := validateTopicConfigs(nConfig.TopicConfigs); s3Error != ErrNone {
return s3Error
}
// Add validation for other configurations.
return ErrNone
}
// Unmarshals input value of AWS ARN format into minioTopic object.
// Returned value represents minio topic type, currently supported are
// - listen
func unmarshalTopicARN(topicARN string) arnTopic {
topic := arnTopic{}
if !strings.HasPrefix(topicARN, minioTopic+serverConfig.GetRegion()+":") {
return topic
}
topicType := strings.TrimPrefix(topicARN, minioTopic+serverConfig.GetRegion()+":")
switch {
case strings.HasSuffix(topicType, snsTypeMinio):
topic.Type = snsTypeMinio
} // Add more topic here.
topic.AccountID = strings.TrimSuffix(topicType, ":"+topic.Type)
return topic
}
// Unmarshals input value of AWS ARN format into minioSqs object.
// Returned value represents minio sqs types, currently supported are
// - amqp
// - elasticsearch
// - redis
func unmarshalSqsARN(queueARN string) (mSqs arnSQS) {
mSqs = arnSQS{}
if !strings.HasPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":") {
return mSqs
}
sqsType := strings.TrimPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":")
switch {
case strings.HasSuffix(sqsType, queueTypeAMQP):
mSqs.Type = queueTypeAMQP
case strings.HasSuffix(sqsType, queueTypeElastic):
mSqs.Type = queueTypeElastic
case strings.HasSuffix(sqsType, queueTypeRedis):
mSqs.Type = queueTypeRedis
} // Add more queues here.
mSqs.AccountID = strings.TrimSuffix(sqsType, ":"+mSqs.Type)
return mSqs
}
+292
View File
@@ -0,0 +1,292 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import "testing"
// Tests filter name validation.
func TestIsValidFilterName(t *testing.T) {
testCases := []struct {
filterName string
status bool
}{
// Validate if 'prefix' is correct.
{
filterName: "prefix",
status: true,
},
// Validate if 'suffix' is correct.
{
filterName: "suffix",
status: true,
},
// Invalid filter name empty string should return false.
{
filterName: "",
status: false,
},
// Invalid filter name random character should return false.
{
filterName: "unknown",
status: false,
},
}
for i, testCase := range testCases {
status := isValidFilterName(testCase.filterName)
if testCase.status != status {
t.Errorf("Test %d: Expected \"%t\", got \"%t\"", i+1, testCase.status, status)
}
}
}
// Tests list of valid and invalid events.
func TestValidEvents(t *testing.T) {
testCases := []struct {
events []string
errCode APIErrorCode
}{
// Return error for unknown event element.
{
events: []string{
"s3:UnknownAPI",
},
errCode: ErrEventNotification,
},
// Return success for supported event.
{
events: []string{
"s3:ObjectCreated:Put",
},
errCode: ErrNone,
},
// Return success for supported events.
{
events: []string{
"s3:ObjectCreated:*",
"s3:ObjectRemoved:*",
},
errCode: ErrNone,
},
// Return error for empty event list.
{
events: []string{""},
errCode: ErrEventNotification,
},
}
for i, testCase := range testCases {
errCode := checkEvents(testCase.events)
if testCase.errCode != errCode {
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
}
}
}
// Tests topic arn validation.
func TestTopicARN(t *testing.T) {
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("unable initialize config file, %s", err)
}
defer removeAll(rootPath)
testCases := []struct {
topicARN string
errCode APIErrorCode
}{
// Valid minio topic with '1' account id.
{
topicARN: "arn:minio:sns:us-east-1:1:minio",
errCode: ErrNone,
},
// Valid minio topic with '10' account id.
{
topicARN: "arn:minio:sns:us-east-1:10:minio",
errCode: ErrNone,
},
// Invalid empty queue arn.
{
topicARN: "",
errCode: ErrARNNotification,
},
// Invalid region 'us-west-1' in queue arn.
{
topicARN: "arn:minio:sns:us-west-1:1:redis",
errCode: ErrRegionNotification,
},
}
for i, testCase := range testCases {
errCode := checkTopicARN(testCase.topicARN)
if testCase.errCode != errCode {
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
}
}
}
// Tests queue arn validation.
func TestQueueARN(t *testing.T) {
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("unable initialize config file, %s", err)
}
defer removeAll(rootPath)
testCases := []struct {
queueARN string
errCode APIErrorCode
}{
// Valid redis queue arn.
{
queueARN: "arn:minio:sqs:us-east-1:1:redis",
errCode: ErrNone,
},
// Valid elasticsearch queue arn.
{
queueARN: "arn:minio:sqs:us-east-1:1:elasticsearch",
errCode: ErrNone,
},
// Valid amqp queue arn.
{
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
errCode: ErrNone,
},
// Invalid empty queue arn.
{
queueARN: "",
errCode: ErrARNNotification,
},
// Invalid region 'us-west-1' in queue arn.
{
queueARN: "arn:minio:sqs:us-west-1:1:redis",
errCode: ErrRegionNotification,
},
}
for i, testCase := range testCases {
errCode := checkQueueARN(testCase.queueARN)
if testCase.errCode != errCode {
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
}
}
}
// Test unmarshal topic arn.
func TestUnmarshalTopicARN(t *testing.T) {
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("unable initialize config file, %s", err)
}
defer removeAll(rootPath)
testCases := []struct {
topicARN string
Type string
}{
// Valid minio topic arn.
{
topicARN: "arn:minio:sns:us-east-1:1:listen",
Type: "listen",
},
// Invalid empty topic arn.
{
topicARN: "",
Type: "",
},
// Invalid region 'us-west-1' in topic arn.
{
topicARN: "arn:minio:sns:us-west-1:1:listen",
Type: "",
},
// Partial topic arn.
{
topicARN: "arn:minio:sns:",
Type: "",
},
// Invalid topic service value.
{
topicARN: "arn:minio:sns:us-east-1:1:*",
Type: "",
},
}
for i, testCase := range testCases {
topic := unmarshalTopicARN(testCase.topicARN)
if testCase.Type != topic.Type {
t.Errorf("Test %d: Expected \"%s\", got \"%s\"", i+1, testCase.Type, topic.Type)
}
}
}
// Test unmarshal queue arn.
func TestUnmarshalSqsARN(t *testing.T) {
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("unable initialize config file, %s", err)
}
defer removeAll(rootPath)
testCases := []struct {
queueARN string
Type string
}{
// Valid redis queue arn.
{
queueARN: "arn:minio:sqs:us-east-1:1:redis",
Type: "redis",
},
// Valid elasticsearch queue arn.
{
queueARN: "arn:minio:sqs:us-east-1:1:elasticsearch",
Type: "elasticsearch",
},
// Valid amqp queue arn.
{
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
Type: "amqp",
},
// Invalid empty queue arn.
{
queueARN: "",
Type: "",
},
// Invalid region 'us-west-1' in queue arn.
{
queueARN: "arn:minio:sqs:us-west-1:1:redis",
Type: "",
},
// Partial queue arn.
{
queueARN: "arn:minio:sqs:",
Type: "",
},
// Invalid queue service value.
{
queueARN: "arn:minio:sqs:us-east-1:1:*",
Type: "",
},
}
for i, testCase := range testCases {
mSqs := unmarshalSqsARN(testCase.queueARN)
if testCase.Type != mSqs.Type {
t.Errorf("Test %d: Expected \"%s\", got \"%s\"", i+1, testCase.Type, mSqs.Type)
}
}
}
+54 -38
View File
@@ -18,18 +18,17 @@ package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"regexp"
"strings"
mux "github.com/gorilla/mux"
"github.com/minio/minio/pkg/probe"
"github.com/minio/minio/pkg/wildcard"
)
// maximum supported access policy size.
const maxAccessPolicySize = 20 * 1024 * 1024 // 20KiB.
const maxAccessPolicySize = 20 * 1024 // 20KiB.
// Verify if a given action is valid for the url path based on the
// existing bucket access policy.
@@ -66,23 +65,31 @@ func bucketPolicyMatchStatement(action string, resource string, conditions map[s
// Verify if given action matches with policy statement.
func bucketPolicyActionMatch(action string, statement policyStatement) bool {
for _, policyAction := range statement.Actions {
// Policy action can be a regex, validate the action with matching string.
matched, e := regexp.MatchString(policyAction, action)
fatalIf(probe.NewError(e), "Invalid pattern, please verify the pattern string.", nil)
if matched {
if matched := actionMatch(policyAction, action); matched {
return true
}
}
return false
}
// Match function matches wild cards in 'pattern' for resource.
func resourceMatch(pattern, resource string) bool {
return wildcard.MatchExtended(pattern, resource)
}
// Match function matches wild cards in 'pattern' for action.
func actionMatch(pattern, action string) bool {
return wildcard.Match(pattern, action)
}
// Verify if given resource matches with policy statement.
func bucketPolicyResourceMatch(resource string, statement policyStatement) bool {
for _, presource := range statement.Resources {
matched, e := regexp.MatchString(presource, strings.TrimPrefix(resource, "/"))
fatalIf(probe.NewError(e), "Invalid pattern, please verify the pattern string.", nil)
// For any path matches, we return quickly and the let the caller continue.
if matched {
for _, resourcep := range statement.Resources {
// the resource rule for object could contain "*" wild card.
// the requested object can be given access based on the already set bucket policy if
// the match is successful.
// More info: http://docs.aws.amazon.com/AmazonS3/latest/dev/s3-arn-format.html .
if matched := resourceMatch(resourcep, resource); matched {
return true
}
}
@@ -127,10 +134,9 @@ func bucketPolicyConditionMatch(conditions map[string]string, statement policySt
// -----------------
// This implementation of the PUT operation uses the policy
// subresource to add to or replace a policy on a bucket
func (api objectStorageAPI) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
switch getRequestAuthType(r) {
default:
// For all unknown auth types return error.
@@ -161,32 +167,31 @@ func (api objectStorageAPI) PutBucketPolicyHandler(w http.ResponseWriter, r *htt
// Read access policy up to maxAccessPolicySize.
// http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
// bucket policies are limited to 20KB in size, using a limit reader.
bucketPolicyBuf, e := ioutil.ReadAll(io.LimitReader(r.Body, maxAccessPolicySize))
if e != nil {
errorIf(probe.NewError(e).Trace(bucket), "Reading policy failed.", nil)
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
policyBytes, err := ioutil.ReadAll(io.LimitReader(r.Body, maxAccessPolicySize))
if err != nil {
errorIf(err, "Unable to read from client.")
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
return
}
// Parse bucket policy.
bucketPolicy, e := parseBucketPolicy(bucketPolicyBuf)
if e != nil {
errorIf(probe.NewError(e), "Unable to parse bucket policy.", nil)
var policy = &bucketPolicy{}
err = parseBucketPolicy(bytes.NewReader(policyBytes), policy)
if err != nil {
errorIf(err, "Unable to parse bucket policy.")
writeErrorResponse(w, r, ErrInvalidPolicyDocument, r.URL.Path)
return
}
// Parse check bucket policy.
if s3Error := checkBucketPolicyResources(bucket, bucketPolicy); s3Error != ErrNone {
if s3Error := checkBucketPolicyResources(bucket, policy); s3Error != ErrNone {
writeErrorResponse(w, r, s3Error, r.URL.Path)
return
}
// Save bucket policy.
err := writeBucketPolicy(bucket, bucketPolicyBuf)
if err != nil {
errorIf(err.Trace(bucket, string(bucketPolicyBuf)), "SaveBucketPolicy failed.", nil)
switch err.ToGoError().(type) {
if err = writeBucketPolicy(bucket, api.ObjectAPI, bytes.NewReader(policyBytes), int64(len(policyBytes))); err != nil {
errorIf(err, "Unable to write bucket policy.")
switch err.(type) {
case BucketNameInvalid:
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
default:
@@ -194,6 +199,11 @@ func (api objectStorageAPI) PutBucketPolicyHandler(w http.ResponseWriter, r *htt
}
return
}
// Set the bucket policy in memory.
globalBucketPolicies.SetBucketPolicy(bucket, policy)
// Success.
writeSuccessNoContent(w)
}
@@ -201,7 +211,7 @@ func (api objectStorageAPI) PutBucketPolicyHandler(w http.ResponseWriter, r *htt
// -----------------
// This implementation of the DELETE operation uses the policy
// subresource to add to remove a policy on a bucket.
func (api objectStorageAPI) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
@@ -218,10 +228,9 @@ func (api objectStorageAPI) DeleteBucketPolicyHandler(w http.ResponseWriter, r *
}
// Delete bucket access policy.
err := removeBucketPolicy(bucket)
if err != nil {
errorIf(err.Trace(bucket), "DeleteBucketPolicy failed.", nil)
switch err.ToGoError().(type) {
if err := removeBucketPolicy(bucket, api.ObjectAPI); err != nil {
errorIf(err, "Unable to remove bucket policy.")
switch err.(type) {
case BucketNameInvalid:
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
case BucketPolicyNotFound:
@@ -231,6 +240,11 @@ func (api objectStorageAPI) DeleteBucketPolicyHandler(w http.ResponseWriter, r *
}
return
}
// Remove bucket policy.
globalBucketPolicies.RemoveBucketPolicy(bucket)
// Success.
writeSuccessNoContent(w)
}
@@ -238,7 +252,7 @@ func (api objectStorageAPI) DeleteBucketPolicyHandler(w http.ResponseWriter, r *
// -----------------
// This operation uses the policy
// subresource to return the policy of a specified bucket.
func (api objectStorageAPI) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucket := vars["bucket"]
@@ -255,10 +269,10 @@ func (api objectStorageAPI) GetBucketPolicyHandler(w http.ResponseWriter, r *htt
}
// Read bucket access policy.
p, err := readBucketPolicy(bucket)
policy, err := readBucketPolicy(bucket, api.ObjectAPI)
if err != nil {
errorIf(err.Trace(bucket), "GetBucketPolicy failed.", nil)
switch err.ToGoError().(type) {
errorIf(err, "Unable to read bucket policy.")
switch err.(type) {
case BucketNameInvalid:
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
case BucketPolicyNotFound:
@@ -268,5 +282,7 @@ func (api objectStorageAPI) GetBucketPolicyHandler(w http.ResponseWriter, r *htt
}
return
}
io.Copy(w, bytes.NewReader(p))
// Write to client.
fmt.Fprint(w, policy)
}
+547
View File
@@ -0,0 +1,547 @@
/*
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
// Tests validate Bucket policy resource matcher.
func TestBucketPolicyResourceMatch(t *testing.T) {
// generates statement with given resource..
generateStatement := func(resource string) policyStatement {
statement := policyStatement{}
statement.Resources = []string{resource}
return statement
}
// generates resource prefix.
generateResource := func(bucketName, objectName string) string {
return AWSResourcePrefix + bucketName + "/" + objectName
}
testCases := []struct {
resourceToMatch string
statement policyStatement
expectedResourceMatch bool
}{
// Test case 1-4.
// Policy with resource ending with bucket/* allows access to all objects inside the given bucket.
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
// Test case - 5.
// Policy with resource ending with bucket/oo* should not allow access to bucket/output.txt.
{generateResource("minio-bucket", "output.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), false},
// Test case - 6.
// Policy with resource ending with bucket/oo* should allow access to bucket/ootput.txt.
{generateResource("minio-bucket", "ootput.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), true},
// Test case - 7.
// Policy with resource ending with bucket/oo* allows access to all subfolders starting with "oo" inside given bucket.
{generateResource("minio-bucket", "oop-bucket/my-file"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), true},
// Test case - 8.
{generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/Asia/Japan/*")), false},
// Test case - 9.
{generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/Asia/Japan/*")), false},
// Test case - 10.
// Proves that the name space is flat.
{generateResource("minio-bucket", "Africa/Bihar/India/design_info.doc/Bihar"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix,
"minio-bucket"+"/*/India/*/Bihar")), true},
// Test case - 11.
// Proves that the name space is flat.
{generateResource("minio-bucket", "Asia/China/India/States/Bihar/output.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix,
"minio-bucket"+"/*/India/*/Bihar/*")), true},
}
for i, testCase := range testCases {
actualResourceMatch := bucketPolicyResourceMatch(testCase.resourceToMatch, testCase.statement)
if testCase.expectedResourceMatch != actualResourceMatch {
t.Errorf("Test %d: Expected Resource match to be `%v`, but instead found it to be `%v`", i+1, testCase.expectedResourceMatch, actualResourceMatch)
}
}
}
// TestBucketPolicyActionMatch - Test validates whether given action on the
// bucket/object matches the allowed actions in policyStatement.
// This test preserves the allowed actions for all 3 sets of policies, that is read-write,read-only, write-only.
// The intention of the test is to catch any changes made to allowed action for on eof the above 3 major policy groups mentioned.
func TestBucketPolicyActionMatch(t *testing.T) {
bucketName := getRandomBucketName()
objectPrefix := "test-object"
testCases := []struct {
action string
statement policyStatement
expectedResult bool
}{
// s3:GetBucketLocation is the action necessary to be present in the bucket policy to allow
// fetching of bucket location on an Anonymous/unsigned request.
//r ead-write bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 1).
{"s3:GetBucketLocation", getReadWriteBucketStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 2).
{"s3:GetBucketLocation", getWriteOnlyBucketStatement(bucketName, objectPrefix), true},
// read-only bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 3).
{"s3:GetBucketLocation", getReadOnlyBucketStatement(bucketName, objectPrefix), true},
// Any of the Object level access permissions shouldn't allow for GetBucketLocation operation on an Anonymous/unsigned request (Test cases 4-6).
{"s3:GetBucketLocation", getReadWriteObjectStatement(bucketName, objectPrefix), false},
{"s3:GetBucketLocation", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
{"s3:GetBucketLocation", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// s3:ListBucketMultipartUploads is the action necessary to be present in the bucket policy to allow
// Listing of multipart uploads in a given bucket for an Anonymous/unsigned request.
//read-write bucket policy is expected to allow ListBucketMultipartUploads operation on an anonymous request (Test case 7).
{"s3:ListBucketMultipartUploads", getReadWriteBucketStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to allow ListBucketMultipartUploads operation on an anonymous request (Test case 8).
{"s3:ListBucketMultipartUploads", getWriteOnlyBucketStatement(bucketName, objectPrefix), true},
// read-only bucket policy is expected to not allow ListBucketMultipartUploads operation on an anonymous request (Test case 9).
// the allowed actions in read-only bucket statement are "s3:GetBucketLocation","s3:ListBucket",
// this should not allow for ListBucketMultipartUploads operations.
{"s3:ListBucketMultipartUploads", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
// Any of the object level policy will not allow for s3:ListBucketMultipartUploads (Test cases 10-12).
{"s3:ListBucketMultipartUploads", getReadWriteObjectStatement(bucketName, objectPrefix), false},
{"s3:ListBucketMultipartUploads", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
{"s3:ListBucketMultipartUploads", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// s3:ListBucket is the action necessary to be present in the bucket policy to allow
// listing of all objects inside a given bucket on an Anonymous/unsigned request.
// Cases for testing ListBucket access for different Bucket level access permissions.
// read-only bucket policy is expected to allow ListBucket operation on an anonymous request (Test case 13).
{"s3:ListBucket", getReadOnlyBucketStatement(bucketName, objectPrefix), true},
// read-write bucket policy is expected to allow ListBucket operation on an anonymous request (Test case 14).
{"s3:ListBucket", getReadWriteBucketStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to not allow ListBucket operation on an anonymous request (Test case 15).
// the allowed actions in write-only bucket statement are "s3:GetBucketLocation", "s3:ListBucketMultipartUploads",
// this should not allow for ListBucket operations.
{"s3:ListBucket", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing ListBucket access for different Object level access permissions (Test cases 16-18).
// Any of the Object level access permissions shouldn't allow for ListBucket operation on an Anonymous/unsigned request.
{"s3:ListBucket", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
{"s3:ListBucket", getReadWriteObjectStatement(bucketName, objectPrefix), false},
{"s3:ListBucket", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
// s3:DeleteObject is the action necessary to be present in the bucket policy to allow
// deleting/removal of objects inside a given bucket for an Anonymous/unsigned request.
// Cases for testing DeleteObject access for different Bucket level access permissions (Test cases 19-21).
// Any of the Bucket level access permissions shouldn't allow for DeleteObject operation on an Anonymous/unsigned request.
{"s3:DeleteObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:DeleteObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:DeleteObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing DeleteObject access for different Object level access permissions (Test cases 22).
// read-only bucket policy is expected to not allow Delete Object operation on an anonymous request.
{"s3:DeleteObject", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// read-write bucket policy is expected to allow Delete Bucket operation on an anonymous request (Test cases 23).
{"s3:DeleteObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to allow Delete Object operation on an anonymous request (Test cases 24).
{"s3:DeleteObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
// s3:AbortMultipartUpload is the action necessary to be present in the bucket policy to allow
// cancelling or abortion of an already initiated multipart upload operation for an Anonymous/unsigned request.
// Cases for testing AbortMultipartUpload access for different Bucket level access permissions (Test cases 25-27).
// Any of the Bucket level access permissions shouldn't allow for AbortMultipartUpload operation on an Anonymous/unsigned request.
{"s3:AbortMultipartUpload", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:AbortMultipartUpload", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:AbortMultipartUpload", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing AbortMultipartUpload access for different Object level access permissions.
// read-only object policy is expected to not allow AbortMultipartUpload operation on an anonymous request (Test case 28).
{"s3:AbortMultipartUpload", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// read-write object policy is expected to allow AbortMultipartUpload operation on an anonymous request (Test case 29).
{"s3:AbortMultipartUpload", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only object policy is expected to allow AbortMultipartUpload operation on an anonymous request (Test case 30).
{"s3:AbortMultipartUpload", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
// s3:PutObject is the action necessary to be present in the bucket policy to allow
// uploading of an object for an Anonymous/unsigned request.
// Cases for testing PutObject access for different Bucket level access permissions (Test cases 31-33).
// Any of the Bucket level access permissions shouldn't allow for PutObject operation on an Anonymous/unsigned request.
{"s3:PutObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:PutObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:PutObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing PutObject access for different Object level access permissions.
// read-only object policy is expected to not allow PutObject operation on an anonymous request (Test case 34).
{"s3:PutObject", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// read-write object policy is expected to allow PutObject operation on an anonymous request (Test case 35).
{"s3:PutObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only object policy is expected to allow PutObject operation on an anonymous request (Test case 36).
{"s3:PutObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
// s3:GetObject is the action necessary to be present in the bucket policy to allow
// downloading of an object for an Anonymous/unsigned request.
// Cases for testing GetObject access for different Bucket level access permissions (Test cases 37-39).
// Any of the Bucket level access permissions shouldn't allow for GetObject operation on an Anonymous/unsigned request.
{"s3:GetObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:GetObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:GetObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// Cases for testing GetObject access for different Object level access permissions.
// read-only bucket policy is expected to allow downloading of an Object on an anonymous request (Test case 40).
{"s3:GetObject", getReadOnlyObjectStatement(bucketName, objectPrefix), true},
// read-write bucket policy is expected to allow downloading of an Object on an anonymous request (Test case 41).
{"s3:GetObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only bucket policy is expected to not allow downloading of an Object on an anonymous request (Test case 42).
{"s3:GetObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
// s3:ListMultipartUploadParts is the action necessary to be present in the bucket policy to allow
// Listing of uploaded parts for an Anonymous/unsigned request.
// Any of the Bucket level access permissions shouldn't allow for ListMultipartUploadParts operation on an Anonymous/unsigned request.
// read-only bucket policy is expected to not allow ListMultipartUploadParts operation on an anonymous request (Test cases 43-45).
{"s3:ListMultipartUploadParts", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
{"s3:ListMultipartUploadParts", getReadWriteBucketStatement(bucketName, objectPrefix), false},
{"s3:ListMultipartUploadParts", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
// read-only object policy is expected to not allow ListMultipartUploadParts operation on an anonymous request (Test case 46).
{"s3:ListMultipartUploadParts", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
// read-write object policy is expected to allow ListMultipartUploadParts operation on an anonymous request (Test case 47).
{"s3:ListMultipartUploadParts", getReadWriteObjectStatement(bucketName, objectPrefix), true},
// write-only object policy is expected to allow ListMultipartUploadParts operation on an anonymous request (Test case 48).
{"s3:ListMultipartUploadParts", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
}
for i, testCase := range testCases {
actualResult := bucketPolicyActionMatch(testCase.action, testCase.statement)
if testCase.expectedResult != actualResult {
t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.expectedResult, actualResult)
}
}
}
// Wrapper for calling Put Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
func TestPutBucketPolicyHandler(t *testing.T) {
ExecObjectLayerTest(t, testPutBucketPolicyHandler)
}
// testPutBucketPolicyHandler - Test for Bucket policy end point.
// TODO: Add exhaustive cases with various combination of statement fields.
func testPutBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
initBucketPolicies(obj)
// get random bucket name.
bucketName := getRandomBucketName()
// Create bucket.
err := obj.MakeBucket(bucketName)
if err != nil {
// failed to create newbucket, abort.
t.Fatalf("%s : %s", instanceType, err)
}
// Register the API end points with XL/FS object layer.
apiRouter := initTestAPIEndPoints(obj, []string{"PutBucketPolicy"})
// initialize the server and obtain the credentials and root.
// credentials are necessary to sign the HTTP request.
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("Init Test config failed")
}
// remove the root folder after the test ends.
defer removeAll(rootPath)
credentials := serverConfig.GetCredential()
// template for constructing HTTP request body for PUT bucket policy.
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::%s"]},{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/this*"]}]}`
// test cases with sample input and expected output.
testCases := []struct {
bucketName string
accessKey string
secretKey string
// expected Response.
expectedRespStatus int
}{
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
}
// Iterating over the test cases, calling the function under test and asserting the response.
for i, testCase := range testCases {
// obtain the put bucket policy request body.
bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testCase.bucketName, testCase.bucketName)
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest.NewRecorder()
// construct HTTP request for PUT bucket policy endpoint.
req, err := newTestSignedRequest("PUT", getPutPolicyURL("", testCase.bucketName),
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testCase.accessKey, testCase.secretKey)
if err != nil {
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler.
// Call the ServeHTTP to execute the handler.
apiRouter.ServeHTTP(rec, req)
if rec.Code != testCase.expectedRespStatus {
t.Errorf("Test %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
}
}
}
// Wrapper for calling Get Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
func TestGetBucketPolicyHandler(t *testing.T) {
ExecObjectLayerTest(t, testGetBucketPolicyHandler)
}
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
// TODO: Add exhaustive cases with various combination of statement fields.
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
initBucketPolicies(obj)
// get random bucket name.
bucketName := getRandomBucketName()
// Create bucket.
err := obj.MakeBucket(bucketName)
if err != nil {
// failed to create newbucket, abort.
t.Fatalf("%s : %s", instanceType, err)
}
// Register the API end points with XL/FS object layer.
// Registering only the PutBucketPolicy and GetBucketPolicy handlers.
apiRouter := initTestAPIEndPoints(obj, []string{"PutBucketPolicy", "GetBucketPolicy"})
// initialize the server and obtain the credentials and root.
// credentials are necessary to sign the HTTP request.
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("Init Test config failed")
}
// remove the root folder after the test ends.
defer removeAll(rootPath)
credentials := serverConfig.GetCredential()
// template for constructing HTTP request body for PUT bucket policy.
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::%s"]},{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/this*"]}]}`
// Writing bucket policy before running test on GetBucketPolicy.
putTestPolicies := []struct {
bucketName string
accessKey string
secretKey string
// expected Response.
expectedRespStatus int
}{
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
}
// Iterating over the cases and writing the bucket policy.
// its required to write the policies first before running tests on GetBucketPolicy.
for i, testPolicy := range putTestPolicies {
// obtain the put bucket policy request body.
bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testPolicy.bucketName, testPolicy.bucketName)
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest.NewRecorder()
// construct HTTP request for PUT bucket policy endpoint.
req, err := newTestSignedRequest("PUT", getPutPolicyURL("", testPolicy.bucketName),
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
if err != nil {
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler.
// Call the ServeHTTP to execute the handler.
apiRouter.ServeHTTP(rec, req)
if rec.Code != testPolicy.expectedRespStatus {
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, rec.Code)
}
}
// test cases with inputs and expected result for GetBucketPolicyHandler.
testCases := []struct {
bucketName string
accessKey string
secretKey string
// expected output.
expectedBucketPolicy string
expectedRespStatus int
}{
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, bucketPolicyTemplate, http.StatusOK},
}
// Iterating over the cases, fetching the policy and validating the response.
for i, testCase := range testCases {
// expected bucket policy json string.
expectedBucketPolicyStr := fmt.Sprintf(testCase.expectedBucketPolicy, testCase.bucketName, testCase.bucketName)
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest.NewRecorder()
// construct HTTP request for PUT bucket policy endpoint.
req, err := newTestSignedRequest("GET", getGetPolicyURL("", testCase.bucketName),
0, nil, testCase.accessKey, testCase.secretKey)
if err != nil {
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler, GetBucketPolicyHandler handles the request.
apiRouter.ServeHTTP(rec, req)
// Assert the response code with the expected status.
if rec.Code != testCase.expectedRespStatus {
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
}
// read the response body.
bucketPolicyReadBuf, err := ioutil.ReadAll(rec.Body)
if err != nil {
t.Fatalf("Test %d: %s: Failed parsing response body: <ERROR> %v", i+1, instanceType, err)
}
// Verify whether the bucket policy fetched is same as the one inserted.
if expectedBucketPolicyStr != string(bucketPolicyReadBuf) {
t.Errorf("Test %d: %s: Bucket policy differs from expected value.", i+1, instanceType)
}
}
}
// Wrapper for calling Delete Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
func TestDeleteBucketPolicyHandler(t *testing.T) {
ExecObjectLayerTest(t, testDeleteBucketPolicyHandler)
}
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
// TODO: Add exhaustive cases with various combination of statement fields.
func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
initBucketPolicies(obj)
// get random bucket name.
bucketName := getRandomBucketName()
// Create bucket.
err := obj.MakeBucket(bucketName)
if err != nil {
// failed to create newbucket, abort.
t.Fatalf("%s : %s", instanceType, err)
}
// Register the API end points with XL/FS object layer.
// Registering PutBucketPolicy and DeleteBucketPolicy handlers.
apiRouter := initTestAPIEndPoints(obj, []string{"PutBucketPolicy", "DeleteBucketPolicy"})
// initialize the server and obtain the credentials and root.
// credentials are necessary to sign the HTTP request.
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("Init Test config failed")
}
// remove the root folder after the test ends.
defer removeAll(rootPath)
credentials := serverConfig.GetCredential()
// template for constructing HTTP request body for PUT bucket policy.
bucketPolicyTemplate := `{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket"
],
"Effect": "Allow",
"Principal": {
"AWS": [
"*"
]
},
"Resource": [
"arn:aws:s3:::%s"
]
},
{
"Action": [
"s3:GetObject"
],
"Effect": "Allow",
"Principal": {
"AWS": [
"*"
]
},
"Resource": [
"arn:aws:s3:::%s/this*"
]
}
]
}`
// Writing bucket policy before running test on DeleteBucketPolicy.
putTestPolicies := []struct {
bucketName string
accessKey string
secretKey string
// expected Response.
expectedRespStatus int
}{
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
}
// Iterating over the cases and writing the bucket policy.
// its required to write the policies first before running tests on GetBucketPolicy.
for i, testPolicy := range putTestPolicies {
// obtain the put bucket policy request body.
bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testPolicy.bucketName, testPolicy.bucketName)
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest.NewRecorder()
// construct HTTP request for PUT bucket policy endpoint.
req, err := newTestSignedRequest("PUT", getPutPolicyURL("", testPolicy.bucketName),
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
if err != nil {
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler.
apiRouter.ServeHTTP(rec, req)
if rec.Code != testPolicy.expectedRespStatus {
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, rec.Code)
}
}
// testcases with input and expected output for DeleteBucketPolicyHandler.
testCases := []struct {
bucketName string
accessKey string
secretKey string
// expected Response.
expectedRespStatus int
}{
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
}
// Iterating over the cases and deleting the bucket policy and then asserting response.
for i, testCase := range testCases {
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest.NewRecorder()
// construct HTTP request for Delete bucket policy endpoint.
req, err := newTestSignedRequest("DELETE", getDeletePolicyURL("", testCase.bucketName),
0, nil, testCase.accessKey, testCase.secretKey)
if err != nil {
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
}
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
// Call the ServeHTTP to execute the handler, DeleteBucketPolicyHandler handles the request.
apiRouter.ServeHTTP(rec, req)
// Assert the response code with the expected status.
if rec.Code != testCase.expectedRespStatus {
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
}
}
}
+83
View File
@@ -0,0 +1,83 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
const policyJSON = "policy.json"
func getBucketFromPolicyPath(oldPolicyPath string) string {
bucketPrefix, _ := filepath.Split(oldPolicyPath)
_, bucketName := filepath.Split(strings.TrimSuffix(bucketPrefix, slashSeparator))
return bucketName
}
func cleanupOldBucketPolicyConfigs() error {
// Get old bucket policy config directory.
oldBucketsConfigDir, err := getOldBucketsConfigPath()
fatalIf(err, "Unable to fetch buckets config path to migrate bucket policy")
// Recursively remove configDir/buckets/ - old bucket policy config location.
// N B This is called only if all bucket policies were successfully migrated.
return os.RemoveAll(oldBucketsConfigDir)
}
func migrateBucketPolicyConfig(objAPI ObjectLayer) error {
// Get old bucket policy config directory.
oldBucketsConfigDir, err := getOldBucketsConfigPath()
fatalIf(err, "Unable to fetch buckets config path to migrate bucket policy")
// Check if config directory holding bucket policy exists before
// migration.
_, err = os.Stat(oldBucketsConfigDir)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
// WalkFunc that migrates access-policy.json to
// .minio.sys/buckets/bucketName/policy.json on all disks.
migrateBucketPolicy := func(policyPath string, fileInfo os.FileInfo, err error) error {
// policyFile - e.g /configDir/sample-bucket/access-policy.json
if err != nil {
return err
}
// Skip entries that aren't bucket policy files.
if fileInfo.Name() != "access-policy.json" {
return nil
}
// Get bucketName from old policy file path.
bucketName := getBucketFromPolicyPath(policyPath)
// Read bucket policy config from old location.
policyBytes, err := ioutil.ReadFile(policyPath)
fatalIf(err, "Unable to read bucket policy to migrate bucket policy", policyPath)
newPolicyPath := retainSlash(bucketConfigPrefix) + retainSlash(bucketName) + policyJSON
// Erasure code the policy config to all the disks.
_, err = objAPI.PutObject(minioMetaBucket, newPolicyPath, int64(len(policyBytes)), bytes.NewReader(policyBytes), nil)
fatalIf(err, "Unable to write bucket policy during migration.", newPolicyPath)
return nil
}
return filepath.Walk(oldBucketsConfigDir, migrateBucketPolicy)
}
+43 -20
View File
@@ -22,7 +22,8 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"io"
"path"
"sort"
"strings"
)
@@ -34,6 +35,8 @@ const (
// supportedActionMap - lists all the actions supported by minio.
var supportedActionMap = map[string]struct{}{
"*": {},
"s3:*": {},
"s3:GetObject": {},
"s3:ListBucket": {},
"s3:PutObject": {},
@@ -69,15 +72,25 @@ type policyStatement struct {
Principal policyUser `json:"Principal"`
Actions []string `json:"Action"`
Resources []string `json:"Resource"`
Conditions map[string]map[string]string `json:"Condition"`
Conditions map[string]map[string]string `json:"Condition,omitempty"`
}
// BucketPolicy - minio policy collection
type BucketPolicy struct {
// bucketPolicy - collection of various bucket policy statements.
type bucketPolicy struct {
Version string // date in 0000-00-00 format
Statements []policyStatement `json:"Statement"`
}
// Stringer implementation for the bucket policies.
func (b bucketPolicy) String() string {
bbytes, err := json.Marshal(&b)
if err != nil {
errorIf(err, "Unable to unmarshal bucket policy into JSON %#v", b)
return ""
}
return string(bbytes)
}
// supportedEffectMap - supported effects.
var supportedEffectMap = map[string]struct{}{
"Allow": {},
@@ -200,10 +213,18 @@ var invalidPrefixActions = map[string]struct{}{
// Add actions which do not honor prefixes.
}
// resourcePrefix - provides the prefix removing any wildcards.
func resourcePrefix(resource string) string {
if strings.HasSuffix(resource, "*") {
resource = strings.TrimSuffix(resource, "*")
}
return path.Clean(resource)
}
// checkBucketPolicyResources validates Resources in unmarshalled bucket policy structure.
// First valation of Resources done for given set of Actions.
// Later its validated for recursive Resources.
func checkBucketPolicyResources(bucket string, bucketPolicy BucketPolicy) APIErrorCode {
func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIErrorCode {
// Validate statements for special actions and collect resources
// for others to validate nesting.
var resourceMap = make(map[string]struct{})
@@ -232,7 +253,7 @@ func checkBucketPolicyResources(bucket string, bucketPolicy BucketPolicy) APIErr
var resources []string
for resource := range resourceMap {
resources = append(resources, resource)
resources = append(resources, resourcePrefix(resource))
}
// Sort strings as shorter first.
@@ -241,13 +262,13 @@ func checkBucketPolicyResources(bucket string, bucketPolicy BucketPolicy) APIErr
for len(resources) > 1 {
var resource string
resource, resources = resources[0], resources[1:]
resourceRegex := regexp.MustCompile(resource)
// Loop through all resources, if one of them matches with
// previous shorter one, it means we have detected
// nesting. Reject such rules.
for _, otherResource := range resources {
if resourceRegex.MatchString(otherResource) {
return ErrMalformedPolicy
// Common prefix reject such rules.
if strings.HasPrefix(otherResource, resource) {
return ErrPolicyNesting
}
}
}
@@ -258,44 +279,46 @@ func checkBucketPolicyResources(bucket string, bucketPolicy BucketPolicy) APIErr
// parseBucketPolicy - parses and validates if bucket policy is of
// proper JSON and follows allowed restrictions with policy standards.
func parseBucketPolicy(bucketPolicyBuf []byte) (policy BucketPolicy, err error) {
if err = json.Unmarshal(bucketPolicyBuf, &policy); err != nil {
return BucketPolicy{}, err
func parseBucketPolicy(bucketPolicyReader io.Reader, policy *bucketPolicy) (err error) {
// Parse bucket policy reader.
decoder := json.NewDecoder(bucketPolicyReader)
if err = decoder.Decode(&policy); err != nil {
return err
}
// Policy version cannot be empty.
if len(policy.Version) == 0 {
err = errors.New("Policy version cannot be empty.")
return BucketPolicy{}, err
return err
}
// Policy statements cannot be empty.
if len(policy.Statements) == 0 {
err = errors.New("Policy statement cannot be empty.")
return BucketPolicy{}, err
return err
}
// Loop through all policy statements and validate entries.
for _, statement := range policy.Statements {
// Statement effect should be valid.
if err := isValidEffect(statement.Effect); err != nil {
return BucketPolicy{}, err
return err
}
// Statement principal should be supported format.
if err := isValidPrincipals(statement.Principal.AWS); err != nil {
return BucketPolicy{}, err
return err
}
// Statement actions should be valid.
if err := isValidActions(statement.Actions); err != nil {
return BucketPolicy{}, err
return err
}
// Statement resources should be valid.
if err := isValidResources(statement.Resources); err != nil {
return BucketPolicy{}, err
return err
}
// Statement conditions should be valid.
if err := isValidConditions(statement.Conditions); err != nil {
return BucketPolicy{}, err
return err
}
}
@@ -316,5 +339,5 @@ func parseBucketPolicy(bucketPolicyBuf []byte) (policy BucketPolicy, err error)
policy.Statements = append(denyStatements, allowStatements...)
// Return successfully parsed policy structure.
return policy, nil
return nil
}
+139 -89
View File
@@ -16,10 +16,10 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
"testing"
)
@@ -69,60 +69,89 @@ var (
}
)
// Obtain statements for read-write BucketPolicy.
func setReadWriteStatement(bucketName, objectPrefix string) []policyStatement {
bucketResourceStatement := policyStatement{}
// Obtain bucket statement for read-write bucketPolicy.
func getReadWriteObjectStatement(bucketName, objectPrefix string) policyStatement {
objectResourceStatement := policyStatement{}
statements := []policyStatement{}
bucketResourceStatement.Effect = "Allow"
bucketResourceStatement.Principal.AWS = []string{"*"}
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
bucketResourceStatement.Actions = readWriteBucketActions
objectResourceStatement.Effect = "Allow"
objectResourceStatement.Principal.AWS = []string{"*"}
objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}
objectResourceStatement.Actions = readWriteObjectActions
return objectResourceStatement
}
// Obtain object statement for read-write bucketPolicy.
func getReadWriteBucketStatement(bucketName, objectPrefix string) policyStatement {
bucketResourceStatement := policyStatement{}
bucketResourceStatement.Effect = "Allow"
bucketResourceStatement.Principal.AWS = []string{"*"}
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
bucketResourceStatement.Actions = readWriteBucketActions
return bucketResourceStatement
}
// Obtain statements for read-write bucketPolicy.
func getReadWriteStatement(bucketName, objectPrefix string) []policyStatement {
statements := []policyStatement{}
// Save the read write policy.
statements = append(statements, bucketResourceStatement, objectResourceStatement)
statements = append(statements, getReadWriteBucketStatement(bucketName, objectPrefix), getReadWriteObjectStatement(bucketName, objectPrefix))
return statements
}
// Obtain statements for read only BucketPolicy.
func setReadOnlyStatement(bucketName, objectPrefix string) []policyStatement {
// Obtain bucket statement for read only bucketPolicy.
func getReadOnlyBucketStatement(bucketName, objectPrefix string) policyStatement {
bucketResourceStatement := policyStatement{}
objectResourceStatement := policyStatement{}
statements := []policyStatement{}
bucketResourceStatement.Effect = "Allow"
bucketResourceStatement.Principal.AWS = []string{"*"}
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
bucketResourceStatement.Actions = readOnlyBucketActions
return bucketResourceStatement
}
// Obtain object statement for read only bucketPolicy.
func getReadOnlyObjectStatement(bucketName, objectPrefix string) policyStatement {
objectResourceStatement := policyStatement{}
objectResourceStatement.Effect = "Allow"
objectResourceStatement.Principal.AWS = []string{"*"}
objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}
objectResourceStatement.Actions = readOnlyObjectActions
return objectResourceStatement
}
// Obtain statements for read only bucketPolicy.
func getReadOnlyStatement(bucketName, objectPrefix string) []policyStatement {
statements := []policyStatement{}
// Save the read only policy.
statements = append(statements, bucketResourceStatement, objectResourceStatement)
statements = append(statements, getReadOnlyBucketStatement(bucketName, objectPrefix), getReadOnlyObjectStatement(bucketName, objectPrefix))
return statements
}
// Obtain statements for write only BucketPolicy.
func setWriteOnlyStatement(bucketName, objectPrefix string) []policyStatement {
// Obtain bucket statements for write only bucketPolicy.
func getWriteOnlyBucketStatement(bucketName, objectPrefix string) policyStatement {
bucketResourceStatement := policyStatement{}
objectResourceStatement := policyStatement{}
statements := []policyStatement{}
// Write only policy.
bucketResourceStatement.Effect = "Allow"
bucketResourceStatement.Principal.AWS = []string{"*"}
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
bucketResourceStatement.Actions = writeOnlyBucketActions
return bucketResourceStatement
}
// Obtain object statements for write only bucketPolicy.
func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policyStatement {
objectResourceStatement := policyStatement{}
objectResourceStatement.Effect = "Allow"
objectResourceStatement.Principal.AWS = []string{"*"}
objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}
objectResourceStatement.Actions = writeOnlyObjectActions
return objectResourceStatement
}
// Obtain statements for write only bucketPolicy.
func getWriteOnlyStatement(bucketName, objectPrefix string) []policyStatement {
statements := []policyStatement{}
// Write only policy.
// Save the write only policy.
statements = append(statements, bucketResourceStatement, objectResourceStatement)
statements = append(statements, getWriteOnlyBucketStatement(bucketName, objectPrefix), getWriteOnlyBucketStatement(bucketName, objectPrefix))
return statements
}
@@ -145,11 +174,13 @@ func TestIsValidActions(t *testing.T) {
{[]string{}, errors.New("Action list cannot be empty."), false},
// Test case - 3.
// "s3:DeleteEverything"" is an invalid Action.
{[]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"}, errors.New("Unsupported action found: s3:DeleteEverything, please validate your policy document."), false},
{[]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"},
errors.New("Unsupported action found: s3:DeleteEverything, please validate your policy document."), false},
// Inputs with valid Action.
// Test Case - 4.
{[]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:GetBucketLocation", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts"}, nil, true},
{[]string{"s3:*", "*", "s3:GetObject", "s3:ListBucket",
"s3:PutObject", "s3:GetBucketLocation", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts"}, nil, true},
}
for i, testCase := range testCases {
err := isValidActions(testCase.actions)
@@ -440,48 +471,59 @@ func TestIsValidConditions(t *testing.T) {
}
// Tests validate Policy Action and Resource fields.
func TestCheckBucketPolicyResources(t *testing.T) {
func TestCheckbucketPolicyResources(t *testing.T) {
// constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go).
setValidPrefixActions := func(statements []policyStatement) []policyStatement {
statements[0].Actions = []string{"s3:DeleteObject", "s3:PutObject"}
return statements
}
// contructing policy statement with recursive resources.
// contracting policy statement with recursive resources.
// should result in ErrMalformedPolicy
setRecurseResource := func(statements []policyStatement) []policyStatement {
statements[0].Resources = []string{"arn:aws:s3:::minio-bucket/Asia/*", "arn:aws:s3:::minio-bucket/Asia/India/*"}
return statements
}
// List of BucketPolicy used for tests.
bucketAccessPolicies := []BucketPolicy{
// BucketPolicy - 0.
// constructing policy statement with lexically close characters.
// should not result in ErrMalformedPolicy
setResourceLexical := func(statements []policyStatement) []policyStatement {
statements[0].Resources = []string{"arn:aws:s3:::minio-bucket/op*", "arn:aws:s3:::minio-bucket/oo*"}
return statements
}
// List of bucketPolicy used for tests.
bucketAccessPolicies := []bucketPolicy{
// bucketPolicy - 1.
// Contains valid read only policy statement.
{Version: "1.0", Statements: setReadOnlyStatement("minio-bucket", "")},
// BucketPolicy - 1.
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
// bucketPolicy - 2.
// Contains valid read-write only policy statement.
{Version: "1.0", Statements: setReadWriteStatement("minio-bucket", "Asia/")},
// BucketPolicy - 2.
{Version: "1.0", Statements: getReadWriteStatement("minio-bucket", "Asia/")},
// bucketPolicy - 3.
// Contains valid write only policy statement.
{Version: "1.0", Statements: setWriteOnlyStatement("minio-bucket", "Asia/India/")},
// BucketPolicy - 3.
{Version: "1.0", Statements: getWriteOnlyStatement("minio-bucket", "Asia/India/")},
// bucketPolicy - 4.
// Contains invalidPrefixActions.
// Since resourcePrefix is not to the bucket-name, it return ErrMalformedPolicy.
{Version: "1.0", Statements: setReadOnlyStatement("minio-bucket-fail", "Asia/India/")},
// BucketPolicy - 4.
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket-fail", "Asia/India/")},
// bucketPolicy - 5.
// constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go).
// but bucket part of the resource is not equal to the bucket name.
// this results in return of ErrMalformedPolicy.
{Version: "1.0", Statements: setValidPrefixActions(setWriteOnlyStatement("minio-bucket-fail", "Asia/India/"))},
// BucketPolicy - 5.
// constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go).
// contructing policy statement with recursive resources.
{Version: "1.0", Statements: setValidPrefixActions(getWriteOnlyStatement("minio-bucket-fail", "Asia/India/"))},
// bucketPolicy - 6.
// contracting policy statement with recursive resources.
// should result in ErrMalformedPolicy
{Version: "1.0", Statements: setRecurseResource(setValidPrefixActions(setWriteOnlyStatement("minio-bucket", "")))},
{Version: "1.0", Statements: setRecurseResource(setValidPrefixActions(getWriteOnlyStatement("minio-bucket", "")))},
// BucketPolciy - 7.
// constructing policy statement with non recursive but
// lexically close resources.
// should result in ErrNone.
{Version: "1.0", Statements: setResourceLexical(setValidPrefixActions(getWriteOnlyStatement("minio-bucket", "oo")))},
}
testCases := []struct {
inputPolicy BucketPolicy
inputPolicy bucketPolicy
// expected results.
apiErrCode APIErrorCode
// Flag indicating whether the test should pass.
@@ -502,12 +544,17 @@ func TestCheckBucketPolicyResources(t *testing.T) {
// Resource prefix bucket part is not equal to the bucket name in this case.
{bucketAccessPolicies[4], ErrMalformedPolicy, false},
// Test case - 6.
// contructing policy statement with recursive resources.
// should result in ErrMalformedPolicy.
{bucketAccessPolicies[5], ErrMalformedPolicy, false},
// contracting policy statement with recursive resources.
// should result in ErrPolicyNesting.
{bucketAccessPolicies[5], ErrPolicyNesting, false},
// Test case - 7.
// constructing policy statement with lexically close
// characters.
// should result in ErrNone.
{bucketAccessPolicies[6], ErrNone, true},
}
for i, testCase := range testCases {
apiErrCode := checkBucketPolicyResources("minio-bucket", testCase.inputPolicy)
apiErrCode := checkBucketPolicyResources("minio-bucket", &testCase.inputPolicy)
if apiErrCode != ErrNone && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with Errocode %v", i+1, apiErrCode)
}
@@ -549,53 +596,53 @@ func TestParseBucketPolicy(t *testing.T) {
statements[0].Resources = []string{"my-resource"}
return statements
}
// List of BucketPolicy used for test cases.
bucketAccesPolicies := []BucketPolicy{
// BucketPolicy - 0.
// BucketPolicy statement empty.
// List of bucketPolicy used for test cases.
bucketAccesPolicies := []bucketPolicy{
// bucketPolicy - 0.
// bucketPolicy statement empty.
{Version: "1.0"},
// BucketPolicy - 1.
// BucketPolicy version empty.
// bucketPolicy - 1.
// bucketPolicy version empty.
{Version: "", Statements: []policyStatement{}},
// BucketPolicy - 2.
// Readonly BucketPolicy.
{Version: "1.0", Statements: setReadOnlyStatement("minio-bucket", "")},
// BucketPolicy - 3.
// bucketPolicy - 2.
// Readonly bucketPolicy.
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
// bucketPolicy - 3.
// Read-Write bucket policy.
{Version: "1.0", Statements: setReadWriteStatement("minio-bucket", "Asia/")},
// BucketPolicy - 4.
{Version: "1.0", Statements: getReadWriteStatement("minio-bucket", "Asia/")},
// bucketPolicy - 4.
// Write only bucket policy.
{Version: "1.0", Statements: setWriteOnlyStatement("minio-bucket", "Asia/India/")},
// BucketPolicy - 5.
// BucketPolicy statement contains unsupported action.
{Version: "1.0", Statements: setUnsupportedActions(setReadOnlyStatement("minio-bucket", ""))},
// BucketPolicy - 6.
// BucketPolicy statement contains unsupported Effect.
{Version: "1.0", Statements: setUnsupportedEffect(setReadWriteStatement("minio-bucket", "Asia/"))},
// BucketPolicy - 7.
// BucketPolicy statement contains unsupported Principal.
{Version: "1.0", Statements: setUnsupportedPrincipals(setWriteOnlyStatement("minio-bucket", "Asia/India/"))},
// BucketPolicy - 8.
// BucketPolicy statement contains unsupported Resource.
{Version: "1.0", Statements: setUnsupportedResources(setWriteOnlyStatement("minio-bucket", "Asia/India/"))},
{Version: "1.0", Statements: getWriteOnlyStatement("minio-bucket", "Asia/India/")},
// bucketPolicy - 5.
// bucketPolicy statement contains unsupported action.
{Version: "1.0", Statements: setUnsupportedActions(getReadOnlyStatement("minio-bucket", ""))},
// bucketPolicy - 6.
// bucketPolicy statement contains unsupported Effect.
{Version: "1.0", Statements: setUnsupportedEffect(getReadWriteStatement("minio-bucket", "Asia/"))},
// bucketPolicy - 7.
// bucketPolicy statement contains unsupported Principal.
{Version: "1.0", Statements: setUnsupportedPrincipals(getWriteOnlyStatement("minio-bucket", "Asia/India/"))},
// bucketPolicy - 8.
// bucketPolicy statement contains unsupported Resource.
{Version: "1.0", Statements: setUnsupportedResources(getWriteOnlyStatement("minio-bucket", "Asia/India/"))},
}
testCases := []struct {
inputPolicy BucketPolicy
inputPolicy bucketPolicy
// expected results.
expectedPolicy BucketPolicy
expectedPolicy bucketPolicy
err error
// Flag indicating whether the test should pass.
shouldPass bool
}{
// Test case - 1.
// BucketPolicy statement empty.
{bucketAccesPolicies[0], BucketPolicy{}, errors.New("Policy statement cannot be empty."), false},
// bucketPolicy statement empty.
{bucketAccesPolicies[0], bucketPolicy{}, errors.New("Policy statement cannot be empty."), false},
// Test case - 2.
// BucketPolicy version empty.
{bucketAccesPolicies[1], BucketPolicy{}, errors.New("Policy version cannot be empty."), false},
// bucketPolicy version empty.
{bucketAccesPolicies[1], bucketPolicy{}, errors.New("Policy version cannot be empty."), false},
// Test case - 3.
// Readonly BucketPolicy.
// Readonly bucketPolicy.
{bucketAccesPolicies[2], bucketAccesPolicies[2], nil, true},
// Test case - 4.
// Read-Write bucket policy.
@@ -604,25 +651,28 @@ func TestParseBucketPolicy(t *testing.T) {
// Write only bucket policy.
{bucketAccesPolicies[4], bucketAccesPolicies[4], nil, true},
// Test case - 6.
// BucketPolicy statement contains unsupported action.
// bucketPolicy statement contains unsupported action.
{bucketAccesPolicies[5], bucketAccesPolicies[5], fmt.Errorf("Unsupported action found: s3:DeleteEverything, please validate your policy document."), false},
// Test case - 7.
// BucketPolicy statement contains unsupported Effect.
// bucketPolicy statement contains unsupported Effect.
{bucketAccesPolicies[6], bucketAccesPolicies[6], fmt.Errorf("Unsupported Effect found: DontAllow, please validate your policy document."), false},
// Test case - 8.
// BucketPolicy statement contains unsupported Principal.
// bucketPolicy statement contains unsupported Principal.
{bucketAccesPolicies[7], bucketAccesPolicies[7], fmt.Errorf("Unsupported principal style found: User1111, please validate your policy document."), false},
// Test case - 9.
// BucketPolicy statement contains unsupported Resource.
// bucketPolicy statement contains unsupported Resource.
{bucketAccesPolicies[8], bucketAccesPolicies[8], fmt.Errorf("Unsupported resource style found: my-resource, please validate your policy document."), false},
}
for i, testCase := range testCases {
inputPolicyBytes, e := json.Marshal(testCase.inputPolicy)
if e != nil {
t.Fatalf("Test %d: Couldn't Marshal bucket policy", i+1)
var buffer bytes.Buffer
encoder := json.NewEncoder(&buffer)
err := encoder.Encode(testCase.inputPolicy)
if err != nil {
t.Fatalf("Test %d: Couldn't Marshal bucket policy %s", i+1, err)
}
actualAccessPolicy, err := parseBucketPolicy(inputPolicyBytes)
var actualAccessPolicy = &bucketPolicy{}
err = parseBucketPolicy(&buffer, actualAccessPolicy)
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
}
@@ -637,7 +687,7 @@ func TestParseBucketPolicy(t *testing.T) {
}
// Test passes as expected, but the output values are verified for correctness here.
if err == nil && testCase.shouldPass {
if !reflect.DeepEqual(testCase.expectedPolicy, actualAccessPolicy) {
if testCase.expectedPolicy.String() != actualAccessPolicy.String() {
t.Errorf("Test %d: The expected statements from resource statement generator doesn't match the actual statements", i+1)
}
}
+130 -101
View File
@@ -17,135 +17,164 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/minio/minio/pkg/probe"
"bytes"
"errors"
"io"
"path"
"sync"
)
// getBucketsConfigPath - get buckets path.
func getBucketsConfigPath() (string, *probe.Error) {
// Variable represents bucket policies in memory.
var globalBucketPolicies *bucketPolicies
// Global bucket policies list, policies are enforced on each bucket looking
// through the policies here.
type bucketPolicies struct {
rwMutex *sync.RWMutex
// Collection of 'bucket' policies.
bucketPolicyConfigs map[string]*bucketPolicy
}
// Fetch bucket policy for a given bucket.
func (bp bucketPolicies) GetBucketPolicy(bucket string) *bucketPolicy {
bp.rwMutex.RLock()
defer bp.rwMutex.RUnlock()
return bp.bucketPolicyConfigs[bucket]
}
// Set a new bucket policy for a bucket, this operation will overwrite
// any previous bucketpolicies for the bucket.
func (bp *bucketPolicies) SetBucketPolicy(bucket string, policy *bucketPolicy) error {
bp.rwMutex.Lock()
defer bp.rwMutex.Unlock()
if policy == nil {
return errors.New("invalid argument")
}
bp.bucketPolicyConfigs[bucket] = policy
return nil
}
// Remove bucket policy for a bucket, from in-memory map.
func (bp *bucketPolicies) RemoveBucketPolicy(bucket string) {
bp.rwMutex.Lock()
defer bp.rwMutex.Unlock()
delete(bp.bucketPolicyConfigs, bucket)
}
// Loads all bucket policies from persistent layer.
func loadAllBucketPolicies(objAPI ObjectLayer) (policies map[string]*bucketPolicy, err error) {
// List buckets to proceed loading all notification configuration.
buckets, err := objAPI.ListBuckets()
if err != nil {
return nil, err
}
policies = make(map[string]*bucketPolicy)
// Loads bucket policy.
for _, bucket := range buckets {
var policy *bucketPolicy
policy, err = readBucketPolicy(bucket.Name, objAPI)
if err != nil {
switch err.(type) {
case BucketPolicyNotFound:
continue
}
return nil, err
}
policies[bucket.Name] = policy
}
// Success.
return policies, nil
}
// Intialize all bucket policies.
func initBucketPolicies(objAPI ObjectLayer) error {
if objAPI == nil {
return errInvalidArgument
}
// Read all bucket policies.
policies, err := loadAllBucketPolicies(objAPI)
if err != nil {
return err
}
globalBucketPolicies = &bucketPolicies{
rwMutex: &sync.RWMutex{},
bucketPolicyConfigs: policies,
}
return nil
}
// getOldBucketsConfigPath - get old buckets config path. (Only used for migrating old bucket policies)
func getOldBucketsConfigPath() (string, error) {
configPath, err := getConfigPath()
if err != nil {
return "", err.Trace()
return "", err
}
return filepath.Join(configPath, "buckets"), nil
return path.Join(configPath, "buckets"), nil
}
// createBucketsConfigPath - create buckets directory.
func createBucketsConfigPath() *probe.Error {
bucketsConfigPath, err := getBucketsConfigPath()
if err != nil {
return err
}
if e := os.MkdirAll(bucketsConfigPath, 0700); e != nil {
return probe.NewError(e)
}
return nil
}
// getBucketConfigPath - get bucket config path.
func getBucketConfigPath(bucket string) (string, *probe.Error) {
bucketsConfigPath, err := getBucketsConfigPath()
if err != nil {
return "", err.Trace()
}
return filepath.Join(bucketsConfigPath, bucket), nil
}
// createBucketConfigPath - create bucket config directory.
func createBucketConfigPath(bucket string) *probe.Error {
bucketConfigPath, err := getBucketConfigPath(bucket)
if err != nil {
return err
}
if e := os.MkdirAll(bucketConfigPath, 0700); e != nil {
return probe.NewError(e)
}
return nil
}
// readBucketPolicy - read bucket policy.
func readBucketPolicy(bucket string) ([]byte, *probe.Error) {
// readBucketPolicy - reads bucket policy for an input bucket, returns BucketPolicyNotFound
// if bucket policy is not found. This function also parses the bucket policy into an object.
func readBucketPolicy(bucket string, objAPI ObjectLayer) (*bucketPolicy, error) {
// Verify bucket is valid.
if !IsValidBucketName(bucket) {
return nil, probe.NewError(BucketNameInvalid{Bucket: bucket})
return nil, BucketNameInvalid{Bucket: bucket}
}
bucketConfigPath, err := getBucketConfigPath(bucket)
policyPath := pathJoin(bucketConfigPrefix, bucket, policyJSON)
objInfo, err := objAPI.GetObjectInfo(minioMetaBucket, policyPath)
if err != nil {
return nil, err.Trace()
}
// Get policy file.
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json")
if _, e := os.Stat(bucketPolicyFile); e != nil {
if os.IsNotExist(e) {
return nil, probe.NewError(BucketPolicyNotFound{Bucket: bucket})
if _, ok := err.(ObjectNotFound); ok {
return nil, BucketPolicyNotFound{Bucket: bucket}
}
return nil, probe.NewError(e)
return nil, err
}
var buffer bytes.Buffer
err = objAPI.GetObject(minioMetaBucket, policyPath, 0, objInfo.Size, &buffer)
if err != nil {
if _, ok := err.(ObjectNotFound); ok {
return nil, BucketPolicyNotFound{Bucket: bucket}
}
return nil, err
}
// Parse the saved policy.
var policy = &bucketPolicy{}
err = parseBucketPolicy(&buffer, policy)
if err != nil {
return nil, err
accessPolicyBytes, e := ioutil.ReadFile(bucketPolicyFile)
if e != nil {
return nil, probe.NewError(e)
}
return accessPolicyBytes, nil
return policy, nil
}
// removeBucketPolicy - remove bucket policy.
func removeBucketPolicy(bucket string) *probe.Error {
// removeBucketPolicy - removes any previously written bucket policy. Returns BucketPolicyNotFound
// if no policies are found.
func removeBucketPolicy(bucket string, objAPI ObjectLayer) error {
// Verify bucket is valid.
if !IsValidBucketName(bucket) {
return probe.NewError(BucketNameInvalid{Bucket: bucket})
return BucketNameInvalid{Bucket: bucket}
}
bucketConfigPath, err := getBucketConfigPath(bucket)
if err != nil {
return err.Trace(bucket)
}
// Get policy file.
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json")
if _, e := os.Stat(bucketPolicyFile); e != nil {
if os.IsNotExist(e) {
return probe.NewError(BucketPolicyNotFound{Bucket: bucket})
policyPath := pathJoin(bucketConfigPrefix, bucket, policyJSON)
if err := objAPI.DeleteObject(minioMetaBucket, policyPath); err != nil {
if _, ok := err.(ObjectNotFound); ok {
return BucketPolicyNotFound{Bucket: bucket}
}
return probe.NewError(e)
return err
}
return nil
}
// writeBucketPolicy - save bucket policy.
func writeBucketPolicy(bucket string, accessPolicyBytes []byte) *probe.Error {
// writeBucketPolicy - save all bucket policies.
func writeBucketPolicy(bucket string, objAPI ObjectLayer, reader io.Reader, size int64) error {
// Verify if bucket path legal
if !IsValidBucketName(bucket) {
return probe.NewError(BucketNameInvalid{Bucket: bucket})
return BucketNameInvalid{Bucket: bucket}
}
// Create bucket config path.
if err := createBucketConfigPath(bucket); err != nil {
return err.Trace()
}
bucketConfigPath, err := getBucketConfigPath(bucket)
if err != nil {
return err.Trace()
}
// Get policy file.
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json")
if _, e := os.Stat(bucketPolicyFile); e != nil {
if !os.IsNotExist(e) {
return probe.NewError(e)
}
}
// Write bucket policy.
if e := ioutil.WriteFile(bucketPolicyFile, accessPolicyBytes, 0600); e != nil {
return probe.NewError(e)
}
return nil
policyPath := pathJoin(bucketConfigPrefix, bucket, policyJSON)
_, err := objAPI.PutObject(minioMetaBucket, policyPath, size, reader, nil)
return err
}
+6 -1
View File
@@ -16,13 +16,18 @@
package main
// DO NOT EDIT THIS FILE DIRECTLY. These are build-time constants
// set through buildscripts/gen-ldflags.go.
var (
// minioGOPATH - GOPATH value at the time of build.
minioGOPATH = ""
// minioVersion - version time.RFC3339.
minioVersion = "DEVELOPMENT.GOGET"
// minioReleaseTag - release tag in TAG.%Y-%m-%dT%H-%M-%SZ.
minioReleaseTag = "DEVELOPMENT.GOGET"
// minioCommitID - latest commit id.
minioCommitID = "DEVELOPMENT.GOGET"
// minioShortCommitID - first 12 characters from mcCommitID
// minioShortCommitID - first 12 characters from minioCommitID.
minioShortCommitID = minioCommitID[:12]
)
+1 -1
View File
@@ -212,7 +212,7 @@ main() {
echo "Please install them "
echo "${MISSING}"
echo
echo "Follow https://github.com/minio/minio/blob/master/INSTALLGO.md for further instructions"
echo "Follow https://docs.minio.io/docs/how-to-install-golang for further instructions"
exit 1
fi
echo "Done"
+5 -4
View File
@@ -28,10 +28,11 @@ import (
func genLDFlags(version string) string {
var ldflagsStr string
ldflagsStr = "-X main.minioVersion=" + version + " "
ldflagsStr = ldflagsStr + "-X main.minioReleaseTag=" + releaseTag(version) + " "
ldflagsStr = ldflagsStr + "-X main.minioCommitID=" + commitID() + " "
ldflagsStr = ldflagsStr + "-X main.minioShortCommitID=" + commitID()[:12]
ldflagsStr = "-X main.minioVersion=" + version
ldflagsStr += " -X main.minioReleaseTag=" + releaseTag(version)
ldflagsStr += " -X main.minioCommitID=" + commitID()
ldflagsStr += " -X main.minioShortCommitID=" + commitID()[:12]
ldflagsStr += " -X main.minioGOPATH=" + os.Getenv("GOPATH")
return ldflagsStr
}
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
echo "" > coverage.txt
for d in $(go list ./... | grep -v vendor); do
go test -coverprofile=profile.out -covermode=atomic $d
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done
+10 -15
View File
@@ -19,37 +19,32 @@ package main
import (
"os"
"path/filepath"
"github.com/minio/go-homedir"
"github.com/minio/minio/pkg/probe"
)
// createCertsPath create certs path.
func createCertsPath() *probe.Error {
func createCertsPath() error {
certsPath, err := getCertsPath()
if err != nil {
return err.Trace()
return err
}
if err := os.MkdirAll(certsPath, 0700); err != nil {
return probe.NewError(err)
}
return nil
return os.MkdirAll(certsPath, 0700)
}
// getCertsPath get certs path.
func getCertsPath() (string, *probe.Error) {
homeDir, e := homedir.Dir()
if e != nil {
return "", probe.NewError(e)
func getCertsPath() (string, error) {
var certsPath string
configDir, err := getConfigPath()
if err != nil {
return "", err
}
certsPath := filepath.Join(homeDir, globalMinioCertsDir)
certsPath = filepath.Join(configDir, globalMinioCertsDir)
return certsPath, nil
}
// mustGetCertsPath must get certs path.
func mustGetCertsPath() string {
certsPath, err := getCertsPath()
fatalIf(err.Trace(), "Unable to retrieve certs path.", nil)
fatalIf(err, "Failed to get certificate path.")
return certsPath
}
+60
View File
@@ -0,0 +1,60 @@
/*
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// Make sure we have a valid certs path.
func TestGetCertsPath(t *testing.T) {
path, err := getCertsPath()
if err != nil {
t.Error(err)
}
if path == "" {
t.Errorf("expected path to not be an empty string, got: '%s'", path)
}
// Ensure it contains some sort of path separator.
if !strings.ContainsRune(path, os.PathSeparator) {
t.Errorf("expected path to contain file separator")
}
// It should also be an absolute path.
if !filepath.IsAbs(path) {
t.Errorf("expected path to be an absolute path")
}
// This will error if something goes wrong, so just call it.
mustGetCertsPath()
}
// Ensure that the certificate and key file getters contain their respective
// file name and endings.
func TestGetFiles(t *testing.T) {
file := mustGetCertFile()
if !strings.Contains(file, globalMinioCertFile) {
t.Errorf("CertFile does not contain %s", globalMinioCertFile)
}
file = mustGetKeyFile()
if !strings.Contains(file, globalMinioKeyFile) {
t.Errorf("KeyFile does not contain %s", globalMinioKeyFile)
}
}
+48
View File
@@ -0,0 +1,48 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"fmt"
"net"
)
// Make sure that none of the other processes are listening on the
// specified port on any of the interfaces.
//
// On linux if a process is listening on 127.0.0.1:9000 then Listen()
// on ":9000" fails with the error "port already in use".
// However on Mac OSX Listen() on ":9000" falls back to the IPv6 address.
// This causes confusion on Mac OSX that minio server is not reachable
// on 127.0.0.1 even though minio server is running. So before we start
// the minio server we make sure that the port is free on each tcp network.
func checkPortAvailability(port int) error {
network := [3]string{"tcp", "tcp4", "tcp6"}
for _, n := range network {
l, err := net.Listen(n, fmt.Sprintf(":%d", port))
if err != nil {
return err
}
// look for error so we don't have dangling connection
if err = l.Close(); err != nil {
return err
}
}
return nil
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"fmt"
"net"
"runtime"
"testing"
)
// Tests for port availability logic written for server startup sequence.
func TestCheckPortAvailability(t *testing.T) {
tests := []struct {
port int
}{
{getFreePort()},
{getFreePort()},
}
for _, test := range tests {
// This test should pass if the ports are available
err := checkPortAvailability(test.port)
if err != nil {
t.Fatalf("checkPortAvailability test failed for port: %d. Error: %v", test.port, err)
}
// Now use the ports and check again
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", test.port))
if err != nil {
t.Fail()
}
defer ln.Close()
err = checkPortAvailability(test.port)
// Skip if the os is windows due to https://github.com/golang/go/issues/7598
if err == nil && runtime.GOOS != "windows" {
t.Fatalf("checkPortAvailability should fail for port: %d. Error: %v", test.port, err)
}
}
}
+4 -8
View File
@@ -26,19 +26,15 @@ var commandsTree = newTrie()
// Collection of minio flags currently supported.
var globalFlags = []cli.Flag{
cli.BoolFlag{
Name: "quiet, q",
Usage: "Suppress chatty console output.",
},
cli.BoolFlag{
Name: "debug",
Usage: "Enable debugging output.",
},
cli.StringFlag{
Name: "config-dir, C",
Value: mustGetConfigPath(),
Usage: "Path to configuration folder.",
},
cli.BoolFlag{
Name: "quiet",
Usage: "Suppress chatty output.",
},
}
// registerCommand registers a cli command.
+139 -28
View File
@@ -22,7 +22,6 @@ import (
"path/filepath"
"github.com/minio/mc/pkg/console"
"github.com/minio/minio/pkg/probe"
"github.com/minio/minio/pkg/quick"
)
@@ -33,40 +32,40 @@ func migrateConfig() {
migrateV2ToV3()
// Migrate version '3' to '4'.
migrateV3ToV4()
// Migrate version '4' to '5'.
migrateV4ToV5()
// Migrate version '5' to '6.
migrateV5ToV6()
}
// Version '1' is not supported anymore and deprecated, safe to delete.
func purgeV1() {
cv1, err := loadConfigV1()
if err != nil {
if os.IsNotExist(err.ToGoError()) {
return
}
if err != nil && os.IsNotExist(err) {
return
}
fatalIf(err.Trace(), "Unable to load config version 1.", nil)
fatalIf(err, "Unable to load config version 1.")
if cv1.Version == "1" {
console.Println("Unsupported config version 1 found, removed successfully.")
console.Println("Removed unsupported config version 1.")
/// Purge old fsUsers.json file
configPath, err := getConfigPath()
fatalIf(err.Trace(), "Unable to retrieve config path.", nil)
fatalIf(err, "Unable to retrieve config path.")
configFile := filepath.Join(configPath, "fsUsers.json")
os.RemoveAll(configFile)
removeAll(configFile)
}
fatalIf(probe.NewError(errors.New("")), "Unexpected version found "+cv1.Version+", cannot migrate.", nil)
fatalIf(errors.New(""), "Failed to migrate unrecognized config version "+cv1.Version+".")
}
// Version '2' to '3' config migration adds new fields and re-orders
// previous fields. Simplifies config for future additions.
func migrateV2ToV3() {
cv2, err := loadConfigV2()
if err != nil {
if os.IsNotExist(err.ToGoError()) {
return
}
if err != nil && os.IsNotExist(err) {
return
}
fatalIf(err.Trace(), "Unable to load config version 2.", nil)
fatalIf(err, "Unable to load config version 2.")
if cv2.Version != "2" {
return
}
@@ -78,6 +77,10 @@ func migrateV2ToV3() {
SecretAccessKey: cv2.Credentials.SecretAccessKey,
}
srvConfig.Region = cv2.Credentials.Region
if srvConfig.Region == "" {
// Region needs to be set for AWS Signature V4.
srvConfig.Region = "us-east-1"
}
srvConfig.Logger.Console = consoleLogger{
Enable: true,
Level: "fatal",
@@ -99,14 +102,14 @@ func migrateV2ToV3() {
srvConfig.Logger.Syslog = slogger
qc, err := quick.New(srvConfig)
fatalIf(err.Trace(), "Unable to initialize config.", nil)
fatalIf(err, "Unable to initialize config.")
configFile, err := getConfigFile()
fatalIf(err.Trace(), "Unable to get config file.", nil)
fatalIf(err, "Unable to get config file.")
// Migrate the config.
err = qc.Save(configFile)
fatalIf(err.Trace(), "Migrating from version "+cv2.Version+" to "+srvConfig.Version+" failed.", nil)
fatalIf(err, "Failed to migrate config from "+cv2.Version+" to "+srvConfig.Version+" failed.")
console.Println("Migration from version " + cv2.Version + " to " + srvConfig.Version + " completed successfully.")
}
@@ -116,32 +119,140 @@ func migrateV2ToV3() {
// the config for future additions.
func migrateV3ToV4() {
cv3, err := loadConfigV3()
if err != nil {
if os.IsNotExist(err.ToGoError()) {
return
}
if err != nil && os.IsNotExist(err) {
return
}
fatalIf(err.Trace(), "Unable to load config version 3.", nil)
fatalIf(err, "Unable to load config version 3.")
if cv3.Version != "3" {
return
}
// Save only the new fields, ignore the rest.
srvConfig := &serverConfigV4{}
srvConfig.Version = globalMinioConfigVersion
srvConfig := &configV4{}
srvConfig.Version = "4"
srvConfig.Credential = cv3.Credential
srvConfig.Region = cv3.Region
if srvConfig.Region == "" {
// Region needs to be set for AWS Signature Version 4.
srvConfig.Region = "us-east-1"
}
srvConfig.Logger.Console = cv3.Logger.Console
srvConfig.Logger.File = cv3.Logger.File
srvConfig.Logger.Syslog = cv3.Logger.Syslog
qc, err := quick.New(srvConfig)
fatalIf(err.Trace(), "Unable to initialize the quick config.", nil)
fatalIf(err, "Unable to initialize the quick config.")
configFile, err := getConfigFile()
fatalIf(err.Trace(), "Unable to get config file.", nil)
fatalIf(err, "Unable to get config file.")
err = qc.Save(configFile)
fatalIf(err.Trace(), "Migrating from version "+cv3.Version+" to "+srvConfig.Version+" failed.", nil)
fatalIf(err, "Failed to migrate config from "+cv3.Version+" to "+srvConfig.Version+" failed.")
console.Println("Migration from version " + cv3.Version + " to " + srvConfig.Version + " completed successfully.")
}
// Version '5' to '6' migrates config, removes previous fields related
// to backend types and server address. This change further simplifies
// the config for future additions.
func migrateV5ToV6() {
cv5, err := loadConfigV5()
if err != nil && os.IsNotExist(err) {
return
}
fatalIf(err, "Unable to load config version 5.")
if cv5.Version != "5" {
return
}
// Save only the new fields, ignore the rest.
srvConfig := &serverConfigV6{}
srvConfig.Version = globalMinioConfigVersion
srvConfig.Credential = cv5.Credential
srvConfig.Region = cv5.Region
if srvConfig.Region == "" {
// Region needs to be set for AWS Signature Version 4.
srvConfig.Region = "us-east-1"
}
srvConfig.Logger.Console = cv5.Logger.Console
srvConfig.Logger.File = cv5.Logger.File
srvConfig.Logger.Syslog = cv5.Logger.Syslog
srvConfig.Notify.AMQP = map[string]amqpNotify{
"1": {
Enable: cv5.Logger.AMQP.Enable,
URL: cv5.Logger.AMQP.URL,
Exchange: cv5.Logger.AMQP.Exchange,
RoutingKey: cv5.Logger.AMQP.RoutingKey,
Mandatory: cv5.Logger.AMQP.Mandatory,
Immediate: cv5.Logger.AMQP.Immediate,
Durable: cv5.Logger.AMQP.Durable,
Internal: cv5.Logger.AMQP.Internal,
NoWait: cv5.Logger.AMQP.NoWait,
AutoDeleted: cv5.Logger.AMQP.AutoDeleted,
},
}
srvConfig.Notify.ElasticSearch = map[string]elasticSearchNotify{
"1": {
Enable: cv5.Logger.ElasticSearch.Enable,
URL: cv5.Logger.ElasticSearch.URL,
Index: cv5.Logger.ElasticSearch.Index,
},
}
srvConfig.Notify.Redis = map[string]redisNotify{
"1": {
Enable: cv5.Logger.Redis.Enable,
Addr: cv5.Logger.Redis.Addr,
Password: cv5.Logger.Redis.Password,
Key: cv5.Logger.Redis.Key,
},
}
qc, err := quick.New(srvConfig)
fatalIf(err, "Unable to initialize the quick config.")
configFile, err := getConfigFile()
fatalIf(err, "Unable to get config file.")
err = qc.Save(configFile)
fatalIf(err, "Failed to migrate config from "+cv5.Version+" to "+srvConfig.Version+" failed.")
console.Println("Migration from version " + cv5.Version + " to " + srvConfig.Version + " completed successfully.")
}
// Version '4' to '5' migrates config, removes previous fields related
// to backend types and server address. This change further simplifies
// the config for future additions.
func migrateV4ToV5() {
cv4, err := loadConfigV4()
if err != nil && os.IsNotExist(err) {
return
}
fatalIf(err, "Unable to load config version 4.")
if cv4.Version != "4" {
return
}
// Save only the new fields, ignore the rest.
srvConfig := &configV5{}
srvConfig.Version = globalMinioConfigVersion
srvConfig.Credential = cv4.Credential
srvConfig.Region = cv4.Region
if srvConfig.Region == "" {
// Region needs to be set for AWS Signature Version 4.
srvConfig.Region = "us-east-1"
}
srvConfig.Logger.Console = cv4.Logger.Console
srvConfig.Logger.File = cv4.Logger.File
srvConfig.Logger.Syslog = cv4.Logger.Syslog
srvConfig.Logger.AMQP.Enable = false
srvConfig.Logger.ElasticSearch.Enable = false
srvConfig.Logger.Redis.Enable = false
qc, err := quick.New(srvConfig)
fatalIf(err, "Unable to initialize the quick config.")
configFile, err := getConfigFile()
fatalIf(err, "Unable to get config file.")
err = qc.Save(configFile)
fatalIf(err, "Failed to migrate config from "+cv4.Version+" to "+srvConfig.Version+" failed.")
console.Println("Migration from version " + cv4.Version + " to " + srvConfig.Version + " completed successfully.")
}
+159 -31
View File
@@ -4,7 +4,6 @@ import (
"os"
"path/filepath"
"github.com/minio/minio/pkg/probe"
"github.com/minio/minio/pkg/quick"
)
@@ -16,25 +15,25 @@ type configV1 struct {
}
// loadConfigV1 load config
func loadConfigV1() (*configV1, *probe.Error) {
func loadConfigV1() (*configV1, error) {
configPath, err := getConfigPath()
if err != nil {
return nil, err.Trace()
return nil, err
}
configFile := filepath.Join(configPath, "fsUsers.json")
if _, err := os.Stat(configFile); err != nil {
return nil, probe.NewError(err)
if _, err = os.Stat(configFile); err != nil {
return nil, err
}
a := &configV1{}
a.Version = "1"
qc, err := quick.New(a)
c := &configV1{}
c.Version = "1"
qc, err := quick.New(c)
if err != nil {
return nil, err.Trace()
return nil, err
}
if err := qc.Load(configFile); err != nil {
return nil, err.Trace()
return nil, err
}
return qc.Data().(*configV1), nil
return c, nil
}
/////////////////// Config V2 ///////////////////
@@ -60,24 +59,24 @@ type configV2 struct {
}
// loadConfigV2 load config version '2'.
func loadConfigV2() (*configV2, *probe.Error) {
func loadConfigV2() (*configV2, error) {
configFile, err := getConfigFile()
if err != nil {
return nil, err.Trace()
return nil, err
}
if _, err := os.Stat(configFile); err != nil {
return nil, probe.NewError(err)
if _, err = os.Stat(configFile); err != nil {
return nil, err
}
a := &configV2{}
a.Version = "2"
qc, err := quick.New(a)
c := &configV2{}
c.Version = "2"
qc, err := quick.New(c)
if err != nil {
return nil, err.Trace()
return nil, err
}
if err := qc.Load(configFile); err != nil {
return nil, err.Trace()
return nil, err
}
return qc.Data().(*configV2), nil
return c, nil
}
/////////////////// Config V3 ///////////////////
@@ -127,22 +126,151 @@ type configV3 struct {
}
// loadConfigV3 load config version '3'.
func loadConfigV3() (*configV3, *probe.Error) {
func loadConfigV3() (*configV3, error) {
configFile, err := getConfigFile()
if err != nil {
return nil, err.Trace()
return nil, err
}
if _, err := os.Stat(configFile); err != nil {
return nil, probe.NewError(err)
if _, err = os.Stat(configFile); err != nil {
return nil, err
}
a := &configV3{}
a.Version = "3"
qc, err := quick.New(a)
c := &configV3{}
c.Version = "3"
qc, err := quick.New(c)
if err != nil {
return nil, err.Trace()
return nil, err
}
if err := qc.Load(configFile); err != nil {
return nil, err.Trace()
return nil, err
}
return qc.Data().(*configV3), nil
return c, nil
}
// logger type representing version '4' logger config.
type loggerV4 struct {
Console struct {
Enable bool `json:"enable"`
Level string `json:"level"`
} `json:"console"`
File struct {
Enable bool `json:"enable"`
Filename string `json:"fileName"`
Level string `json:"level"`
} `json:"file"`
Syslog struct {
Enable bool `json:"enable"`
Addr string `json:"address"`
Level string `json:"level"`
} `json:"syslog"`
}
// configV4 server configuration version '4'.
type configV4 struct {
Version string `json:"version"`
// S3 API configuration.
Credential credential `json:"credential"`
Region string `json:"region"`
// Additional error logging configuration.
Logger loggerV4 `json:"logger"`
}
// loadConfigV4 load config version '4'.
func loadConfigV4() (*configV4, error) {
configFile, err := getConfigFile()
if err != nil {
return nil, err
}
if _, err = os.Stat(configFile); err != nil {
return nil, err
}
c := &configV4{}
c.Version = "4"
qc, err := quick.New(c)
if err != nil {
return nil, err
}
if err := qc.Load(configFile); err != nil {
return nil, err
}
return c, nil
}
// logger type representing version '5' logger config.
type loggerV5 struct {
Console struct {
Enable bool `json:"enable"`
Level string `json:"level"`
} `json:"console"`
File struct {
Enable bool `json:"enable"`
Filename string `json:"fileName"`
Level string `json:"level"`
} `json:"file"`
Syslog struct {
Enable bool `json:"enable"`
Addr string `json:"address"`
Level string `json:"level"`
} `json:"syslog"`
AMQP struct {
Enable bool `json:"enable"`
Level string `json:"level"`
URL string `json:"url"`
Exchange string `json:"exchange"`
RoutingKey string `json:"routingKey"`
ExchangeType string `json:"exchangeType"`
Mandatory bool `json:"mandatory"`
Immediate bool `json:"immediate"`
Durable bool `json:"durable"`
Internal bool `json:"internal"`
NoWait bool `json:"noWait"`
AutoDeleted bool `json:"autoDeleted"`
} `json:"amqp"`
ElasticSearch struct {
Enable bool `json:"enable"`
Level string `json:"level"`
URL string `json:"url"`
Index string `json:"index"`
} `json:"elasticsearch"`
Redis struct {
Enable bool `json:"enable"`
Level string `json:"level"`
Addr string `json:"address"`
Password string `json:"password"`
Key string `json:"key"`
} `json:"redis"`
}
// configV5 server configuration version '5'.
type configV5 struct {
Version string `json:"version"`
// S3 API configuration.
Credential credential `json:"credential"`
Region string `json:"region"`
// Additional error logging configuration.
Logger loggerV5 `json:"logger"`
}
// loadConfigV5 load config version '5'.
func loadConfigV5() (*configV5, error) {
configFile, err := getConfigFile()
if err != nil {
return nil, err
}
if _, err = os.Stat(configFile); err != nil {
return nil, err
}
c := &configV5{}
c.Version = "5"
qc, err := quick.New(c)
if err != nil {
return nil, err
}
if err := qc.Load(configFile); err != nil {
return nil, err
}
return c, nil
}
+93 -45
View File
@@ -20,12 +20,11 @@ import (
"os"
"sync"
"github.com/minio/minio/pkg/probe"
"github.com/minio/minio/pkg/quick"
)
// serverConfigV4 server configuration version '4'.
type serverConfigV4 struct {
// serverConfigV6 server configuration version '5'.
type serverConfigV6 struct {
Version string `json:"version"`
// S3 API configuration.
@@ -35,74 +34,71 @@ type serverConfigV4 struct {
// Additional error logging configuration.
Logger logger `json:"logger"`
// Notification queue configuration.
Notify notifier `json:"notify"`
// Read Write mutex.
rwMutex *sync.RWMutex
}
// initConfig - initialize server config. config version (called only once).
func initConfig() *probe.Error {
func initConfig() error {
if !isConfigFileExists() {
srvCfg := &serverConfigV4{}
// Initialize server config.
srvCfg := &serverConfigV6{}
srvCfg.Version = globalMinioConfigVersion
srvCfg.Region = "us-east-1"
srvCfg.Credential = mustGenAccessKeys()
// Enable console logger by default on a fresh run.
srvCfg.Logger.Console = consoleLogger{
Enable: true,
Level: "fatal",
}
srvCfg.rwMutex = &sync.RWMutex{}
// Create config path.
err := createConfigPath()
if err != nil {
return err.Trace()
}
// Create certs path.
err = createCertsPath()
if err != nil {
return err.Trace()
return err
}
// Save the new config globally.
serverConfig = srvCfg
// Save config into file.
err = serverConfig.Save()
if err != nil {
return err.Trace()
}
return nil
return serverConfig.Save()
}
configFile, err := getConfigFile()
if err != nil {
return err.Trace()
return err
}
if _, e := os.Stat(configFile); err != nil {
return probe.NewError(e)
if _, err = os.Stat(configFile); err != nil {
return err
}
srvCfg := &serverConfigV4{}
srvCfg := &serverConfigV6{}
srvCfg.Version = globalMinioConfigVersion
srvCfg.rwMutex = &sync.RWMutex{}
qc, err := quick.New(srvCfg)
if err != nil {
return err.Trace()
return err
}
if err := qc.Load(configFile); err != nil {
return err.Trace()
return err
}
// Save the loaded config globally.
serverConfig = qc.Data().(*serverConfigV4)
serverConfig = srvCfg
// Set the version properly after the unmarshalled json is loaded.
serverConfig.Version = globalMinioConfigVersion
return nil
}
// serverConfig server config.
var serverConfig *serverConfigV4
var serverConfig *serverConfigV6
// GetVersion get current config version.
func (s serverConfigV4) GetVersion() string {
func (s serverConfigV6) GetVersion() string {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Version
@@ -110,98 +106,150 @@ func (s serverConfigV4) GetVersion() string {
/// Logger related.
func (s *serverConfigV6) SetAMQPNotifyByID(accountID string, amqpn amqpNotify) {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
s.Notify.AMQP[accountID] = amqpn
}
func (s serverConfigV6) GetAMQP() map[string]amqpNotify {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Notify.AMQP
}
// GetAMQPNotify get current AMQP logger.
func (s serverConfigV6) GetAMQPNotifyByID(accountID string) amqpNotify {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Notify.AMQP[accountID]
}
func (s *serverConfigV6) SetElasticSearchNotifyByID(accountID string, esNotify elasticSearchNotify) {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
s.Notify.ElasticSearch[accountID] = esNotify
}
func (s serverConfigV6) GetElasticSearch() map[string]elasticSearchNotify {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Notify.ElasticSearch
}
// GetElasticSearchNotify get current ElasicSearch logger.
func (s serverConfigV6) GetElasticSearchNotifyByID(accountID string) elasticSearchNotify {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Notify.ElasticSearch[accountID]
}
func (s *serverConfigV6) SetRedisNotifyByID(accountID string, rNotify redisNotify) {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
s.Notify.Redis[accountID] = rNotify
}
func (s serverConfigV6) GetRedis() map[string]redisNotify {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Notify.Redis
}
// GetRedisNotify get current Redis logger.
func (s serverConfigV6) GetRedisNotifyByID(accountID string) redisNotify {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Notify.Redis[accountID]
}
// SetFileLogger set new file logger.
func (s *serverConfigV4) SetFileLogger(flogger fileLogger) {
func (s *serverConfigV6) SetFileLogger(flogger fileLogger) {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
s.Logger.File = flogger
}
// GetFileLogger get current file logger.
func (s serverConfigV4) GetFileLogger() fileLogger {
func (s serverConfigV6) GetFileLogger() fileLogger {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Logger.File
}
// SetConsoleLogger set new console logger.
func (s *serverConfigV4) SetConsoleLogger(clogger consoleLogger) {
func (s *serverConfigV6) SetConsoleLogger(clogger consoleLogger) {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
s.Logger.Console = clogger
}
// GetConsoleLogger get current console logger.
func (s serverConfigV4) GetConsoleLogger() consoleLogger {
func (s serverConfigV6) GetConsoleLogger() consoleLogger {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Logger.Console
}
// SetSyslogLogger set new syslog logger.
func (s *serverConfigV4) SetSyslogLogger(slogger syslogLogger) {
func (s *serverConfigV6) SetSyslogLogger(slogger syslogLogger) {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
s.Logger.Syslog = slogger
}
// GetSyslogLogger get current syslog logger.
func (s *serverConfigV4) GetSyslogLogger() syslogLogger {
func (s *serverConfigV6) GetSyslogLogger() syslogLogger {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Logger.Syslog
}
// SetRegion set new region.
func (s *serverConfigV4) SetRegion(region string) {
func (s *serverConfigV6) SetRegion(region string) {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
s.Region = region
}
// GetRegion get current region.
func (s serverConfigV4) GetRegion() string {
func (s serverConfigV6) GetRegion() string {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Region
}
// SetCredentials set new credentials.
func (s *serverConfigV4) SetCredential(creds credential) {
func (s *serverConfigV6) SetCredential(creds credential) {
s.rwMutex.Lock()
defer s.rwMutex.Unlock()
s.Credential = creds
}
// GetCredentials get current credentials.
func (s serverConfigV4) GetCredential() credential {
func (s serverConfigV6) GetCredential() credential {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
return s.Credential
}
// Save config.
func (s serverConfigV4) Save() *probe.Error {
func (s serverConfigV6) Save() error {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
// get config file.
configFile, err := getConfigFile()
if err != nil {
return err.Trace()
return err
}
// initialize quick.
qc, err := quick.New(&s)
if err != nil {
return err.Trace()
return err
}
// Save config file.
if err := qc.Save(configFile); err != nil {
return err.Trace()
}
// Return success.
return nil
return qc.Save(configFile)
}
+13 -17
View File
@@ -21,7 +21,6 @@ import (
"path/filepath"
"github.com/minio/go-homedir"
"github.com/minio/minio/pkg/probe"
)
// configPath for custom config path only for testing purposes
@@ -33,13 +32,13 @@ func setGlobalConfigPath(configPath string) {
}
// getConfigPath get server config path
func getConfigPath() (string, *probe.Error) {
func getConfigPath() (string, error) {
if customConfigPath != "" {
return customConfigPath, nil
}
homeDir, e := homedir.Dir()
if e != nil {
return "", probe.NewError(e)
homeDir, err := homedir.Dir()
if err != nil {
return "", err
}
configPath := filepath.Join(homeDir, globalMinioConfigDir)
return configPath, nil
@@ -48,27 +47,24 @@ func getConfigPath() (string, *probe.Error) {
// mustGetConfigPath must get server config path.
func mustGetConfigPath() string {
configPath, err := getConfigPath()
fatalIf(err.Trace(), "Unable to get config path.", nil)
fatalIf(err, "Unable to get config path.")
return configPath
}
// createConfigPath create server config path.
func createConfigPath() *probe.Error {
func createConfigPath() error {
configPath, err := getConfigPath()
if err != nil {
return err.Trace()
return err
}
if err := os.MkdirAll(configPath, 0700); err != nil {
return probe.NewError(err)
}
return nil
return os.MkdirAll(configPath, 0700)
}
// isConfigFileExists - returns true if config file exists.
func isConfigFileExists() bool {
st, e := os.Stat(mustGetConfigFile())
st, err := os.Stat(mustGetConfigFile())
// If file exists and is regular return true.
if e == nil && st.Mode().IsRegular() {
if err == nil && st.Mode().IsRegular() {
return true
}
return false
@@ -77,16 +73,16 @@ func isConfigFileExists() bool {
// mustGetConfigFile must get server config file.
func mustGetConfigFile() string {
configFile, err := getConfigFile()
fatalIf(err.Trace(), "Unable to get config file.", nil)
fatalIf(err, "Unable to get config file.")
return configFile
}
// getConfigFile get server config file.
func getConfigFile() (string, *probe.Error) {
func getConfigFile() (string, error) {
configPath, err := getConfigPath()
if err != nil {
return "", err.Trace()
return "", err
}
return filepath.Join(configPath, globalMinioConfigFile), nil
}
+49
View File
@@ -0,0 +1,49 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import "net/http"
// Standard cross domain policy information located at https://s3.amazonaws.com/crossdomain.xml
var crossDomainXML = `<?xml version="1.0"?><!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd"><cross-domain-policy><allow-access-from domain="*" secure="false" /></cross-domain-policy>`
// Cross domain policy implements http.Handler interface, implementing a custom ServerHTTP.
type crossDomainPolicy struct {
handler http.Handler
}
// A cross-domain policy file is an XML document that grants a web client, such as Adobe Flash Player
// or Adobe Acrobat (though not necessarily limited to these), permission to handle data across domains.
// When clients request content hosted on a particular source domain and that content make requests
// directed towards a domain other than its own, the remote domain needs to host a cross-domain
// policy file that grants access to the source domain, allowing the client to continue the transaction.
func setCrossDomainPolicy(h http.Handler) http.Handler {
return crossDomainPolicy{handler: h}
}
func (c crossDomainPolicy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Look for 'crossdomain.xml' in the incoming request.
switch r.URL.Path {
case "/crossdomain.xml":
// Write the standard cross domain policy xml.
w.Write([]byte(crossDomainXML))
// Request completed, no need to serve to other handlers.
return
}
// Continue to serve the request further.
c.handler.ServeHTTP(w, r)
}
+66
View File
@@ -0,0 +1,66 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"math"
"testing"
)
// Test minimum function which calculates the minimal value in a list of integers
func TestMinimum(t *testing.T) {
type testCase struct {
listval []int
expected int
pass bool
}
testCases := []testCase{
{listval: []int{3, 4, 15}, expected: 3},
{listval: []int{}, expected: math.MaxInt32},
}
// Validate all the test cases.
for i, tt := range testCases {
val := minimum(tt.listval)
if val != tt.expected {
t.Errorf("Test %d:, Expected %d, got %d", i+1, tt.expected, val)
}
}
}
// Test DamerauLevenshtein which calculates the difference distance between two words
func TestDamerauLevenshtein(t *testing.T) {
type testCase struct {
word1 string
word2 string
distance int
}
testCases := []testCase{
{word1: "", word2: "", distance: 0},
{word1: "a", word2: "a", distance: 0},
{word1: "a", word2: "b", distance: 1},
{word1: "rm", word2: "tm", distance: 1},
{word1: "version", word2: "evrsion", distance: 1},
{word1: "version", word2: "bersio", distance: 2},
}
// Validate all the test cases.
for i, tt := range testCases {
d := DamerauLevenshteinDistance(tt.word1, tt.word2)
if d != tt.distance {
t.Errorf("Test %d:, Expected %d, got %d", i+1, tt.distance, d)
}
}
}
Vendored Executable
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# usage: ./benchcmp.sh <commit-sha1> <commit-sha2>
# Exit on any non zero return value on execution of a command.
set -e
# path of benchcmp.
benchcmp=${GOPATH}/bin/benchcmp
# function which runs the benchmark comparison.
RunBenchCmp () {
# Path for storing output of benchmark at commit 1.
commit1Bench=/tmp/minio-$1.bench
# Path for storing output of benchmark at commit 2.
commit2Bench=/tmp/minio-$2.bench
# switch to commit $1.
git checkout $1
# Check if the benchmark results for given commit 1 already exists.
# Benchmarks are time/resource consuming operations, run only if the the results doesn't exist.
if [[ ! -f $commit1Bench ]]
then
echo "Running benchmarks at $1"
go test -run=NONE -bench=. | tee $commit1Bench
fi
# get back to the commit from which it was started.
git checkout -
echo "Checking into commit $2"
# switch to commit $2
git checkout $2
# Check if the benchmark results for given commit 2 already exists.
# Benchmarks are time/resource consuming operations, run only if the the results doesn't exist.
if [[ ! -f $commit2Bench ]]
then
# Running benchmarks at $2.
echo "Running benchmarks at $2"
go test -run=NONE -bench=. | tee $commit2Bench
fi
# get back to the commit from which it was started.
git checkout -
# Comparing the benchmarks.
echo "Running benchmark comparison between $1 and $2 ..."
$benchcmp $commit1Bench $commit2Bench
echo "Done."
}
# check if 2 commit SHA's of snapshots of code for which benchmp has to be done is provided.
if [ ! $# -eq 2 ]
then
# exit if commit SHA's are not provided.
echo $#
echo "Need Commit SHA's of 2 snapshots to be supplied to run benchmark comparison."
exit 1
fi
# check if benchcmp exists.
if [[ -x "$benchcmp" ]]
then
RunBenchCmp $1 $2
else
# install benchcmp if doesnt't exist.
echo "fetching Benchcmp..."
go get -u golang.org/x/tools/cmd/benchcmp
echo "Done."
RunBenchCmp $1 $2
fi
+50
View File
@@ -0,0 +1,50 @@
[Unit]
Description=Minio
Documentation=http://minio.io
Wants=network-online.target
After=network-online.target
AssertPathExists=/etc/minio/
AssertFileIsExecutable=/usr/share/minio/bin/minio
[Service]
WorkingDirectory=/usr/share/minio/
User=minio
Group=minio
PermissionsStartOnly=true
EnvironmentFile=-/etc/default/minio
ExecStartPre=/bin/bash -c "[ -n \"${MINIO_VOLUMES}\" ] || echo \"Variable MINIO_VOLUMES not set in /etc/defaults/minio\""
ExecStartPre=/bin/bash -c "(for i in $MINIO_VOLUMES; do if [ ! -d $i ]; then echo \"Directory $i of variable MINIO_VOLUMES isn't an existing directory\" && false; exit; fi; done)"
ExecStartPre=-/bin/chown -R root:minio /etc/minio/
ExecStartPre=-/bin/chmod -R 660 /etc/minio/
ExecStartPre=-/bin/chmod -R ug+X /etc/minio/
ExecStart=/usr/share/minio/bin/minio \
--config-dir "/etc/minio/" \
server \
$MINIO_OPTS \
$MINIO_VOLUMES
StandardOutput=journal
StandardError=inherit
# Specifies the maximum file descriptor number that can be opened by this process
LimitNOFILE=65536
# Disable timeout logic and wait until process is stopped
TimeoutStopSec=0
# SIGTERM signal is used to stop Minio
KillSignal=SIGTERM
SendSIGKILL=no
SuccessExitStatus=0
[Install]
WantedBy=multi-user.target
# Built for ${project.name}-${project.version} (${project.name})
+36
View File
@@ -0,0 +1,36 @@
# Readme
Service script for minio service for systemd.
# Installation
```
mkdir /etc/minio/
mkdir -p /usr/share/minio/bin/
wget -O /usr/share/minio/bin/minio http://...
chmod +x /usr/share/minio/bin/minio
```
Create minio user.
```
useradd minio
```
Create default configuration. Don't forget to update MINIO_VOLUMES with the correct path(s).
```
cat <<EOT >> /etc/default/minio
MINIO_OPTS="--address :9000"
MINIO_VOLUMES="/tmp/minio/"
EOT
```
# Systemctl
Put minio.service in /etc/systemd/system/
```
curl https://raw.githubusercontent.com/minio/minio/master/dist/linux-systemd/minio.service > /etc/systemd/system/
```
Enable startup on boot
```
systemctl enable minio.service
```
-8
View File
@@ -1,8 +0,0 @@
# use "make dockerimage" to build
FROM alpine:3.3
RUN apk add --no-cache ca-certificates bash
ADD minio.dockerimage /minio
ADD export /export
EXPOSE 9000
COPY start.sh /start.sh
ENTRYPOINT ["/start.sh"]
-10
View File
@@ -1,10 +0,0 @@
#!/bin/bash
## Fix this check when we arrive at XL.
if [ -z "$1" ]; then
echo "Invalid arguments"
echo "Usage: <export_dir>"
exit 1
else
/bin/mkdir -p "$1" && /minio server "$1"
fi
+18 -7
View File
@@ -1,28 +1,39 @@
### Run Minio docker image
# Minio Docker Quickstart Guide [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Test Minio Docker Container
## 1. Test Minio Docker Container
Minio generates new access and secret keys each time you run this command. Container state is lost after you end this session. This mode is only intended for testing purpose.
```bash
```sh
docker run -p 9000:9000 minio/minio /export
```
## Run Minio Docker Container
## 2. Run Minio Docker Container
Minio container requires a persistent volume to store configuration and application data. Following command maps local persistent directories from the host OS to virtual config `~/.minio` and export `/export` directories.
```bash
```sh
docker run -p 9000:9000 --name minio1 \
-v /mnt/export/minio1:/export \
-v /mnt/config/minio1:/root/.minio \
minio/minio /export
```
## Custom Access and Secret Keys
## 3. Custom Access and Secret Keys
To override Minio's auto-generated keys, you may pass secret and access keys explicitly as environment variables. Minio server also allows regular strings as access and secret keys.
```bash
```sh
docker run -p 9000:9000 --name minio1 \
-e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \
-e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
-v /mnt/export/minio1:/export \
-v /mnt/config/minio1:/root/.minio \
minio/minio /export
```
+193
View File
@@ -0,0 +1,193 @@
# Minio FreeBSD Quickstart Guide [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
### Minio with ZFS backend - FreeBSD
This example assumes that you have a FreeBSD 10.x running
#### Step 1.
As root on the FreeBSD edit `/etc/rc.conf`
```sh
zfs_enable="YES"
```
Start ZFS service
```sh
# service zfs start
```
```sh
# dd if=/dev/zero of=/zfs bs=1M count=4000
```
Configure a loopback device on the `/zfs` file.
```sh
# mdconfig -a -t vnode -f /zfs
```
Create zfs pool
```sh
# zpool create minio-example /dev/md0
```
```sh
# df /minio-example
Filesystem 512-blocks Used Avail Capacity Mounted on
minio-example 7872440 38 7872402 0% /minio-example
```
Verify if it is writable
```sh
# touch /minio-example/testfile
# ls -l /minio-example/testfile
-rw-r--r-- 1 root wheel 0 Apr 26 00:51 /minio-example/testfile
```
Now you have successfully created a ZFS pool for further reading please refer to [ZFS Quickstart Guide](https://www.freebsd.org/doc/handbook/zfs-quickstart.html)
However, this pool is not taking advantage of any ZFS features, so let's create a ZFS filesytem on this pool with compression enabled. ZFS supports many compression algorithms: lzjb, gzip, zle, lz4. LZ4 is often the most performant algorithm in terms of compression of data versus system overhead.
```sh
# zfs create minio-example/compressed-objects
# zfs set compression=lz4 minio-example/compressed-objects
```
To keep monitoring your pool use
```sh
# zpool status
pool: minio-example
state: ONLINE
scan: none requested
config:
NAME STATE READ WRITE CKSUM
minio-example ONLINE 0 0 0
md0 ONLINE 0 0 0
errors: No known data errors
```
#### Step 2.
Now start minio server on the ``/minio-example/compressed-objects``, change the permissions such that this directory is accessibly by a normal user
```sh
# chown -R minio-user:minio-user /minio-example/compressed-objects
```
Now login as ``minio-user`` and start minio server.
```sh
$ curl https://dl.minio.io/server/minio/release/freebsd-amd64/minio > minio
$ chmod 755 minio
$ ./minio server /minio-example/compressed-objects
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
AccessKey: USWUXHGYZQYFYFFIT3RE
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Region: us-east-1
Browser Access:
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
$ mc config host add myminio http://localhost:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
Object API (Amazon S3 compatible):
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
Java: https://docs.minio.io/docs/java-client-quickstart-guide
Python: https://docs.minio.io/docs/python-client-quickstart-guide
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
```
Point your browser to http://localhost:9000 and login with the credentials displayed on the command line.
Now you have a S3 compatible server running on top of your ZFS backend which transparently provides disk level compression for your uploaded objects.
Thanks for using Minio, awaiting feedback :-)
#### Building Minio Server From Source
It is possible to build the minio server from source on FreeBSD. To do this we will used the golang distribution provided by the FreeBSD pkg infrastructure.
First we will need to install golang as well as GNU make:
```sh
$ sudo pkg install go gmake
Updating FreeBSD repository catalogue..
FreeBSD repository is up-to-date.
All repositories are up-to-date.
The following 1 package(s) will be affected (of 0 checked):
New packages to be INSTALLED:
go: 1.6.2,1
gmake: 4.1_2
The process will require 261 MiB more space.
40 MiB to be downloaded.
Proceed with this action? [y/N]: y
Fetching go-1.6.2,1.txz: 100% 40 MiB 10.4MB/s 00:04
Fetching gmake-4.1_2.txz: 100% 363 KiB 372.2kB/s 00:01
Checking integrity... done (0 conflicting)
[1/2] Installing go-1.6.2,1...
[1/2] Extracting go-1.6.2,1: 100%
[2/2] Installing gmake-4.1_2...
[2/2] Extracting gmake-4.1_2: 100%
```
Next we need to configure our environment for golang. Insert the following lines into your ~/.profile file, and be sure to source the file before proceeding to the next step:
```sh
GOPATH=$HOME/golang; export GOPATH
GOROOT=/usr/local/go/; export GOROOT
```
Now we can proceed with the normal build process of minio server as found [here](https://github.com/nomadlogic/minio/blob/master/CONTRIBUTING.md). The only caveat is we need to specify gmake (GNU make) when building minio server as the current Makefile is not BSD make compatible:
```sh
$ mkdir -p $GOPATH/src/github.com/minio
$ cd $GOPATH/src/github.com/minio
$ git clone <paste saved URL for personal forked minio repo>
$ cd minio
$ gmake
```
From here you can start the server as you would with a precompiled minio server build.
+6
View File
@@ -0,0 +1,6 @@
## Backends
Minio currently implements two types of backends namely.
- Filesystem layer (fs).
- ErasureCode layer (XL).
+25
View File
@@ -0,0 +1,25 @@
### Backend format `fs.json`
```go
// objectPartInfo Info of each part kept in the multipart metadata
// file after CompleteMultipartUpload() is called.
type objectPartInfo struct {
Number int `json:"number"`
Name string `json:"name"`
ETag string `json:"etag"`
Size int64 `json:"size"`
}
// A fsMetaV1 represents a metadata header mapping keys to sets of values.
type fsMetaV1 struct {
Version string `json:"version"`
Format string `json:"format"`
Minio struct {
Release string `json:"release"`
} `json:"minio"`
// Metadata map for current object `fs.json`.
Meta map[string]string `json:"meta,omitempty"`
Parts []objectPartInfo `json:"parts,omitempty"`
}
```
+4
View File
@@ -0,0 +1,4 @@
{
"format": "fs",
"version": "1"
}
+19
View File
@@ -0,0 +1,19 @@
{
"version": "1.0.0",
"format": "fs",
"minio": {
"release": "DEVELOPMENT.GOGET"
},
"meta": {
"content-type": "binary/octet-stream",
"content-encoding": "gzip"
},
"parts": [
{
"number": 1,
"name": "part.1",
"size": 29,
"eTag": "",
},
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"version": "1",
"format": "fs",
"uploadIds": [
{
"uploadID": "id",
"startTime": "time",
}
]
}
+54
View File
@@ -0,0 +1,54 @@
### Backend format `xl.json`
```go
// objectPartInfo Info of each part kept in the multipart metadata
// file after CompleteMultipartUpload() is called.
type objectPartInfo struct {
Number int `json:"number"`
Name string `json:"name"`
ETag string `json:"etag"`
Size int64 `json:"size"`
}
// checkSumInfo - carries checksums of individual scattered parts per disk.
type checkSumInfo struct {
Name string `json:"name"`
Algorithm string `json:"algorithm"`
Hash string `json:"hash"`
}
// erasureInfo - carries erasure coding related information, block
// distribution and checksums.
type erasureInfo struct {
Algorithm string `json:"algorithm"`
DataBlocks int `json:"data"`
ParityBlocks int `json:"parity"`
BlockSize int64 `json:"blockSize"`
Index int `json:"index"`
Distribution []int `json:"distribution"`
Checksum []checkSumInfo `json:"checksum,omitempty"`
}
// statInfo - carries stat information of the object.
type statInfo struct {
Size int64 `json:"size"` // Size of the object `xl.json`.
ModTime time.Time `json:"modTime"` // ModTime of the object `xl.json`.
}
// A xlMetaV1 represents `xl.json` metadata header.
type xlMetaV1 struct {
Version string `json:"version"` // Version of the current `xl.json`.
Format string `json:"format"` // Format of the current `xl.json`.
Stat statInfo `json:"stat"` // Stat of the current object `xl.json`.
// Erasure coded info for the current object `xl.json`.
Erasure erasureInfo `json:"erasure"`
// Minio release tag for current object `xl.json`.
Minio struct {
Release string `json:"release"`
} `json:"minio"`
// Metadata map for current object `xl.json`.
Meta map[string]string `json:"meta,omitempty"`
// Captures all the individual object `xl.json`.
Parts []objectPartInfo `json:"parts,omitempty"`
}
```
+20
View File
@@ -0,0 +1,20 @@
{
"xl": {
"jbod": [
"8aa2b1bc-0e5a-49e0-8221-05228336b040",
"3467a69b-0266-478a-9e10-e819447e4545",
"d4a4505b-4e4f-4864-befd-4f36adb0bc66",
"592b6583-ca26-47af-b991-ba6d097e34e8",
"c7ef69f0-dbf5-4c0e-b167-d30a441bad7e",
"f0b36ea3-fe96-4f2b-bced-22c7f33e0e0c",
"b83abf39-e39d-4e7b-8e16-6f9953455a48",
"7d63dfc9-5441-4243-bd36-de8db0691982",
"c1bbffc5-81f9-4251-9398-33a959b3ce37",
"64408f94-26e0-4277-9593-2d703f4d5a91"
],
"disk": "8aa2b1bc-0e5a-49e0-8221-05228336b040",
"version": "1"
},
"format": "xl",
"version": "1"
}
+10
View File
@@ -0,0 +1,10 @@
{
"version": "1.0.0",
"format": "xl",
"uploadIds": [
{
"uploadID": "id",
"startTime": "time",
}
]
}
+50
View File
@@ -0,0 +1,50 @@
{
"parts": [
{
"number": 1,
"size": 5242880,
"etag": "3565c6e741e69a007a5ac7db893a62b5",
"name": "part.1"
},
{
"number": 2,
"size": 5242880,
"etag": "d416712335c280ab1e39498552937764",
"name": "part.2"
}
],
"meta": {
"md5Sum": "97586a5290d4f5a41328062d6a7da593-3",
"content-type": "application\/octet-stream",
"content-encoding": "gzip"
},
"minio": {
"release": "DEVELOPMENT.GOGET"
},
"erasure": {
"algorithm": "klauspost/reedsolomon/vandermonde",
"index": 2,
"distribution": [ 1, 3, 4, 2, 5, 8, 7, 6, 9 ],
"blockSize": 4194304,
"parity": 5,
"data": 5,
"checksum": [
{
"name": "part.1",
"algorithm": "blake2b",
"hash": "173c2cda7fb9e2798f91ba10135b65475fd2b97684355b328941d1c470a37549502672b2b4630c8268d5e5f79c8eb2fe433a25c6368f31d75b2c0504e3104c0e",
},
{
"name": "part.2",
"algorithm": "blake2b",
"hash": "14f9ba0006b2db7cd171507e79032ad3a34eab7df02b6564c487614b6d1e6613343a4f56074aa3473ac9c3b26e00cbbeae937d478cca4cfb138e72838ebb0826",
},
],
},
"stat": {
"modTime": "2016-05-24T00:09:40.122390255Z",
"size": 14824084
},
"format": "xl",
"version": "1.0.0"
}
+12 -11
View File
@@ -19,21 +19,22 @@ minio server <testdir>
JSON RPC namespace is `Web`.
#### Auth Operations
#### Server operations.
* ServerInfo - fetches current server information, includes memory statistics, minio binary
version, golang runtime version and more.
#### Auth operations
* Login - waits for 'username, password' and on success replies a new Json Web Token (JWT).
* ResetToken - resets token, requires password and token.
* Logout - currently a dummy operation.
* SetAuth - change access credentials with new 'username, password'.
* GetAuth - fetch the current auth from the server.
#### Bucket/Object Operations.
#### Bucket/Object operations.
* ListBuckets - lists buckets, requires a valid token.
* ListObjects - lists objects, requires a valid token.
* MakeBucket - make a new bucket, requires a valid token.
* GetObjectURL - generates a URL for download access, requires a valid token.
(generated URL is valid for 1hr)
* PutObjectURL - generates a URL for upload access, requies a valid token.
(generated URL is valid for 1hr)
#### Server Operations.
* DiskInfo - get backend disk statistics.
* RemoveObject - removes an object from a bucket, requires a valid token.
* Upload - uploads a new object from the browser, requires a valid token.
* Download - downloads an object from a bucket, requires a valid token.
+39
View File
@@ -0,0 +1,39 @@
## Object caching
Object caching by turned on by default with following settings
- Default cache size 8GB, can be changed from environment variable
``MINIO_CACHE_SIZE`` supports both SI and ISO IEC standard forms
for input size parameters.
- Default expiration of entries is 72 hours, can be changed from
environment variable ``MINIO_CACHE_EXPIRY`` supportings Go
``time.Duration`` with valid units "ns", "us" (or "µs"),
"ms", "s", "m", "h".
- Default expiry interval is 1/4th of the expiration hours, so
expiration sweep happens across the cache every 1/4th the time
duration of the set entry expiration duration.
### Tricks
Setting MINIO_CACHE_SIZE=0 will turn off caching entirely.
Setting MINIO_CACHE_EXPIRY=0s will turn off cache garbage collections,
all cached objects will never expire.
### Behavior
Caching happens for both GET and PUT.
- GET caches new objects for entries not found in cache,
otherwise serves from the cache.
- PUT/POST caches all successfully uploaded objects.
NOTE: Cache is not populated if there are any errors
while reading from the disk.
Expiration happens automatically based on the configured
interval as explained above, frequently accessed objects
stay alive for significantly longer time due to the fact
that expiration time is reset for every cache hit.
+58
View File
@@ -0,0 +1,58 @@
# Minio Erasure Code Quickstart Guide [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Minio protects data against hardware failures and silent data corruption using erasure code and checksums. You may lose half the number (N/2) of drives and still be able to recover the data.
## 1. Prerequisites:
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio)
## What is Erasure Code?
Erasure code is a mathematical algorithm to reconstruct missing or corrupted data. Minio uses Reed-Solomon code to shard objects into N/2 data and N/2 parity blocks. This means that in a 12 drive setup, an object is sharded across as 6 data and 6 parity blocks. You can lose as many as 6 drives (be it parity or data) and still reconstruct the data reliably from the remaining drives.
## Why is Erasure Code useful?
Erasure code protects data from multiple drives failure unlike RAID or replication. For eg RAID6 can protect against 2 drive failure whereas in Minio erasure code you can lose as many as half number of drives and still the data remains safe. Further Minio's erasure code is at object level and can heal one object at a time. For RAID, healing can only be performed at volume level which translates into huge down time. As Minio encodes each object individually with a high parity count. Storage servers once deployed should not require drive replacement or healing for the lifetime of the server. Minio's erasure coded backend is designed for operational efficiency and takes full advantage of hardware acceleration whenever available.
![Erasure](https://raw.githubusercontent.com/minio/minio/master/docs/screenshots/erasure-code.jpg?raw=true)
## What is Bit Rot protection?
Bit Rot also known as Data Rot or Silent Data Corruption is a serious data loss issue faced by disk drives today. Data on the drive may silently get corrupted without signalling an error has occurred. This makes Bit Rot more dangerous than permanent hard drive failure.
Minio's erasure coded backend uses high speed [BLAKE2](https://blog.minio.io/accelerating-blake2b-by-4x-using-simd-in-go-assembly-33ef16c8a56b#.jrp1fdwer) hash based checksums to protect against Bit Rot.
## Deployment Scenarios
Minio server runs on a variety of hardware, operating systems and virtual/container environments.
Minio erasure code backend is limited by design to a minimum of 6 drives and a maximum of 16 drives. The hard limit of 16 drives comes from operational experience. Failure domain becomes too large beyond 16 drives. If you need to scale beyond 16 drives, you may run multiple instances of Minio server on different ports.
#### Reference Physical Hardware:
* [SMC 5018A-AR12L (Intel Atom)](http://www.supermicro.com/products/system/1U/5018/SSG-5018A-AR12L.cfm?parts=SHOW) - SMC 1U SoC Atom C2750 platform with 12x 3.5” drive bays
* [Quanta Grid D51B-2U (OCP Compliant) ](http://www.qct.io/Product/Servers/Rackmount-Servers/2U/QuantaGrid-D51B-2U-p256c77c70c83c118)- Quanta 2U DP E5-2600v3 platform with 12x 3.5” drive bays
* [Cisco UCS C240 M4 Rack Server](http://www.cisco.com/c/en/us/products/servers-unified-computing/ucs-c240-m4-rack-server/index.html) - Cisco 2U DP E5-2600v3 platform with 12x 3.5” drive bays
* [Intel® Server System R2312WTTYSR](http://ark.intel.com/products/88286) - Intel 2U DP E5-2600v3 platform with 12x 3.5” drive bays
#### Reference Cloud Hosting Providers:
* [Packet](https://www.packet.net): Packet is the baremetal cloud provider.
* [Digital Ocean](https://www.digitalocean.com): Deploy an SSD cloud server in 55 seconds.
* [OVH](https://www.ovh.com/us): Build your own infrastructure with OVH public cloud.
* [Onlinetech](http://www.onlinetech.com): Secure, compliant enterprise cloud.
* [SSD Nodes](https://www.ssdnodes.com): Simple, high performance cloud provider with truly personalized support.
## 2. Run Minio Server with Erasure Code.
Example: Start Minio server in a 12 drives setup.
```sh
$ minio server /mnt/export1/backend /mnt/export2/backend /mnt/export3/backend /mnt/export4/backend /mnt/export5/backend /mnt/export6/backend /mnt/export7/backend /mnt/export8/backend /mnt/export9/backend /mnt/export10/backend /mnt/export11/backend /mnt/export12/backend
```
## 3. Test your setup
You may unplug drives randomly and continue to perform I/O on the system.
@@ -0,0 +1,80 @@
# How to run multiple Minio server instances on single machine. [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
![minio_MULTIVERSE](https://github.com/minio/minio/blob/master/docs/screenshots/multiport.jpeg?raw=true)
In this document we will illustrate how to set up multiple Minio server instances on single machine. These Minio servers are running on their own port, data directory & configuration directory.
## 1. Prerequisites
* Download Minio server from [here](https://docs.minio.io/docs/minio)
* Download & Install mc from [here](https://docs.minio.io/docs/minio-client-quickstart-guide)
* Ports should be available for Minio server's use
## 2. Install and Configure Minio Server
Minio server is running on port 9002, 9003, 9004 with associated data directory and configuration file directory.
**Minio server on port ``9002``**
```sh
$ ./minio -C /home/minio/minio1/.minio server --address 192.168.1.11:9002 minio1/data1/
Endpoint: http://192.168.1.11:9002
AccessKey: XTW9SWKRWYKWE9M9K9RG
SecretKey: pZehbS5UNrA9BAhYHnWC/QVvQ7vGVge48WGHzG9t
Region: us-east-1
```
**Minio server on port ``9003``**
```sh
$ ./minio -C /home/minio/minio2/.minio server --address 192.168.1.11:9003 minio1/data2/
Endpoint: http://192.168.1.11:9003
AccessKey: UTD2WWPJOK754KMZKHWF
SecretKey: DbikDIY4+wItcexJa4nyrwQC0V2r7kLsK5SsRgHb
Region: us-east-1
```
**Minio server on port ``9004``**
```sh
$ ./minio -C /home/minio/minio3/.minio server --address 192.168.1.11:9004 minio1/data3/
Endpoint: http://192.168.1.11:9004
AccessKey: KXLOJ908VEJ2K9RGUFHQ
SecretKey: LpkeePMtEWAa6payiCovfrNKiFHhABsJhMwGynF8
Region: us-east-1
```
This is how directory structure will look like for ``minio1``, replace it with your local setup.
```sh
$ tree -la minio1/
minio1/
├── data1
└── .minio
├── certs
├── config.json
└── config.json.old
3 directories, 2 files
```
**Testing it all**
Using terminal comamnd ``netstat`` we can see ``Minio Server`` is running on different ports on same machine.
```sh
$ netstat -ntlp | grep minio
tcp 0 0 192.168.1.11:9002 0.0.0.0:* LISTEN 29573/minio
tcp 0 0 192.168.1.11:9003 0.0.0.0:* LISTEN 29597/minio
tcp 0 0 192.168.1.11:9004 0.0.0.0:* LISTEN 29631/minio
```
# Explore Further
* [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide)
* [Minio Client Complete Guide](https://docs.minio.io/docs/minio-client-complete-guide)
@@ -0,0 +1,110 @@
# Minio Server configuration files Guide [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
In this document we will walk through the configuration files of Minio Server.
## Minio Server configuration directory
Minio Server configurations are stored in file name ``.minio``. It's a hidden file which resides on user's home directory.
**This how the structure of the directory looks like:**
```sh
$ tree ~/.minio/
/Users/supernova/.minio/
├── certs
├── config.json
└── config.json.old
1 directory, 2 files
```
### Files and directories.
##### ``certs`` directory
``certs`` directory stores key & cert information, which are needed to run Minio in ``HTTPS``. You can read more on running Minio with ``HTTPS`` with Let's Encrypt cert with Concert [here](https://docs.minio.io/docs/generate-let-s-encypt-certificate-using-concert-for-minio)
##### ``config.json``
config.json is the configuration file for Minio, it gets generated after you install and start Minio.
```sh
$ cat config.json
{
"version": "6",
"credential": {
"accessKey": "YI7S1CKXB76RGOGT6R8W",
"secretKey": "FJ9PWUVNXGPfiI72WMRFepN3LsFgW3MjsxSALroV"
},
"region": "us-east-1",
"logger": {
"console": {
"enable": true,
"level": "fatal"
},
"file": {
"enable": false,
"fileName": "",
"level": "error"
},
"syslog": {
"enable": false,
"address": "",
"level": "debug"
}
},
"notify": {
"amqp": {
"1": {
"enable": false,
"url": "",
"exchange": "",
"routingKey": "",
"exchangeType": "",
"mandatory": false,
"immediate": false,
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
}
},
"elasticsearch": {
"1": {
"enable": false,
"url": "",
"index": ""
}
},
"redis": {
"1": {
"enable": false,
"address": "",
"password": "",
"key": ""
}
}
}
}
```
``version`` : Represents `version` number of current configuration file.
``credential`` : Represents authentication credentials for the server, value is automatically generated upon first server start.
``region`` : Represents deployment region for the server, value defaults to `us-east-1`.
``logger `` : Represents various logging types supported for server error logs, console logger is enabled by default.
``notify``: Represents various notification types supported. These notification types should be configured prior to using bucket
##### ``config.json.old``
This file keeps previous config file version details.
## Explore Further
* [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide)
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

@@ -0,0 +1,116 @@
# How to use ``mc mirror`` to setup replication between two sites running Minio. [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
![minio_MIRROR](https://github.com/minio/minio/blob/master/docs/screenshots/miniomirror.jpeg?raw=true)
In this document we will illustrate how to set up replication between two Minio servers, `minio1` and `minio2` running on ``192.168.1.11`` and ``192.168.1.12`` respectively. We will mirror the data directory on `minio1` to the bucket on `minio2`.
## 1. Prerequisites
* Download Minio server from [here](https://docs.minio.io/docs/minio)
* Download & Install mc from [here](https://docs.minio.io/docs/minio-client-quickstart-guide)
* Familiarity with [``mc mirror``](https://docs.minio.io/docs/minio-client-complete-guide#mirror)
## 2. Install and Configure Minio Server
### Setup minio1
```sh
$ ./minio server minio1-data/
Endpoint: http://127.0.0.1:9000 http://192.168.1.11:9000
AccessKey: MURIVYBYNPTYE7O8I779
SecretKey: lVbZmz4CvGkBl7JKw5icuL7RCcSvpBJTkAJTFQwz
Region: us-east-1
```
**mc alias**
Alias is a short name to your cloud storage service for ``Minio client``. End-point, access and secret keys are supplied by your cloud storage provider. API signature is an optional argument. By default, it is set to ``S3v4``.
```sh
$ ./mc config host add minio1 http://192.168.1.11:9000 MURIVYBYNPTYE7O8I779 lVbZmz4CvGkBl7JKw5icuL7RCcSvpBJTkAJTFQwz
```
**Create buckets and add objects**
We have created few buckets using [``mc mb``](https://docs.minio.io/docs/minio-client-complete-guide#mb) and added objects to it using [``mc cp``](https://docs.minio.io/docs/minio-client-complete-guide#cp) Minio client commands.
```sh
$ ./mc mb minio1/mybucket
$ ./mc cp myfile.txt minio1/bucket1
$ ./mc ls minio1
[2016-07-31 10:26:55 PDT] 0B bucket1/
[2016-07-31 09:36:17 PDT] 0B bucket2/
[2016-07-31 09:38:08 PDT] 0B bucket3/
```
### Setup minio2
```sh
$ ./minio server minio2-data/
Endpoint: http://127.0.0.1:9000 http://192.168.1.12:9000
AccessKey: YRDRWWQLEWS9OBJ31GZ2
SecretKey: y2sSWzx5ytwvkELcxOuSaQ8n3doNqoIilRpb5Kjj
Region: us-east-1
```
```sh
$ ./mc config host add minio2 http://192.168.1.12:9000 YRDRWWQLEWS9OBJ31GZ2 y2sSWzx5ytwvkELcxOuSaQ8n3doNqoIilRpb5Kjj
```
**Create bucket**
We are creating destination bucket ``mbucket`` on ``minio2`` and adding ``minio2`` alias. The bucket ``mbucket`` will be used to mirror data directory of ``minio1``.
```sh
$ ./mc mb minio1/mbucket
```
```sh
$ ./mc config host add minio2 http://192.168.1.12:9000 YRDRWWQLEWS9OBJ31GZ2 y2sSWzx5ytwvkELcxOuSaQ8n3doNqoIilRpb5Kjj
```
## 4. Setup crontab
Cron is a Unix/Linux system utility by which you can schedule a task process for particular duration, we have tested this setup on Ubuntu Linux.
### Script
Add crontab configuration on `minio1` providing path of data directory, ``minio1-data``.
``--force`` option with ``mc mirror`` overwrites the destination contents, this would keep your contents in sync.
```sh
#!/bin/bash
datadir="/home/minio/minio1-data/"
minio2="minio2/mbucket"
MC_PATH="/home/minio/mc"
$MC_PATH --quiet mirror --force $minio1 $minio2
```
Set executable permissions on the script before adding a cron entry.
```sh
$ chmod 755 /home/minio/minio.sh
```
Set a new cron entry to run ``minio.sh`` script once every 30mins.
```sh
$ crontab -e
*/30 * * * * /home/minio/minio.sh
```
Note: We are going to introduce continuous replication feature in `mc mirror` which will enable the sites to be in sync without having the need to setup cron job.
# Explore Further
* [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide)
* [Minio Client Complete Guide](https://docs.minio.io/docs/minio-client-complete-guide)
+145
View File
@@ -0,0 +1,145 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"encoding/hex"
"hash"
"io"
"sync"
"github.com/klauspost/reedsolomon"
)
// erasureCreateFile - writes an entire stream by erasure coding to
// all the disks, writes also calculate individual block's checksum
// for future bit-rot protection.
func erasureCreateFile(disks []StorageAPI, volume, path string, reader io.Reader, blockSize int64, dataBlocks int, parityBlocks int, algo string, writeQuorum int) (bytesWritten int64, checkSums []string, err error) {
// Allocated blockSized buffer for reading.
buf := make([]byte, blockSize)
hashWriters := newHashWriters(len(disks), algo)
// Read until io.EOF, erasure codes data and writes to all disks.
for {
var blocks [][]byte
n, rErr := io.ReadFull(reader, buf)
// FIXME: this is a bug in Golang, n == 0 and err ==
// io.ErrUnexpectedEOF for io.ReadFull function.
if n == 0 && rErr == io.ErrUnexpectedEOF {
return 0, nil, rErr
}
if rErr == io.EOF {
// We have reached EOF on the first byte read, io.Reader
// must be 0bytes, we don't need to erasure code
// data. Will create a 0byte file instead.
if bytesWritten == 0 {
blocks = make([][]byte, len(disks))
rErr = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum)
if rErr != nil {
return 0, nil, rErr
}
} // else we have reached EOF after few reads, no need to
// add an additional 0bytes at the end.
break
}
if rErr != nil && rErr != io.ErrUnexpectedEOF {
return 0, nil, rErr
}
if n > 0 {
// Returns encoded blocks.
var enErr error
blocks, enErr = encodeData(buf[0:n], dataBlocks, parityBlocks)
if enErr != nil {
return 0, nil, enErr
}
// Write to all disks.
if err = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum); err != nil {
return 0, nil, err
}
bytesWritten += int64(n)
}
}
checkSums = make([]string, len(disks))
for i := range checkSums {
checkSums[i] = hex.EncodeToString(hashWriters[i].Sum(nil))
}
return bytesWritten, checkSums, nil
}
// encodeData - encodes incoming data buffer into
// dataBlocks+parityBlocks returns a 2 dimensional byte array.
func encodeData(dataBuffer []byte, dataBlocks, parityBlocks int) ([][]byte, error) {
rs, err := reedsolomon.New(dataBlocks, parityBlocks)
if err != nil {
return nil, err
}
// Split the input buffer into data and parity blocks.
var blocks [][]byte
blocks, err = rs.Split(dataBuffer)
if err != nil {
return nil, err
}
// Encode parity blocks using data blocks.
err = rs.Encode(blocks)
if err != nil {
return nil, err
}
// Return encoded blocks.
return blocks, nil
}
// appendFile - append data buffer at path.
func appendFile(disks []StorageAPI, volume, path string, enBlocks [][]byte, hashWriters []hash.Hash, writeQuorum int) (err error) {
var wg = &sync.WaitGroup{}
var wErrs = make([]error, len(disks))
// Write encoded data to quorum disks in parallel.
for index, disk := range disks {
if disk == nil {
continue
}
wg.Add(1)
// Write encoded data in routine.
go func(index int, disk StorageAPI) {
defer wg.Done()
wErr := disk.AppendFile(volume, path, enBlocks[index])
if wErr != nil {
wErrs[index] = wErr
return
}
// Calculate hash for each blocks.
hashWriters[index].Write(enBlocks[index])
// Successfully wrote.
wErrs[index] = nil
}(index, disk)
}
// Wait for all the appends to finish.
wg.Wait()
// Do we have write quorum?.
if !isDiskQuorum(wErrs, writeQuorum) {
return errXLWriteQuorum
}
return nil
}
+203
View File
@@ -0,0 +1,203 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"crypto/rand"
"testing"
"github.com/klauspost/reedsolomon"
)
// Simulates a faulty disk for AppendFile()
type AppendDiskDown struct {
*posix
}
func (a AppendDiskDown) AppendFile(volume string, path string, buf []byte) error {
return errFaultyDisk
}
// Test erasureCreateFile()
func TestErasureCreateFile(t *testing.T) {
// Initialize environment needed for the test.
dataBlocks := 7
parityBlocks := 7
blockSize := int64(blockSizeV1)
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
if err != nil {
t.Error(err)
return
}
defer setup.Remove()
disks := setup.disks
// Prepare a slice of 1MB with random data.
data := make([]byte, 1*1024*1024)
_, err = rand.Read(data)
if err != nil {
t.Fatal(err)
}
// Test when all disks are up.
size, _, err := erasureCreateFile(disks, "testbucket", "testobject1", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
if err != nil {
t.Fatal(err)
}
if size != int64(len(data)) {
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
}
// 2 disks down.
disks[4] = AppendDiskDown{disks[4].(*posix)}
disks[5] = AppendDiskDown{disks[5].(*posix)}
// Test when two disks are down.
size, _, err = erasureCreateFile(disks, "testbucket", "testobject2", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
if err != nil {
t.Fatal(err)
}
if size != int64(len(data)) {
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
}
// 4 more disks down. 6 disks down in total.
disks[6] = AppendDiskDown{disks[6].(*posix)}
disks[7] = AppendDiskDown{disks[7].(*posix)}
disks[8] = AppendDiskDown{disks[8].(*posix)}
disks[9] = AppendDiskDown{disks[9].(*posix)}
size, _, err = erasureCreateFile(disks, "testbucket", "testobject3", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
if err != nil {
t.Fatal(err)
}
if size != int64(len(data)) {
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
}
// 1 more disk down. 7 disk down in total. Should return quorum error.
disks[10] = AppendDiskDown{disks[10].(*posix)}
_, _, err = erasureCreateFile(disks, "testbucket", "testobject4", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
if err != errXLWriteQuorum {
t.Errorf("erasureCreateFile returned expected errXLWriteQuorum error, got %s", err)
}
}
// TestErasureEncode checks for encoding for different data sets.
func TestErasureEncode(t *testing.T) {
// Collection of cases for encode cases.
testEncodeCases := []struct {
inputData []byte
inputDataBlocks int
inputParityBlocks int
shouldPass bool
expectedErr error
}{
// TestCase - 1.
// Regular data encoded.
{
[]byte("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."),
8,
8,
true,
nil,
},
// TestCase - 2.
// Empty data errors out.
{
[]byte(""),
8,
8,
false,
reedsolomon.ErrShortData,
},
// TestCase - 3.
// Single byte encoded.
{
[]byte("1"),
4,
4,
true,
nil,
},
// TestCase - 4.
// test case with negative data block.
{
[]byte("1"),
-1,
8,
false,
reedsolomon.ErrInvShardNum,
},
// TestCase - 5.
// test case with negative parity block.
{
[]byte("1"),
8,
-1,
false,
reedsolomon.ErrInvShardNum,
},
// TestCase - 6.
// test case with zero data block.
{
[]byte("1"),
0,
8,
false,
reedsolomon.ErrInvShardNum,
},
// TestCase - 7.
// test case with zero parity block.
{
[]byte("1"),
8,
0,
false,
reedsolomon.ErrInvShardNum,
},
// TestCase - 8.
// test case with data + parity blocks > 255.
// expected to fail with Error Max Shard number.
{
[]byte("1"),
128,
128,
false,
reedsolomon.ErrMaxShardNum,
},
}
// Test encode cases.
for i, testCase := range testEncodeCases {
_, actualErr := encodeData(testCase.inputData, testCase.inputDataBlocks, testCase.inputParityBlocks)
if actualErr != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass but failed instead with \"%s\"", i+1, actualErr)
}
if actualErr == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with error <Error> \"%v\", but instead passed", i+1, testCase.expectedErr)
}
// Failed as expected, but does it fail for the expected reason.
if actualErr != nil && !testCase.shouldPass {
if testCase.expectedErr != actualErr {
t.Errorf("Test %d: Expected Error to be \"%v\", but instead found \"%v\" ", i+1, testCase.expectedErr, actualErr)
}
}
}
}
+350
View File
@@ -0,0 +1,350 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"encoding/hex"
"errors"
"io"
"sync"
"github.com/klauspost/reedsolomon"
"github.com/minio/minio/pkg/bpool"
)
// isSuccessDecodeBlocks - do we have all the blocks to be
// successfully decoded?. Input encoded blocks ordered matrix.
func isSuccessDecodeBlocks(enBlocks [][]byte, dataBlocks int) bool {
// Count number of data and parity blocks that were read.
var successDataBlocksCount = 0
var successParityBlocksCount = 0
for index := range enBlocks {
if enBlocks[index] == nil {
continue
}
// block index lesser than data blocks, update data block count.
if index < dataBlocks {
successDataBlocksCount++
continue
} // else { // update parity block count.
successParityBlocksCount++
}
// Returns true if we have atleast dataBlocks parity.
return successDataBlocksCount == dataBlocks || successDataBlocksCount+successParityBlocksCount >= dataBlocks
}
// isSuccessDataBlocks - do we have all the data blocks?
// Input encoded blocks ordered matrix.
func isSuccessDataBlocks(enBlocks [][]byte, dataBlocks int) bool {
// Count number of data blocks that were read.
var successDataBlocksCount = 0
for index := range enBlocks[:dataBlocks] {
if enBlocks[index] == nil {
continue
}
// block index lesser than data blocks, update data block count.
if index < dataBlocks {
successDataBlocksCount++
}
}
// Returns true if we have atleast the dataBlocks.
return successDataBlocksCount >= dataBlocks
}
// Return readable disks slice from which we can read parallelly.
func getReadDisks(orderedDisks []StorageAPI, index int, dataBlocks int) (readDisks []StorageAPI, nextIndex int, err error) {
readDisks = make([]StorageAPI, len(orderedDisks))
dataDisks := 0
parityDisks := 0
// Count already read data and parity chunks.
for i := 0; i < index; i++ {
if orderedDisks[i] == nil {
continue
}
if i < dataBlocks {
dataDisks++
} else {
parityDisks++
}
}
// Sanity checks - we should never have this situation.
if dataDisks == dataBlocks {
return nil, 0, errUnexpected
}
if dataDisks+parityDisks >= dataBlocks {
return nil, 0, errUnexpected
}
// Find the disks from which next set of parallel reads should happen.
for i := index; i < len(orderedDisks); i++ {
if orderedDisks[i] == nil {
continue
}
if i < dataBlocks {
dataDisks++
} else {
parityDisks++
}
readDisks[i] = orderedDisks[i]
if dataDisks == dataBlocks {
return readDisks, i + 1, nil
} else if dataDisks+parityDisks == dataBlocks {
return readDisks, i + 1, nil
}
}
return nil, 0, errXLReadQuorum
}
// parallelRead - reads chunks in parallel from the disks specified in []readDisks.
func parallelRead(volume, path string, readDisks []StorageAPI, orderedDisks []StorageAPI, enBlocks [][]byte, blockOffset int64, curChunkSize int64, bitRotVerify func(diskIndex int) bool, pool *bpool.BytePool) {
// WaitGroup to synchronise the read go-routines.
wg := &sync.WaitGroup{}
// Read disks in parallel.
for index := range readDisks {
if readDisks[index] == nil {
continue
}
wg.Add(1)
// Reads chunk from readDisk[index] in routine.
go func(index int) {
defer wg.Done()
// Verify bit rot for the file on this disk.
if !bitRotVerify(index) {
// So that we don't read from this disk for the next block.
orderedDisks[index] = nil
return
}
buf, err := pool.Get()
if err != nil {
errorIf(err, "unable to get buffer from byte pool")
orderedDisks[index] = nil
return
}
buf = buf[:curChunkSize]
_, err = readDisks[index].ReadFile(volume, path, blockOffset, buf)
if err != nil {
orderedDisks[index] = nil
return
}
enBlocks[index] = buf
}(index)
}
// Waiting for first routines to finish.
wg.Wait()
}
// erasureReadFile - read bytes from erasure coded files and writes to given writer.
// Erasure coded files are read block by block as per given erasureInfo and data chunks
// are decoded into a data block. Data block is trimmed for given offset and length,
// then written to given writer. This function also supports bit-rot detection by
// verifying checksum of individual block's checksum.
func erasureReadFile(writer io.Writer, disks []StorageAPI, volume string, path string, offset int64, length int64, totalLength int64, blockSize int64, dataBlocks int, parityBlocks int, checkSums []string, algo string, pool *bpool.BytePool) (int64, error) {
// Offset and length cannot be negative.
if offset < 0 || length < 0 {
return 0, errUnexpected
}
// Can't request more data than what is available.
if offset+length > totalLength {
return 0, errUnexpected
}
// chunkSize is the amount of data that needs to be read from each disk at a time.
chunkSize := getChunkSize(blockSize, dataBlocks)
// bitRotVerify verifies if the file on a particular disk doesn't have bitrot
// by verifying the hash of the contents of the file.
bitRotVerify := func() func(diskIndex int) bool {
verified := make([]bool, len(disks))
// Return closure so that we have reference to []verified and
// not recalculate the hash on it every time the function is
// called for the same disk.
return func(diskIndex int) bool {
if verified[diskIndex] {
// Already validated.
return true
}
// Is this a valid block?
isValid := isValidBlock(disks[diskIndex], volume, path, checkSums[diskIndex], algo)
verified[diskIndex] = isValid
return isValid
}
}()
// Total bytes written to writer
bytesWritten := int64(0)
startBlock := offset / blockSize
endBlock := (offset + length) / blockSize
// curChunkSize = chunk size for the current block in the for loop below.
// curBlockSize = block size for the current block in the for loop below.
// curChunkSize and curBlockSize can change for the last block if totalLength%blockSize != 0
curChunkSize := chunkSize
curBlockSize := blockSize
// For each block, read chunk from each disk. If we are able to read all the data disks then we don't
// need to read parity disks. If one of the data disk is missing we need to read DataBlocks+1 number
// of disks. Once read, we Reconstruct() missing data if needed and write it to the given writer.
for block := startBlock; block <= endBlock; block++ {
// Mark all buffers as unused at the start of the loop so that the buffers
// can be reused.
pool.Reset()
// Each element of enBlocks holds curChunkSize'd amount of data read from its corresponding disk.
enBlocks := make([][]byte, len(disks))
if ((offset + bytesWritten) / blockSize) == (totalLength / blockSize) {
// This is the last block for which curBlockSize and curChunkSize can change.
// For ex. if totalLength is 15M and blockSize is 10MB, curBlockSize for
// the last block should be 5MB.
curBlockSize = totalLength % blockSize
curChunkSize = getChunkSize(curBlockSize, dataBlocks)
}
// NOTE: That for the offset calculation we have to use chunkSize and
// not curChunkSize. If we use curChunkSize for offset calculation
// then it can result in wrong offset for the last block.
blockOffset := block * chunkSize
// nextIndex - index from which next set of parallel reads
// should happen.
nextIndex := 0
for {
// readDisks - disks from which we need to read in parallel.
var readDisks []StorageAPI
var err error
// get readable disks slice from which we can read parallelly.
readDisks, nextIndex, err = getReadDisks(disks, nextIndex, dataBlocks)
if err != nil {
return bytesWritten, err
}
// Issue a parallel read across the disks specified in readDisks.
parallelRead(volume, path, readDisks, disks, enBlocks, blockOffset, curChunkSize, bitRotVerify, pool)
if isSuccessDecodeBlocks(enBlocks, dataBlocks) {
// If enough blocks are available to do rs.Reconstruct()
break
}
if nextIndex == len(disks) {
// No more disks to read from.
return bytesWritten, errXLReadQuorum
}
// We do not have enough enough data blocks to reconstruct the data
// hence continue the for-loop till we have enough data blocks.
}
// If we have all the data blocks no need to decode, continue to write.
if !isSuccessDataBlocks(enBlocks, dataBlocks) {
// Reconstruct the missing data blocks.
if err := decodeData(enBlocks, dataBlocks, parityBlocks); err != nil {
return bytesWritten, err
}
}
// Offset in enBlocks from where data should be read from.
enBlocksOffset := int64(0)
// Total data to be read from enBlocks.
enBlocksLength := curBlockSize
// If this is the start block then enBlocksOffset might not be 0.
if block == startBlock {
enBlocksOffset = offset % blockSize
enBlocksLength -= enBlocksOffset
}
remaining := length - bytesWritten
if remaining < enBlocksLength {
// We should not send more data than what was requested.
enBlocksLength = remaining
}
// Write data blocks.
n, err := writeDataBlocks(writer, enBlocks, dataBlocks, enBlocksOffset, enBlocksLength)
if err != nil {
return bytesWritten, err
}
// Update total bytes written.
bytesWritten += n
if bytesWritten == length {
// Done writing all the requested data.
break
}
}
// Success.
return bytesWritten, nil
}
// isValidBlock - calculates the checksum hash for the block and
// validates if its correct returns true for valid cases, false otherwise.
func isValidBlock(disk StorageAPI, volume, path, checkSum, checkSumAlgo string) (ok bool) {
// Disk is not available, not a valid block.
if disk == nil {
return false
}
// Checksum not available, not a valid block.
if checkSum == "" {
return false
}
// Read everything for a given block and calculate hash.
hashWriter := newHash(checkSumAlgo)
hashBytes, err := hashSum(disk, volume, path, hashWriter)
if err != nil {
errorIf(err, "Unable to calculate checksum %s/%s", volume, path)
return false
}
return hex.EncodeToString(hashBytes) == checkSum
}
// decodeData - decode encoded blocks.
func decodeData(enBlocks [][]byte, dataBlocks, parityBlocks int) error {
// Initialized reedsolomon.
rs, err := reedsolomon.New(dataBlocks, parityBlocks)
if err != nil {
return err
}
// Reconstruct encoded blocks.
err = rs.Reconstruct(enBlocks)
if err != nil {
return err
}
// Verify reconstructed blocks (parity).
ok, err := rs.Verify(enBlocks)
if err != nil {
return err
}
if !ok {
// Blocks cannot be reconstructed, corrupted data.
err = errors.New("Verification failed after reconstruction, data likely corrupted.")
return err
}
// Success.
return nil
}
+459
View File
@@ -0,0 +1,459 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"math/rand"
"testing"
"time"
"github.com/minio/minio/pkg/bpool"
)
import "reflect"
// Tests getReadDisks which returns readable disks slice from which we can
// read parallelly.
func testGetReadDisks(t *testing.T, xl xlObjects) {
d := xl.storageDisks
testCases := []struct {
index int // index argument for getReadDisks
argDisks []StorageAPI // disks argument for getReadDisks
retDisks []StorageAPI // disks return value from getReadDisks
nextIndex int // return value from getReadDisks
err error // error return value from getReadDisks
}{
// Test case - 1.
// When all disks are available, should return data disks.
{
0,
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, nil, nil, nil, nil, nil, nil},
8,
nil,
},
// Test case - 2.
// If a parity disk is down, should return all data disks.
{
0,
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], nil, d[10], d[11], d[12], d[13], d[14], d[15]},
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, nil, nil, nil, nil, nil, nil},
8,
nil,
},
// Test case - 3.
// If a data disk is down, should return 7 data and 1 parity.
{
0,
[]StorageAPI{nil, d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
[]StorageAPI{nil, d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], nil, nil, nil, nil, nil, nil, nil},
9,
nil,
},
// Test case - 4.
// If 7 data disks are down, should return 1 data and 7 parity.
{
0,
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], nil},
15,
nil,
},
// Test case - 5.
// When 2 disks fail during parallelRead, next call to getReadDisks should return 3 disks
{
8,
[]StorageAPI{nil, nil, d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, nil, d[8], d[9], nil, nil, nil, nil, nil, nil},
10,
nil,
},
// Test case - 6.
// If 2 disks again fail from the 3 disks returned previously, return next 2 disks
{
11,
[]StorageAPI{nil, nil, d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, d[10], d[11], d[12], d[13], d[14], d[15]},
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, d[11], nil, nil, nil, nil},
12,
nil,
},
// Test case - 7.
// No more disks are available for read, return error
{
13,
[]StorageAPI{nil, nil, d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, d[10], nil, nil, nil, nil, nil},
nil,
0,
errXLReadQuorum,
},
}
for i, test := range testCases {
disks, nextIndex, err := getReadDisks(test.argDisks, test.index, xl.dataBlocks)
if err != test.err {
t.Errorf("test-case %d - expected error : %s, got : %s", i+1, test.err, err)
continue
}
if test.nextIndex != nextIndex {
t.Errorf("test-case %d - expected nextIndex: %d, got : %d", i+1, test.nextIndex, nextIndex)
continue
}
if reflect.DeepEqual(test.retDisks, disks) == false {
t.Errorf("test-case %d : incorrect disks returned. expected %+v, got %+v", i+1, test.retDisks, disks)
continue
}
}
}
// Test getOrderedDisks which returns ordered slice of disks from their
// actual distribution.
func testGetOrderedDisks(t *testing.T, xl xlObjects) {
disks := xl.storageDisks
distribution := []int{16, 14, 12, 10, 8, 6, 4, 2, 1, 3, 5, 7, 9, 11, 13, 15}
orderedDisks := getOrderedDisks(distribution, disks)
// From the "distribution" above you can notice that:
// 1st data block is in the 9th disk (i.e distribution index 8)
// 2nd data block is in the 8th disk (i.e distribution index 7) and so on.
if orderedDisks[0] != disks[8] ||
orderedDisks[1] != disks[7] ||
orderedDisks[2] != disks[9] ||
orderedDisks[3] != disks[6] ||
orderedDisks[4] != disks[10] ||
orderedDisks[5] != disks[5] ||
orderedDisks[6] != disks[11] ||
orderedDisks[7] != disks[4] ||
orderedDisks[8] != disks[12] ||
orderedDisks[9] != disks[3] ||
orderedDisks[10] != disks[13] ||
orderedDisks[11] != disks[2] ||
orderedDisks[12] != disks[14] ||
orderedDisks[13] != disks[1] ||
orderedDisks[14] != disks[15] ||
orderedDisks[15] != disks[0] {
t.Errorf("getOrderedDisks returned incorrect order.")
}
}
// Test for isSuccessDataBlocks and isSuccessDecodeBlocks.
func TestIsSuccessBlocks(t *testing.T) {
dataBlocks := 8
testCases := []struct {
enBlocks [][]byte // data and parity blocks.
successData bool // expected return value of isSuccessDataBlocks()
successDecode bool // expected return value of isSuccessDecodeBlocks()
}{
{
// When all data and partity blocks are available.
[][]byte{
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
},
true,
true,
},
{
// When one data block is not available.
[][]byte{
nil, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
},
false,
true,
},
{
// When one data and all parity are available, enough for reedsolomon.Reconstruct()
[][]byte{
nil, nil, nil, nil, nil, nil, nil, {'a'},
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
},
false,
true,
},
{
// When all data disks are not available, enough for reedsolomon.Reconstruct()
[][]byte{
nil, nil, nil, nil, nil, nil, nil, nil,
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
},
false,
true,
},
{
// Not enough disks for reedsolomon.Reconstruct()
[][]byte{
nil, nil, nil, nil, nil, nil, nil, nil,
nil, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
},
false,
false,
},
}
for i, test := range testCases {
got := isSuccessDataBlocks(test.enBlocks, dataBlocks)
if test.successData != got {
t.Errorf("test-case %d : expected %v got %v", i+1, test.successData, got)
}
got = isSuccessDecodeBlocks(test.enBlocks, dataBlocks)
if test.successDecode != got {
t.Errorf("test-case %d : expected %v got %v", i+1, test.successDecode, got)
}
}
}
// Wrapper function for testGetReadDisks, testGetOrderedDisks.
func TestErasureReadUtils(t *testing.T) {
objLayer, dirs, err := getXLObjectLayer()
if err != nil {
t.Fatal(err)
}
defer removeRoots(dirs)
xl := objLayer.(xlObjects)
testGetReadDisks(t, xl)
testGetOrderedDisks(t, xl)
}
// Simulates a faulty disk for ReadFile()
type ReadDiskDown struct {
*posix
}
func (r ReadDiskDown) ReadFile(volume string, path string, offset int64, buf []byte) (n int64, err error) {
return 0, errFaultyDisk
}
func TestErasureReadFileDiskFail(t *testing.T) {
// Initialize environment needed for the test.
dataBlocks := 7
parityBlocks := 7
blockSize := int64(blockSizeV1)
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
if err != nil {
t.Error(err)
return
}
defer setup.Remove()
disks := setup.disks
// Prepare a slice of 1MB with random data.
data := make([]byte, 1*1024*1024)
length := int64(len(data))
_, err = rand.Read(data)
if err != nil {
t.Fatal(err)
}
// Create a test file to read from.
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
if err != nil {
t.Fatal(err)
}
if size != length {
t.Errorf("erasureCreateFile returned %d, expected %d", size, length)
}
// create byte pool which will be used by erasureReadFile for
// reading from disks and erasure decoding.
chunkSize := getChunkSize(blockSize, dataBlocks)
pool := bpool.NewBytePool(chunkSize, len(disks))
buf := &bytes.Buffer{}
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
if err != nil {
t.Error(err)
}
if !bytes.Equal(buf.Bytes(), data) {
t.Error("Contents of the erasure coded file differs")
}
// 2 disks down. Read should succeed.
disks[4] = ReadDiskDown{disks[4].(*posix)}
disks[5] = ReadDiskDown{disks[5].(*posix)}
buf.Reset()
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
if err != nil {
t.Error(err)
}
if !bytes.Equal(buf.Bytes(), data) {
t.Error("Contents of the erasure coded file differs")
}
// 4 more disks down. 6 disks down in total. Read should succeed.
disks[6] = ReadDiskDown{disks[6].(*posix)}
disks[8] = ReadDiskDown{disks[8].(*posix)}
disks[9] = ReadDiskDown{disks[9].(*posix)}
disks[11] = ReadDiskDown{disks[11].(*posix)}
buf.Reset()
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
if err != nil {
t.Error(err)
}
if !bytes.Equal(buf.Bytes(), data) {
t.Error("Contents of the erasure coded file differs")
}
// 2 more disk down. 8 disks down in total. Read should fail.
disks[12] = ReadDiskDown{disks[12].(*posix)}
disks[13] = ReadDiskDown{disks[13].(*posix)}
buf.Reset()
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
if err != errXLReadQuorum {
t.Fatal("expected errXLReadQuorum error")
}
}
func TestErasureReadFileOffsetLength(t *testing.T) {
// Initialize environment needed for the test.
dataBlocks := 7
parityBlocks := 7
blockSize := int64(1 * 1024 * 1024)
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
if err != nil {
t.Error(err)
return
}
defer setup.Remove()
disks := setup.disks
// Prepare a slice of 5MB with random data.
data := make([]byte, 5*1024*1024)
length := int64(len(data))
_, err = rand.Read(data)
if err != nil {
t.Fatal(err)
}
// Create a test file to read from.
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
if err != nil {
t.Fatal(err)
}
if size != length {
t.Errorf("erasureCreateFile returned %d, expected %d", size, length)
}
testCases := []struct {
offset, length int64
}{
// Full file.
{0, length},
// Read nothing.
{length, 0},
// 2nd block.
{blockSize, blockSize},
// Test cases for random offsets and lengths.
{blockSize - 1, 2},
{blockSize - 1, blockSize + 1},
{blockSize + 1, blockSize - 1},
{blockSize + 1, blockSize},
{blockSize + 1, blockSize + 1},
{blockSize*2 - 1, blockSize + 1},
{length - 1, 1},
{length - blockSize, blockSize},
{length - blockSize - 1, blockSize},
{length - blockSize - 1, blockSize + 1},
}
chunkSize := getChunkSize(blockSize, dataBlocks)
pool := bpool.NewBytePool(chunkSize, len(disks))
// Compare the data read from file with "data" byte array.
for i, testCase := range testCases {
expected := data[testCase.offset:(testCase.offset + testCase.length)]
buf := &bytes.Buffer{}
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", testCase.offset, testCase.length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
if err != nil {
t.Error(err)
continue
}
got := buf.Bytes()
if !bytes.Equal(expected, got) {
t.Errorf("Test %d : read data is different from what was expected", i+1)
}
}
}
// Test erasureReadFile with random offset and lengths.
// This test is t.Skip()ed as it a long time to run, hence should be run
// explicitly after commenting out t.Skip()
func TestErasureReadFileRandomOffsetLength(t *testing.T) {
// Comment the following line to run this test.
t.SkipNow()
// Initialize environment needed for the test.
dataBlocks := 7
parityBlocks := 7
blockSize := int64(1 * 1024 * 1024)
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
if err != nil {
t.Error(err)
return
}
defer setup.Remove()
disks := setup.disks
// Prepare a slice of 5MB with random data.
data := make([]byte, 5*1024*1024)
length := int64(len(data))
_, err = rand.Read(data)
if err != nil {
t.Fatal(err)
}
// 10000 iterations with random offsets and lengths.
iterations := 10000
// Create a test file to read from.
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
if err != nil {
t.Fatal(err)
}
if size != length {
t.Errorf("erasureCreateFile returned %d, expected %d", size, length)
}
// To generate random offset/length.
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// create pool buffer which will be used by erasureReadFile for
// reading from disks and erasure decoding.
chunkSize := getChunkSize(blockSize, dataBlocks)
pool := bpool.NewBytePool(chunkSize, len(disks))
buf := &bytes.Buffer{}
// Verify erasureReadFile() for random offsets and lengths.
for i := 0; i < iterations; i++ {
offset := r.Int63n(length)
readLen := r.Int63n(length - offset)
expected := data[offset : offset+readLen]
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", offset, readLen, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
if err != nil {
t.Fatal(err, offset, readLen)
}
got := buf.Bytes()
if !bytes.Equal(expected, got) {
t.Fatalf("read data is different from what was expected, offset=%d length=%d", offset, readLen)
}
buf.Reset()
}
}
+186
View File
@@ -0,0 +1,186 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"errors"
"hash"
"io"
"github.com/klauspost/reedsolomon"
"github.com/minio/blake2b-simd"
)
// newHashWriters - inititialize a slice of hashes for the disk count.
func newHashWriters(diskCount int, algo string) []hash.Hash {
hashWriters := make([]hash.Hash, diskCount)
for index := range hashWriters {
hashWriters[index] = newHash(algo)
}
return hashWriters
}
// newHash - gives you a newly allocated hash depending on the input algorithm.
func newHash(algo string) hash.Hash {
switch algo {
case "blake2b":
return blake2b.New512()
// Add new hashes here.
default:
// Default to blake2b.
return blake2b.New512()
}
}
// hashSum calculates the hash of the entire path and returns.
func hashSum(disk StorageAPI, volume, path string, writer hash.Hash) ([]byte, error) {
// Allocate staging buffer of 128KiB for copyBuffer.
buf := make([]byte, readSizeV1)
// Copy entire buffer to writer.
if err := copyBuffer(writer, disk, volume, path, buf); err != nil {
return nil, err
}
// Return the final hash sum.
return writer.Sum(nil), nil
}
// getDataBlockLen - get length of data blocks from encoded blocks.
func getDataBlockLen(enBlocks [][]byte, dataBlocks int) int {
size := 0
// Figure out the data block length.
for _, block := range enBlocks[:dataBlocks] {
size += len(block)
}
return size
}
// Writes all the data blocks from encoded blocks until requested
// outSize length. Provides a way to skip bytes until the offset.
func writeDataBlocks(dst io.Writer, enBlocks [][]byte, dataBlocks int, offset int64, length int64) (int64, error) {
// Offset and out size cannot be negative.
if offset < 0 || length < 0 {
return 0, errUnexpected
}
// Do we have enough blocks?
if len(enBlocks) < dataBlocks {
return 0, reedsolomon.ErrTooFewShards
}
// Do we have enough data?
if int64(getDataBlockLen(enBlocks, dataBlocks)) < length {
return 0, reedsolomon.ErrShortData
}
// Counter to decrement total left to write.
write := length
// Counter to increment total written.
totalWritten := int64(0)
// Write all data blocks to dst.
for _, block := range enBlocks[:dataBlocks] {
// Skip blocks until we have reached our offset.
if offset >= int64(len(block)) {
// Decrement offset.
offset -= int64(len(block))
continue
} else {
// Skip until offset.
block = block[offset:]
// Reset the offset for next iteration to read everything
// from subsequent blocks.
offset = 0
}
// We have written all the blocks, write the last remaining block.
if write < int64(len(block)) {
n, err := io.Copy(dst, bytes.NewReader(block[:write]))
if err != nil {
return 0, err
}
totalWritten += n
break
}
// Copy the block.
n, err := io.Copy(dst, bytes.NewReader(block))
if err != nil {
return 0, err
}
// Decrement output size.
write -= n
// Increment written.
totalWritten += n
}
// Success.
return totalWritten, nil
}
// chunkSize is roughly BlockSize/DataBlocks.
// chunkSize is calculated such that chunkSize*DataBlocks accommodates BlockSize bytes.
// So chunkSize*DataBlocks can be slightly larger than BlockSize if BlockSize is not divisible by
// DataBlocks. The extra space will have 0-padding.
func getChunkSize(blockSize int64, dataBlocks int) int64 {
return (blockSize + int64(dataBlocks) - 1) / int64(dataBlocks)
}
// copyBuffer - copies from disk, volume, path to input writer until either EOF
// is reached at volume, path or an error occurs. A success copyBuffer returns
// err == nil, not err == EOF. Because copyBuffer is defined to read from path
// until EOF. It does not treat an EOF from ReadFile an error to be reported.
// Additionally copyBuffer stages through the provided buffer; otherwise if it
// has zero length, returns error.
func copyBuffer(writer io.Writer, disk StorageAPI, volume string, path string, buf []byte) error {
// Error condition of zero length buffer.
if buf != nil && len(buf) == 0 {
return errors.New("empty buffer in readBuffer")
}
// Starting offset for Reading the file.
startOffset := int64(0)
// Read until io.EOF.
for {
n, err := disk.ReadFile(volume, path, startOffset, buf)
if n > 0 {
m, wErr := writer.Write(buf[:n])
if wErr != nil {
return wErr
}
if int64(m) != n {
return io.ErrShortWrite
}
}
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
break
}
return err
}
// Progress the offset.
startOffset += n
}
// Success.
return nil
}
+160
View File
@@ -0,0 +1,160 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"errors"
"io"
"testing"
)
// Test validates the number hash writers returned.
func TestNewHashWriters(t *testing.T) {
diskNum := 8
hashWriters := newHashWriters(diskNum, bitRotAlgo)
if len(hashWriters) != diskNum {
t.Errorf("Expected %d hashWriters, but instead got %d", diskNum, len(hashWriters))
}
}
// Tests validate the output of getChunkSize.
func TestGetChunkSize(t *testing.T) {
// Refer to comments on getChunkSize() for details.
testCases := []struct {
blockSize int64
dataBlocks int
// expected result.
expectedChunkSize int64
}{
{
10,
10,
1,
},
{
10,
11,
1,
},
{
10,
9,
2,
},
}
// Verify getChunkSize() for the test cases.
for i, testCase := range testCases {
got := getChunkSize(testCase.blockSize, testCase.dataBlocks)
if testCase.expectedChunkSize != got {
t.Errorf("Test %d : expected=%d got=%d", i+1, testCase.expectedChunkSize, got)
}
}
}
// TestCopyBuffer - Tests validate the result and errors produced when `copyBuffer` is called with sample inputs.
func TestCopyBuffer(t *testing.T) {
// create posix test setup
disk, diskPath, err := newPosixTestSetup()
if err != nil {
t.Fatalf("Unable to create posix test setup, %s", err)
}
defer removeAll(diskPath)
volume := "success-vol"
// Setup test environment.
if err = disk.MakeVol(volume); err != nil {
t.Fatalf("Unable to create volume, %s", err)
}
// creating io.Writer for the input to copyBuffer.
buffers := []*bytes.Buffer{
new(bytes.Buffer),
new(bytes.Buffer),
new(bytes.Buffer),
}
testFile := "testFile"
testContent := []byte("hello, world")
err = disk.AppendFile(volume, testFile, testContent)
if err != nil {
t.Fatalf("AppendFile failed: <ERROR> %s", err)
}
testCases := []struct {
writer io.Writer
disk StorageAPI
volume string
path string
buf []byte
expectedResult []byte
// flag to indicate whether test case should pass.
shouldPass bool
expectedErr error
}{
// Test case - 1.
// case with empty buffer.
{nil, nil, "", "", []byte{}, nil, false, errors.New("empty buffer in readBuffer")},
// Test case - 2.
// Test case with empty volume.
{buffers[0], disk, "", "", make([]byte, 5), nil, false, errInvalidArgument},
// Test case - 3.
// Test case with non existent volume.
{buffers[0], disk, "abc", "", make([]byte, 5), nil, false, errVolumeNotFound},
// Test case - 4.
// Test case with empty filename/path.
{buffers[0], disk, volume, "", make([]byte, 5), nil, false, errIsNotRegular},
// Test case - 5.
// Test case with non existent file name.
{buffers[0], disk, volume, "abcd", make([]byte, 5), nil, false, errFileNotFound},
// Test case - 6.
// Test case where the writer returns EOF.
{NewEOFWriter(buffers[0], 3), disk, volume, testFile, make([]byte, 5), nil, false, io.EOF},
// Test case - 7.
// Test case to produce io.ErrShortWrite, the TruncateWriter returns after writing 3 bytes.
{TruncateWriter(buffers[1], 3), disk, volume, testFile, make([]byte, 5), nil, false, io.ErrShortWrite},
// Teset case - 8.
// Valid case, expected to read till EOF and write the contents into the writer.
{buffers[2], disk, volume, testFile, make([]byte, 5), testContent, true, nil},
}
// iterate over the test cases and call copy Buffer with data.
for i, testCase := range testCases {
actualErr := copyBuffer(testCase.writer, testCase.disk, testCase.volume, testCase.path, testCase.buf)
if actualErr != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass but failed instead with \"%s\"", i+1, actualErr)
}
if actualErr == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with error <Error> \"%v\", but instead passed", i+1, testCase.expectedErr)
}
// Failed as expected, but does it fail for the expected reason.
if actualErr != nil && !testCase.shouldPass {
if testCase.expectedErr.Error() != actualErr.Error() {
t.Errorf("Test %d: Expected Error to be \"%v\", but instead found \"%v\" ", i+1, testCase.expectedErr, actualErr)
}
}
// test passed as expected, asserting the result.
if actualErr == nil && testCase.shouldPass {
if !bytes.Equal(testCase.expectedResult, buffers[2].Bytes()) {
t.Errorf("Test %d: copied buffer differs from the expected one.", i+1)
}
}
}
}
+184
View File
@@ -0,0 +1,184 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"testing"
)
// mustEncodeData - encodes data slice and provides encoded 2 dimensional slice.
func mustEncodeData(data []byte, dataBlocks, parityBlocks int) [][]byte {
encodedData, err := encodeData(data, dataBlocks, parityBlocks)
if err != nil {
// Upon failure panic this function.
panic(err)
}
return encodedData
}
// Generates good encoded data with one parity block and data block missing.
func getGoodEncodedData(data []byte, dataBlocks, parityBlocks int) [][]byte {
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
encodedData[3] = nil
encodedData[1] = nil
return encodedData
}
// Generates bad encoded data with one parity block and data block with garbage data.
func getBadEncodedData(data []byte, dataBlocks, parityBlocks int) [][]byte {
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
encodedData[3] = []byte("garbage")
encodedData[1] = []byte("garbage")
return encodedData
}
// Generates encoded data with all data blocks missing.
func getMissingData(data []byte, dataBlocks, parityBlocks int) [][]byte {
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
for i := 0; i < dataBlocks+1; i++ {
encodedData[i] = nil
}
return encodedData
}
// Generates encoded data with less number of blocks than expected data blocks.
func getInsufficientData(data []byte, dataBlocks, parityBlocks int) [][]byte {
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
// Return half of the data.
return encodedData[:dataBlocks/2]
}
// Represents erasure encoding matrix dataBlocks and paritBlocks.
type encodingMatrix struct {
dataBlocks int
parityBlocks int
}
// List of encoding matrices the tests will run on.
var encodingMatrices = []encodingMatrix{
{3, 3}, // 3 data, 3 parity blocks.
{4, 4}, // 4 data, 4 parity blocks.
{5, 5}, // 5 data, 5 parity blocks.
{6, 6}, // 6 data, 6 parity blocks.
{7, 7}, // 7 data, 7 parity blocks.
{8, 8}, // 8 data, 8 parity blocks.
}
// Tests erasure decoding functionality for various types of inputs.
func TestErasureDecode(t *testing.T) {
data := []byte("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.")
// List of decoding cases
// - validates bad encoded data.
// - validates good encoded data.
// - validates insufficient data.
testDecodeCases := []struct {
enFn func([]byte, int, int) [][]byte
shouldPass bool
}{
// Generates bad encoded data.
{
enFn: getBadEncodedData,
shouldPass: false,
},
// Generates good encoded data.
{
enFn: getGoodEncodedData,
shouldPass: true,
},
// Generates missing data.
{
enFn: getMissingData,
shouldPass: false,
},
// Generates short data.
{
enFn: getInsufficientData,
shouldPass: false,
},
}
// Validates all decode tests.
for i, testCase := range testDecodeCases {
for _, encodingMatrix := range encodingMatrices {
// Encoding matrix.
dataBlocks := encodingMatrix.dataBlocks
parityBlocks := encodingMatrix.parityBlocks
// Data block size.
blockSize := len(data)
// Generates encoded data based on type of testCase function.
encodedData := testCase.enFn(data, dataBlocks, parityBlocks)
// Decodes the data.
err := decodeData(encodedData, dataBlocks, parityBlocks)
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass by failed instead with %s", i+1, err)
}
// Proceed to extract the data blocks.
decodedDataWriter := new(bytes.Buffer)
_, err = writeDataBlocks(decodedDataWriter, encodedData, dataBlocks, 0, int64(blockSize))
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass by failed instead with %s", i+1, err)
}
// Validate if decoded data is what we expected.
if bytes.Equal(decodedDataWriter.Bytes(), data) != testCase.shouldPass {
err := errUnexpected
t.Errorf("Test %d: Expected to pass by failed instead %s", i+1, err)
}
}
}
}
// Setup for erasureCreateFile and erasureReadFile tests.
type erasureTestSetup struct {
dataBlocks int
parityBlocks int
blockSize int64
diskPaths []string
disks []StorageAPI
}
// Removes the temporary disk directories.
func (e erasureTestSetup) Remove() {
for _, path := range e.diskPaths {
removeAll(path)
}
}
// Returns an initialized setup for erasure tests.
func newErasureTestSetup(dataBlocks int, parityBlocks int, blockSize int64) (*erasureTestSetup, error) {
diskPaths := make([]string, dataBlocks+parityBlocks)
disks := make([]StorageAPI, len(diskPaths))
var err error
for i := range diskPaths {
disks[i], diskPaths[i], err = newPosixTestSetup()
if err != nil {
return nil, err
}
err = disks[i].MakeVol("testbucket")
if err != nil {
return nil, err
}
}
return &erasureTestSetup{dataBlocks, parityBlocks, blockSize, diskPaths, disks}, nil
}
+384
View File
@@ -0,0 +1,384 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"net/url"
"path"
"sync"
"time"
"github.com/Sirupsen/logrus"
)
// Global event notification queue. This is the queue that would be used to send all notifications.
type eventNotifier struct {
rwMutex *sync.RWMutex
// Collection of 'bucket' and notification config.
notificationConfigs map[string]*notificationConfig
snsTargets map[string][]chan []NotificationEvent
queueTargets map[string]*logrus.Logger
}
// Represents data to be sent with notification event.
type eventData struct {
Type EventName
Bucket string
ObjInfo ObjectInfo
ReqParams map[string]string
}
// New notification event constructs a new notification event message from
// input request metadata which completed successfully.
func newNotificationEvent(event eventData) NotificationEvent {
/// Construct a new object created event.
region := serverConfig.GetRegion()
tnow := time.Now().UTC()
sequencer := fmt.Sprintf("%X", tnow.UnixNano())
// Following blocks fills in all the necessary details of s3 event message structure.
// http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html
nEvent := NotificationEvent{
EventVersion: "2.0",
EventSource: "aws:s3",
AwsRegion: region,
EventTime: tnow.Format(timeFormatAMZ),
EventName: event.Type.String(),
UserIdentity: defaultIdentity(),
RequestParameters: event.ReqParams,
ResponseElements: map[string]string{},
S3: eventMeta{
SchemaVersion: "1.0",
ConfigurationID: "Config",
Bucket: bucketMeta{
Name: event.Bucket,
OwnerIdentity: defaultIdentity(),
ARN: "arn:aws:s3:::" + event.Bucket,
},
},
}
escapedObj := url.QueryEscape(event.ObjInfo.Name)
// For delete object event type, we do not need to set ETag and Size.
if event.Type == ObjectRemovedDelete {
nEvent.S3.Object = objectMeta{
Key: escapedObj,
Sequencer: sequencer,
}
return nEvent
}
// For all other events we should set ETag and Size.
nEvent.S3.Object = objectMeta{
Key: escapedObj,
ETag: event.ObjInfo.MD5Sum,
Size: event.ObjInfo.Size,
Sequencer: sequencer,
}
// Success.
return nEvent
}
// Fetch the saved queue target.
func (en eventNotifier) GetQueueTarget(queueARN string) *logrus.Logger {
return en.queueTargets[queueARN]
}
func (en eventNotifier) GetSNSTarget(snsARN string) []chan []NotificationEvent {
en.rwMutex.RLock()
defer en.rwMutex.RUnlock()
return en.snsTargets[snsARN]
}
// Set a new sns target for an input sns ARN.
func (en *eventNotifier) SetSNSTarget(snsARN string, listenerCh chan []NotificationEvent) error {
en.rwMutex.Lock()
defer en.rwMutex.Unlock()
if listenerCh == nil {
return errors.New("invalid argument")
}
en.snsTargets[snsARN] = append(en.snsTargets[snsARN], listenerCh)
return nil
}
// Remove sns target for an input sns ARN.
func (en *eventNotifier) RemoveSNSTarget(snsARN string, listenerCh chan []NotificationEvent) {
en.rwMutex.Lock()
defer en.rwMutex.Unlock()
snsTarget, ok := en.snsTargets[snsARN]
if ok {
for i, savedListenerCh := range snsTarget {
if listenerCh == savedListenerCh {
snsTarget = append(snsTarget[:i], snsTarget[i+1:]...)
if len(snsTarget) == 0 {
delete(en.snsTargets, snsARN)
break
}
en.snsTargets[snsARN] = snsTarget
}
}
}
}
// Returns true if bucket notification is set for the bucket, false otherwise.
func (en *eventNotifier) IsBucketNotificationSet(bucket string) bool {
if en == nil {
return false
}
en.rwMutex.RLock()
defer en.rwMutex.RUnlock()
_, ok := en.notificationConfigs[bucket]
return ok
}
// Fetch bucket notification config for an input bucket.
func (en eventNotifier) GetBucketNotificationConfig(bucket string) *notificationConfig {
en.rwMutex.RLock()
defer en.rwMutex.RUnlock()
return en.notificationConfigs[bucket]
}
// Set a new notification config for a bucket, this operation will overwrite any previous
// notification configs for the bucket.
func (en *eventNotifier) SetBucketNotificationConfig(bucket string, notificationCfg *notificationConfig) error {
en.rwMutex.Lock()
defer en.rwMutex.Unlock()
if notificationCfg == nil {
return errors.New("invalid argument")
}
en.notificationConfigs[bucket] = notificationCfg
return nil
}
// eventNotify notifies an event to relevant targets based on their
// bucket notification configs.
func eventNotify(event eventData) {
// Notifies a new event.
// List of events reported through this function are
// - s3:ObjectCreated:Put
// - s3:ObjectCreated:Post
// - s3:ObjectCreated:Copy
// - s3:ObjectCreated:CompleteMultipartUpload
// - s3:ObjectRemoved:Delete
nConfig := eventN.GetBucketNotificationConfig(event.Bucket)
// No bucket notifications enabled, drop the event notification.
if len(nConfig.QueueConfigs) == 0 && len(nConfig.TopicConfigs) == 0 && len(nConfig.LambdaConfigs) == 0 {
return
}
// Event type.
eventType := event.Type.String()
// Object name.
objectName := event.ObjInfo.Name
// Save the notification event to be sent.
notificationEvent := []NotificationEvent{newNotificationEvent(event)}
// Validate if the event and object match the queue configs.
for _, qConfig := range nConfig.QueueConfigs {
eventMatch := eventMatch(eventType, qConfig.Events)
ruleMatch := filterRuleMatch(objectName, qConfig.Filter.Key.FilterRules)
if eventMatch && ruleMatch {
targetLog := eventN.GetQueueTarget(qConfig.QueueARN)
if targetLog != nil {
targetLog.WithFields(logrus.Fields{
"Records": notificationEvent,
}).Info()
}
}
}
// Validate if the event and object match the sns configs.
for _, topicConfig := range nConfig.TopicConfigs {
ruleMatch := filterRuleMatch(objectName, topicConfig.Filter.Key.FilterRules)
eventMatch := eventMatch(eventType, topicConfig.Events)
if eventMatch && ruleMatch {
targetListeners := eventN.GetSNSTarget(topicConfig.TopicARN)
for _, listener := range targetListeners {
listener <- notificationEvent
}
}
}
}
// loads notifcation config if any for a given bucket, returns back structured notification config.
func loadNotificationConfig(bucket string, objAPI ObjectLayer) (*notificationConfig, error) {
// Construct the notification config path.
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
objInfo, err := objAPI.GetObjectInfo(minioMetaBucket, notificationConfigPath)
if err != nil {
// 'notification.xml' not found return 'errNoSuchNotifications'.
// This is default when no bucket notifications are found on the bucket.
switch err.(type) {
case ObjectNotFound:
return nil, errNoSuchNotifications
}
// Returns error for other errors.
return nil, err
}
var buffer bytes.Buffer
err = objAPI.GetObject(minioMetaBucket, notificationConfigPath, 0, objInfo.Size, &buffer)
if err != nil {
// 'notification.xml' not found return 'errNoSuchNotifications'.
// This is default when no bucket notifications are found on the bucket.
switch err.(type) {
case ObjectNotFound:
return nil, errNoSuchNotifications
}
// Returns error for other errors.
return nil, err
}
// Unmarshal notification bytes.
notificationConfigBytes := buffer.Bytes()
notificationCfg := &notificationConfig{}
if err = xml.Unmarshal(notificationConfigBytes, &notificationCfg); err != nil {
return nil, err
} // Successfully marshalled notification configuration.
// Return success.
return notificationCfg, nil
}
// loads all bucket notifications if present.
func loadAllBucketNotifications(objAPI ObjectLayer) (map[string]*notificationConfig, error) {
// List buckets to proceed loading all notification configuration.
buckets, err := objAPI.ListBuckets()
if err != nil {
return nil, err
}
configs := make(map[string]*notificationConfig)
// Loads all bucket notifications.
for _, bucket := range buckets {
var nCfg *notificationConfig
nCfg, err = loadNotificationConfig(bucket.Name, objAPI)
if err != nil {
if err == errNoSuchNotifications {
continue
}
return nil, err
}
configs[bucket.Name] = nCfg
}
// Success.
return configs, nil
}
// Loads all queue targets, initializes each queueARNs depending on their config.
// Each instance of queueARN registers its own logrus to communicate with the
// queue service. QueueARN once initialized is not initialized again for the
// same queueARN, instead previous connection is used.
func loadAllQueueTargets() (map[string]*logrus.Logger, error) {
queueTargets := make(map[string]*logrus.Logger)
// Load all amqp targets, initialize their respective loggers.
for accountID, amqpN := range serverConfig.GetAMQP() {
if !amqpN.Enable {
continue
}
// Construct the queue ARN for AMQP.
queueARN := minioSqs + serverConfig.GetRegion() + ":" + accountID + ":" + queueTypeAMQP
// Queue target if already initialized we move to the next ARN.
_, ok := queueTargets[queueARN]
if ok {
continue
}
// Using accountID we can now initialize a new AMQP logrus instance.
amqpLog, err := newAMQPNotify(accountID)
if err != nil {
return nil, err
}
queueTargets[queueARN] = amqpLog
}
// Load redis targets, initialize their respective loggers.
for accountID, redisN := range serverConfig.GetRedis() {
if !redisN.Enable {
continue
}
// Construct the queue ARN for Redis.
queueARN := minioSqs + serverConfig.GetRegion() + ":" + accountID + ":" + queueTypeRedis
// Queue target if already initialized we move to the next ARN.
_, ok := queueTargets[queueARN]
if ok {
continue
}
// Using accountID we can now initialize a new Redis logrus instance.
redisLog, err := newRedisNotify(accountID)
if err != nil {
return nil, err
}
queueTargets[queueARN] = redisLog
}
// Load elastic targets, initialize their respective loggers.
for accountID, elasticN := range serverConfig.GetElasticSearch() {
if !elasticN.Enable {
continue
}
// Construct the queue ARN for Elastic.
queueARN := minioSqs + serverConfig.GetRegion() + ":" + accountID + ":" + queueTypeElastic
_, ok := queueTargets[queueARN]
if ok {
continue
}
// Using accountID we can now initialize a new ElasticSearch logrus instance.
elasticLog, err := newElasticNotify(accountID)
if err != nil {
return nil, err
}
queueTargets[queueARN] = elasticLog
}
// Successfully initialized queue targets.
return queueTargets, nil
}
// Global instance of event notification queue.
var eventN *eventNotifier
// Initialize event notifier.
func initEventNotifier(objAPI ObjectLayer) error {
if objAPI == nil {
return errInvalidArgument
}
// Read all saved bucket notifications.
configs, err := loadAllBucketNotifications(objAPI)
if err != nil {
return err
}
// Initializes all queue targets.
queueTargets, err := loadAllQueueTargets()
if err != nil {
return err
}
// Inititalize event notifier queue.
eventN = &eventNotifier{
rwMutex: &sync.RWMutex{},
notificationConfigs: configs,
queueTargets: queueTargets,
snsTargets: make(map[string][]chan []NotificationEvent),
}
return nil
}
+67
View File
@@ -0,0 +1,67 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import "testing"
// Tests various forms of inititalization of event notifier.
func TestInitEventNotifier(t *testing.T) {
fs, disk, err := getSingleNodeObjectLayer()
if err != nil {
t.Fatal("Unable to initialize FS backend.", err)
}
xl, disks, err := getXLObjectLayer()
if err != nil {
t.Fatal("Unable to initialize XL backend.", err)
}
disks = append(disks, disk)
for _, d := range disks {
defer removeAll(d)
}
// Collection of test cases for inititalizing event notifier.
testCases := []struct {
objAPI ObjectLayer
configs map[string]*notificationConfig
err error
}{
// Test 1 - invalid arguments.
{
objAPI: nil,
err: errInvalidArgument,
},
// Test 2 - valid FS object layer but no bucket notifications.
{
objAPI: fs,
err: nil,
},
// Test 3 - valid XL object layer but no bucket notifications.
{
objAPI: xl,
err: nil,
},
}
// Validate if event notifier is properly initialized.
for i, testCase := range testCases {
err = initEventNotifier(testCase.objAPI)
if err != testCase.err {
t.Errorf("Test %d: Expected %s, but got: %s", i+1, testCase.err, err)
}
}
}
+115
View File
@@ -0,0 +1,115 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
// Simulates disk returning errFaultyDisk on all methods of StorageAPI
// interface after successCount number of successes.
type faultyDisk struct {
disk *posix
successCount int
}
// instantiates a faulty
func newFaultyDisk(disk *posix, n int) *faultyDisk {
return &faultyDisk{disk: disk, successCount: n}
}
func (f *faultyDisk) MakeVol(volume string) (err error) {
if f.successCount > 0 {
f.successCount--
return f.disk.MakeVol(volume)
}
return errFaultyDisk
}
func (f *faultyDisk) ListVols() (vols []VolInfo, err error) {
if f.successCount > 0 {
f.successCount--
return f.disk.ListVols()
}
return nil, errFaultyDisk
}
func (f *faultyDisk) StatVol(volume string) (volInfo VolInfo, err error) {
if f.successCount > 0 {
f.successCount--
return f.disk.StatVol(volume)
}
return VolInfo{}, errFaultyDisk
}
func (f *faultyDisk) DeleteVol(volume string) (err error) {
if f.successCount > 0 {
f.successCount--
return f.disk.DeleteVol(volume)
}
return errFaultyDisk
}
func (f *faultyDisk) ListDir(volume, path string) (entries []string, err error) {
if f.successCount > 0 {
f.successCount--
return f.disk.ListDir(volume, path)
}
return []string{}, errFaultyDisk
}
func (f *faultyDisk) ReadFile(volume string, path string, offset int64, buf []byte) (n int64, err error) {
if f.successCount > 0 {
f.successCount--
return f.disk.ReadFile(volume, path, offset, buf)
}
return 0, errFaultyDisk
}
func (f *faultyDisk) AppendFile(volume, path string, buf []byte) error {
if f.successCount > 0 {
f.successCount--
return f.disk.AppendFile(volume, path, buf)
}
return errFaultyDisk
}
func (f *faultyDisk) RenameFile(srcVolume, srcPath, dstVolume, dstPath string) error {
if f.successCount > 0 {
f.successCount--
return f.disk.RenameFile(srcVolume, srcPath, dstVolume, dstPath)
}
return errFaultyDisk
}
func (f *faultyDisk) StatFile(volume string, path string) (file FileInfo, err error) {
if f.successCount > 0 {
f.successCount--
return f.disk.StatFile(volume, path)
}
return FileInfo{}, errFaultyDisk
}
func (f *faultyDisk) DeleteFile(volume string, path string) (err error) {
if f.successCount > 0 {
f.successCount--
return f.disk.DeleteFile(volume, path)
}
return errFaultyDisk
}
func (f *faultyDisk) ReadAll(volume string, path string) (buf []byte, err error) {
if f.successCount > 0 {
f.successCount--
return f.disk.ReadAll(volume, path)
}
return nil, errFaultyDisk
}
+875
View File
@@ -0,0 +1,875 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"sync"
)
// fsFormat - structure holding 'fs' format.
type fsFormat struct {
Version string `json:"version"`
}
// xlFormat - structure holding 'xl' format.
type xlFormat struct {
Version string `json:"version"` // Version of 'xl' format.
Disk string `json:"disk"` // Disk field carries assigned disk uuid.
// JBOD field carries the input disk order generated the first
// time when fresh disks were supplied.
JBOD []string `json:"jbod"`
}
// formatConfigV1 - structure holds format config version '1'.
type formatConfigV1 struct {
Version string `json:"version"` // Version of the format config.
// Format indicates the backend format type, supports two values 'xl' and 'fs'.
Format string `json:"format"`
FS *fsFormat `json:"fs,omitempty"` // FS field holds fs format.
XL *xlFormat `json:"xl,omitempty"` // XL field holds xl format.
}
/*
All disks online
-----------------
- All Unformatted - format all and return success.
- Some Unformatted - format all and return success.
- Any JBOD inconsistent - return failure // Requires deep inspection, phase2.
- Some are corrupt (missing format.json) - return failure // Requires deep inspection, phase2.
- Any unrecognized disks - return failure
Some disks are offline and we have quorum.
-----------------
- Some unformatted - no heal, return success.
- Any JBOD inconsistent - return failure // Requires deep inspection, phase2.
- Some are corrupt (missing format.json) - return failure // Requires deep inspection, phase2.
- Any unrecognized disks - return failure
No read quorum
-----------------
failure for all cases.
// Pseudo code for managing `format.json`.
// Generic checks.
if (no quorum) return error
if (any disk is corrupt) return error // phase2
if (jbod inconsistent) return error // phase2
if (disks not recognized) // Always error.
// Specific checks.
if (all disks online)
if (all disks return format.json)
if (jbod consistent)
if (all disks recognized)
return
else
if (all disks return format.json not found)
(initialize format)
return
else (some disks return format.json not found)
(heal format)
return
fi
fi
else // No healing at this point forward, some disks are offline or dead.
if (some disks return format.json not found)
if (with force)
// Offline disks are marked as dead.
(heal format) // Offline disks should be marked as dead.
return success
else (without force)
// --force is necessary to heal few drives, because some drives
// are offline. Offline disks will be marked as dead.
return error
fi
fi
*/
// error returned when some disks are found to be unformatted.
var errSomeDiskUnformatted = errors.New("some disks are found to be unformatted")
// error returned when some disks are offline.
var errSomeDiskOffline = errors.New("some disks are offline")
// errDiskOrderMismatch - returned when disk UUID is not in consistent JBOD order.
var errDiskOrderMismatch = errors.New("disk order mismatch")
// Returns error slice into understandable errors.
func reduceFormatErrs(errs []error, diskCount int) (err error) {
var errUnformattedDiskCount = 0
var errDiskNotFoundCount = 0
var errCorruptedFormatCount = 0
for _, dErr := range errs {
if dErr == errUnformattedDisk {
errUnformattedDiskCount++
} else if dErr == errDiskNotFound {
errDiskNotFoundCount++
} else if dErr == errCorruptedFormat {
errCorruptedFormatCount++
}
}
if errCorruptedFormatCount > 0 {
return errCorruptedFormat
}
// Unformatted disks found, we need to figure out if any disks are offline.
if errUnformattedDiskCount > 0 {
// Returns errUnformattedDisk if all disks report unFormattedDisk.
if errUnformattedDiskCount < diskCount {
if errDiskNotFoundCount > 0 {
// Only some disks are fresh but some disks are offline as well.
return errSomeDiskOffline
}
// Some disks are fresh disks an unformatted, not disks are offline.
return errSomeDiskUnformatted
}
// All disks returned unformatted, all disks must be fresh.
return errUnformattedDisk
}
// No unformatted disks found no need to handle disk not found case, return success here.
return nil
}
// loadAllFormats - load all format config from all input disks in parallel.
func loadAllFormats(bootstrapDisks []StorageAPI) ([]*formatConfigV1, []error) {
// Initialize sync waitgroup.
var wg = &sync.WaitGroup{}
// Initialize list of errors.
var sErrs = make([]error, len(bootstrapDisks))
// Initialize format configs.
var formatConfigs = make([]*formatConfigV1, len(bootstrapDisks))
// Make a volume entry on all underlying storage disks.
for index, disk := range bootstrapDisks {
if disk == nil {
sErrs[index] = errDiskNotFound
continue
}
wg.Add(1)
// Make a volume inside a go-routine.
go func(index int, disk StorageAPI) {
defer wg.Done()
formatConfig, lErr := loadFormat(disk)
if lErr != nil {
sErrs[index] = lErr
return
}
formatConfigs[index] = formatConfig
}(index, disk)
}
// Wait for all make vol to finish.
wg.Wait()
for _, err := range sErrs {
if err != nil {
// Return all formats and errors.
return formatConfigs, sErrs
}
}
// Return all formats and nil
return formatConfigs, nil
}
// genericFormatCheck - validates and returns error.
// if (no quorum) return error
// if (any disk is corrupt) return error // phase2
// if (jbod inconsistent) return error // phase2
// if (disks not recognized) // Always error.
func genericFormatCheck(formatConfigs []*formatConfigV1, sErrs []error) (err error) {
// Calculate the errors.
var (
errCorruptFormatCount = 0
errCount = 0
)
// Through all errors calculate the actual errors.
for _, lErr := range sErrs {
if lErr == nil {
continue
}
// These errors are good conditions, means disk is online.
if lErr == errUnformattedDisk || lErr == errVolumeNotFound {
continue
}
if lErr == errCorruptedFormat {
errCorruptFormatCount++
} else {
errCount++
}
}
// Calculate read quorum.
readQuorum := len(formatConfigs)/2 + 1
// Validate the err count under tolerant limit.
if errCount > len(formatConfigs)-readQuorum {
return errXLReadQuorum
}
// Check if number of corrupted format under quorum
if errCorruptFormatCount > len(formatConfigs)-readQuorum {
return errCorruptedFormat
}
// Validates if format and JBOD are consistent across all disks.
if err = checkFormatXL(formatConfigs); err != nil {
return err
}
// Success..
return nil
}
// isSavedUUIDInOrder - validates if disk uuid is present and valid in all
// available format config JBOD. This function also validates if the disk UUID
// is always available on all JBOD under the same order.
func isSavedUUIDInOrder(uuid string, formatConfigs []*formatConfigV1) bool {
var orderIndexes []int
// Validate each for format.json for relevant uuid.
for _, formatConfig := range formatConfigs {
if formatConfig == nil {
continue
}
// Validate if UUID is present in JBOD.
uuidIndex := findDiskIndex(uuid, formatConfig.XL.JBOD)
if uuidIndex == -1 {
// UUID not found.
errorIf(errDiskNotFound, "Disk %s not found in JBOD list", uuid)
return false
}
// Save the position of UUID present in JBOD.
orderIndexes = append(orderIndexes, uuidIndex+1)
}
// Once uuid is found, verify if the uuid
// present in same order across all format configs.
prevOrderIndex := orderIndexes[0]
for _, orderIndex := range orderIndexes {
if prevOrderIndex != orderIndex {
errorIf(errDiskOrderMismatch, "Disk %s is in wrong order wanted %d, saw %d ", uuid, prevOrderIndex, orderIndex)
return false
}
}
// Returns success, when we have verified if uuid
// is consistent and valid across all format configs.
return true
}
// checkDisksConsistency - checks if all disks are consistent with all JBOD entries on all disks.
func checkDisksConsistency(formatConfigs []*formatConfigV1) error {
var disks = make([]string, len(formatConfigs))
// Collect currently available disk uuids.
for index, formatConfig := range formatConfigs {
if formatConfig == nil {
disks[index] = ""
continue
}
disks[index] = formatConfig.XL.Disk
}
// Validate collected uuids and verify JBOD.
for _, uuid := range disks {
if uuid == "" {
continue
}
// Is uuid present on all JBOD ?.
if !isSavedUUIDInOrder(uuid, formatConfigs) {
return fmt.Errorf("%s disk not found in JBOD", uuid)
}
}
return nil
}
// checkJBODConsistency - validate xl jbod order if they are consistent.
func checkJBODConsistency(formatConfigs []*formatConfigV1) error {
var sentinelJBOD []string
// Extract first valid JBOD.
for _, format := range formatConfigs {
if format == nil {
continue
}
sentinelJBOD = format.XL.JBOD
break
}
for _, format := range formatConfigs {
if format == nil {
continue
}
currentJBOD := format.XL.JBOD
if !reflect.DeepEqual(sentinelJBOD, currentJBOD) {
return errors.New("Inconsistent JBOD found.")
}
}
return nil
}
// findDiskIndex returns position of disk in JBOD.
func findDiskIndex(disk string, jbod []string) int {
for index, uuid := range jbod {
if uuid == disk {
return index
}
}
return -1
}
// reorderDisks - reorder disks in JBOD order.
func reorderDisks(bootstrapDisks []StorageAPI, formatConfigs []*formatConfigV1) ([]StorageAPI, error) {
var savedJBOD []string
for _, format := range formatConfigs {
if format == nil {
continue
}
savedJBOD = format.XL.JBOD
break
}
// Pick the first JBOD list to verify the order and construct new set of disk slice.
var newDisks = make([]StorageAPI, len(bootstrapDisks))
for fIndex, format := range formatConfigs {
if format == nil {
continue
}
jIndex := findDiskIndex(format.XL.Disk, savedJBOD)
if jIndex == -1 {
return nil, errors.New("Unrecognized uuid " + format.XL.Disk + " found")
}
newDisks[jIndex] = bootstrapDisks[fIndex]
}
return newDisks, nil
}
// loadFormat - loads format.json from disk.
func loadFormat(disk StorageAPI) (format *formatConfigV1, err error) {
buf, err := disk.ReadAll(minioMetaBucket, formatConfigFile)
if err != nil {
// 'file not found' and 'volume not found' as
// same. 'volume not found' usually means its a fresh disk.
if err == errFileNotFound || err == errVolumeNotFound {
var vols []VolInfo
vols, err = disk.ListVols()
if err != nil {
return nil, err
}
if len(vols) > 1 {
// 'format.json' not found, but we found user data.
return nil, errCorruptedFormat
}
// No other data found, its a fresh disk.
return nil, errUnformattedDisk
}
return nil, err
}
// Try to decode format json into formatConfigV1 struct.
format = &formatConfigV1{}
if err = json.Unmarshal(buf, format); err != nil {
return nil, err
}
// Success.
return format, nil
}
// isFormatNotFound - returns true if all `format.json` are not
// found on all disks.
func isFormatNotFound(formats []*formatConfigV1) bool {
for _, format := range formats {
// One of the `format.json` is found.
if format != nil {
return false
}
}
// All format.json missing, success.
return true
}
// isFormatFound - returns true if all input formats are found on
// all disks.
func isFormatFound(formats []*formatConfigV1) bool {
for _, format := range formats {
// One of `format.json` is not found.
if format == nil {
return false
}
}
// All format.json present, success.
return true
}
// Heals any missing format.json on the drives. Returns error only for unexpected errors
// as regular errors can be ignored since there might be enough quorum to be operational.
// Heals only fresh disks.
func healFormatXLFreshDisks(storageDisks []StorageAPI) error {
formatConfigs := make([]*formatConfigV1, len(storageDisks))
var referenceConfig *formatConfigV1
// Loads `format.json` from all disks.
for index, disk := range storageDisks {
// Disk not found or ignored is a valid case.
if disk == nil {
// Return nil, one of the disk is offline.
return nil
}
formatXL, err := loadFormat(disk)
if err != nil {
if err == errUnformattedDisk {
// format.json is missing, should be healed.
continue
} else if err == errDiskNotFound { // Is a valid case we
// can proceed without healing.
return nil
}
// Return error for unsupported errors.
return err
} // Success.
formatConfigs[index] = formatXL
}
// All `format.json` has been read successfully, previously completed.
if isFormatFound(formatConfigs) {
// Return success.
return nil
}
// All disks are fresh, format.json will be written by initFormatXL()
if isFormatNotFound(formatConfigs) {
return initFormatXL(storageDisks)
}
// Validate format configs for consistency in JBOD and disks.
if err := checkFormatXL(formatConfigs); err != nil {
return err
}
if referenceConfig == nil {
// This config will be used to update the drives missing format.json.
for _, formatConfig := range formatConfigs {
if formatConfig == nil {
continue
}
referenceConfig = formatConfig
break
}
}
// Collect new JBOD.
newJBOD := referenceConfig.XL.JBOD
// Reorder the disks based on the JBOD order.
orderedDisks, err := reorderDisks(storageDisks, formatConfigs)
if err != nil {
return err
}
// From ordered disks fill the UUID position.
for index, disk := range orderedDisks {
if disk == nil {
newJBOD[index] = getUUID()
}
}
// Collect new format configs.
var newFormatConfigs = make([]*formatConfigV1, len(orderedDisks))
// Collect new format configs that need to be written.
for index := range orderedDisks {
// New configs are generated since we are going
// to re-populate across all disks.
config := &formatConfigV1{
Version: referenceConfig.Version,
Format: referenceConfig.Format,
XL: &xlFormat{
Version: referenceConfig.XL.Version,
Disk: newJBOD[index],
JBOD: newJBOD,
},
}
newFormatConfigs[index] = config
}
// Fill in the missing disk back from format configs.
// We need to make sure we have kept the previous order
// and allowed fresh disks to be arranged anywhere.
// Following block facilitates to put fresh disks.
for index, format := range formatConfigs {
// Format is missing so we go through ordered disks.
if format == nil {
// At this point when disk is missing the fresh disk
// in the stack get it back from storageDisks.
for oIndex, disk := range orderedDisks {
if disk == nil {
orderedDisks[oIndex] = storageDisks[index]
break
}
}
}
}
// Save new `format.json` across all disks, in JBOD order.
return saveFormatXL(orderedDisks, newFormatConfigs)
}
// Disks from storageDiks are put in assignedDisks if found in orderedDisks and in unAssignedDisks otherwise
func splitDisksByUse(storageDisks, orderedDisks []StorageAPI) (assignedDisks []StorageAPI, unAssignedDisks []StorageAPI) {
// Populate unAssignDisks
for i := range storageDisks {
found := false
for j := range orderedDisks {
if storageDisks[i] == orderedDisks[j] {
found = true
assignedDisks = append(assignedDisks, storageDisks[i])
break
}
}
if !found {
unAssignedDisks = append(unAssignedDisks, storageDisks[i])
}
}
return assignedDisks, unAssignedDisks
}
// Inspect the content of all disks to guess the right order according to the format files.
// The right order is represented in orderedDisks
func reorderDisksByInspection(orderedDisks, storageDisks []StorageAPI, formatConfigs []*formatConfigV1) ([]StorageAPI, error) {
for index, format := range formatConfigs {
if format != nil {
continue
}
vols, err := storageDisks[index].ListVols()
if err != nil {
return nil, err
}
if len(vols) == 0 {
continue
}
volName := ""
// Avoid picking minioMetaBucket because ListVols() returns a non ordered list
for i := range vols {
if vols[i].Name != minioMetaBucket {
volName = vols[i].Name
break
}
}
if volName == "" {
continue
}
objects, err := storageDisks[index].ListDir(volName, "")
if err != nil {
return nil, err
}
if len(objects) == 0 {
continue
}
xlData, err := readXLMeta(storageDisks[index], volName, objects[0])
if err != nil {
if err == errFileNotFound {
continue
}
return nil, err
}
diskIndex := -1
for i, d := range xlData.Erasure.Distribution {
if d == xlData.Erasure.Index {
diskIndex = i
}
}
// Check for found results
if diskIndex == -1 || orderedDisks[diskIndex] != nil {
// Some inconsistent data are found, exit immediately.
return nil, errCorruptedFormat
}
orderedDisks[diskIndex] = storageDisks[index]
}
return orderedDisks, nil
}
// Heals corrupted format json in all disks
func healFormatXLCorruptedDisks(storageDisks []StorageAPI) error {
formatConfigs := make([]*formatConfigV1, len(storageDisks))
var referenceConfig *formatConfigV1
// Loads `format.json` from all disks.
for index, disk := range storageDisks {
// Disk not found or ignored is a valid case.
if disk == nil {
// Return nil, one of the disk is offline.
return nil
}
formatXL, err := loadFormat(disk)
if err != nil {
if err == errUnformattedDisk || err == errCorruptedFormat {
// format.json is missing or corrupted, should be healed.
continue
} else if err == errDiskNotFound { // Is a valid case we
// can proceed without healing.
return nil
}
// Return error for unsupported errors.
return err
} // Success.
formatConfigs[index] = formatXL
}
// All `format.json` has been read successfully, previously completed.
if isFormatFound(formatConfigs) {
// Return success.
return nil
}
// All disks are fresh, format.json will be written by initFormatXL()
if isFormatNotFound(formatConfigs) {
return initFormatXL(storageDisks)
}
// Validate format configs for consistency in JBOD and disks.
if err := checkFormatXL(formatConfigs); err != nil {
return err
}
if referenceConfig == nil {
// This config will be used to update the drives missing format.json.
for _, formatConfig := range formatConfigs {
if formatConfig == nil {
continue
}
referenceConfig = formatConfig
break
}
}
// Collect new JBOD.
newJBOD := referenceConfig.XL.JBOD
// Reorder the disks based on the JBOD order.
orderedDisks, err := reorderDisks(storageDisks, formatConfigs)
if err != nil {
return err
}
// From ordered disks fill the UUID position.
for index, disk := range orderedDisks {
if disk == nil {
newJBOD[index] = getUUID()
}
}
// For disks with corrupted formats, inspect the disks contents to guess the disks order
orderedDisks, err = reorderDisksByInspection(orderedDisks, storageDisks, formatConfigs)
if err != nil {
return err
}
// At this stage, all disks with corrupted formats but with objects inside found their way.
// Now take care of unformatted disks, which are the `unAssignedDisks`
_, unAssignedDisks := splitDisksByUse(storageDisks, orderedDisks)
// Assign unassigned disks to nil elements in orderedDisks
for i, disk := range orderedDisks {
if disk == nil && len(unAssignedDisks) > 0 {
orderedDisks[i] = unAssignedDisks[0]
unAssignedDisks = unAssignedDisks[1:]
}
}
// Collect new format configs.
var newFormatConfigs = make([]*formatConfigV1, len(orderedDisks))
// Collect new format configs that need to be written.
for index := range orderedDisks {
// New configs are generated since we are going
// to re-populate across all disks.
config := &formatConfigV1{
Version: referenceConfig.Version,
Format: referenceConfig.Format,
XL: &xlFormat{
Version: referenceConfig.XL.Version,
Disk: newJBOD[index],
JBOD: newJBOD,
},
}
newFormatConfigs[index] = config
}
// Save new `format.json` across all disks, in JBOD order.
return saveFormatXL(orderedDisks, newFormatConfigs)
}
// loadFormatXL - loads XL `format.json` and returns back properly
// ordered storage slice based on `format.json`.
func loadFormatXL(bootstrapDisks []StorageAPI) (disks []StorageAPI, err error) {
var unformattedDisksFoundCnt = 0
var diskNotFoundCount = 0
formatConfigs := make([]*formatConfigV1, len(bootstrapDisks))
// Try to load `format.json` bootstrap disks.
for index, disk := range bootstrapDisks {
if disk == nil {
diskNotFoundCount++
continue
}
var formatXL *formatConfigV1
formatXL, err = loadFormat(disk)
if err != nil {
if err == errUnformattedDisk {
unformattedDisksFoundCnt++
continue
} else if err == errDiskNotFound {
diskNotFoundCount++
continue
}
return nil, err
}
// Save valid formats.
formatConfigs[index] = formatXL
}
// If all disks indicate that 'format.json' is not available return 'errUnformattedDisk'.
if unformattedDisksFoundCnt > len(bootstrapDisks)-(len(bootstrapDisks)/2+1) {
return nil, errUnformattedDisk
} else if diskNotFoundCount == len(bootstrapDisks) {
return nil, errDiskNotFound
} else if diskNotFoundCount > len(bootstrapDisks)-(len(bootstrapDisks)/2+1) {
return nil, errXLReadQuorum
}
// Validate the format configs read are correct.
if err = checkFormatXL(formatConfigs); err != nil {
return nil, err
}
// Erasure code requires disks to be presented in the same order each time.
return reorderDisks(bootstrapDisks, formatConfigs)
}
// checkFormatXL - verifies if format.json format is intact.
func checkFormatXL(formatConfigs []*formatConfigV1) error {
for _, formatXL := range formatConfigs {
if formatXL == nil {
continue
}
// Validate format version and format type.
if formatXL.Version != "1" {
return fmt.Errorf("Unsupported version of backend format [%s] found.", formatXL.Version)
}
if formatXL.Format != "xl" {
return fmt.Errorf("Unsupported backend format [%s] found.", formatXL.Format)
}
if formatXL.XL.Version != "1" {
return fmt.Errorf("Unsupported XL backend format found [%s]", formatXL.XL.Version)
}
if len(formatConfigs) != len(formatXL.XL.JBOD) {
return fmt.Errorf("Number of disks %d did not match the backend format %d", len(formatConfigs), len(formatXL.XL.JBOD))
}
}
if err := checkJBODConsistency(formatConfigs); err != nil {
return err
}
return checkDisksConsistency(formatConfigs)
}
// saveFormatXL - populates `format.json` on disks in its order.
func saveFormatXL(storageDisks []StorageAPI, formats []*formatConfigV1) error {
var errs = make([]error, len(storageDisks))
var wg = &sync.WaitGroup{}
// Write `format.json` to all disks.
for index, disk := range storageDisks {
if disk == nil {
continue
}
wg.Add(1)
go func(index int, disk StorageAPI, format *formatConfigV1) {
defer wg.Done()
// Marshal and write to disk.
formatBytes, err := json.Marshal(format)
if err != nil {
errs[index] = err
return
}
// Purge any existing temporary file, okay to ignore errors here.
disk.DeleteFile(minioMetaBucket, formatConfigFileTmp)
// Append file `format.json.tmp`.
if err = disk.AppendFile(minioMetaBucket, formatConfigFileTmp, formatBytes); err != nil {
errs[index] = err
return
}
// Rename file `format.json.tmp` --> `format.json`.
if err = disk.RenameFile(minioMetaBucket, formatConfigFileTmp, minioMetaBucket, formatConfigFile); err != nil {
errs[index] = err
return
}
}(index, disk, formats[index])
}
// Wait for the routines to finish.
wg.Wait()
// Validate if we encountered any errors, return quickly.
for _, err := range errs {
if err != nil {
// Failure.
return err
}
}
// Success.
return nil
}
// initFormatXL - save XL format configuration on all disks.
func initFormatXL(storageDisks []StorageAPI) (err error) {
// Initialize jbods.
var jbod = make([]string, len(storageDisks))
// Initialize formats.
var formats = make([]*formatConfigV1, len(storageDisks))
// Initialize `format.json`.
for index, disk := range storageDisks {
if disk == nil {
continue
}
// Allocate format config.
formats[index] = &formatConfigV1{
Version: "1",
Format: "xl",
XL: &xlFormat{
Version: "1",
Disk: getUUID(),
},
}
jbod[index] = formats[index].XL.Disk
}
// Update the jbod entries.
for index, disk := range storageDisks {
if disk == nil {
continue
}
// Save jbod.
formats[index].XL.JBOD = jbod
}
// Save formats `format.json` across all disks.
return saveFormatXL(storageDisks, formats)
}
+445
View File
@@ -0,0 +1,445 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"bytes"
"testing"
)
// generates a valid format.json for XL backend.
func genFormatXLValid() []*formatConfigV1 {
jbod := make([]string, 8)
formatConfigs := make([]*formatConfigV1, 8)
for index := range jbod {
jbod[index] = getUUID()
}
for index := range jbod {
formatConfigs[index] = &formatConfigV1{
Version: "1",
Format: "xl",
XL: &xlFormat{
Version: "1",
Disk: jbod[index],
JBOD: jbod,
},
}
}
return formatConfigs
}
// generates a invalid format.json version for XL backend.
func genFormatXLInvalidVersion() []*formatConfigV1 {
jbod := make([]string, 8)
formatConfigs := make([]*formatConfigV1, 8)
for index := range jbod {
jbod[index] = getUUID()
}
for index := range jbod {
formatConfigs[index] = &formatConfigV1{
Version: "1",
Format: "xl",
XL: &xlFormat{
Version: "1",
Disk: jbod[index],
JBOD: jbod,
},
}
}
// Corrupt version numbers.
formatConfigs[0].Version = "2"
formatConfigs[3].Version = "-1"
return formatConfigs
}
// generates a invalid format.json JBOD for XL backend.
func genFormatXLInvalidJBOD() []*formatConfigV1 {
jbod := make([]string, 8)
formatConfigs := make([]*formatConfigV1, 8)
for index := range jbod {
jbod[index] = getUUID()
}
for index := range jbod {
formatConfigs[index] = &formatConfigV1{
Version: "1",
Format: "xl",
XL: &xlFormat{
Version: "1",
Disk: jbod[index],
JBOD: jbod,
},
}
}
for index := range jbod {
jbod[index] = getUUID()
}
// Corrupt JBOD entries on disk 6 and disk 8.
formatConfigs[5].XL.JBOD = jbod
formatConfigs[7].XL.JBOD = jbod
return formatConfigs
}
// generates a invalid format.json Disk UUID for XL backend.
func genFormatXLInvalidDisks() []*formatConfigV1 {
jbod := make([]string, 8)
formatConfigs := make([]*formatConfigV1, 8)
for index := range jbod {
jbod[index] = getUUID()
}
for index := range jbod {
formatConfigs[index] = &formatConfigV1{
Version: "1",
Format: "xl",
XL: &xlFormat{
Version: "1",
Disk: jbod[index],
JBOD: jbod,
},
}
}
// Make disk 5 and disk 8 have inconsistent disk uuid's.
formatConfigs[4].XL.Disk = getUUID()
formatConfigs[7].XL.Disk = getUUID()
return formatConfigs
}
// generates a invalid format.json Disk UUID in wrong order for XL backend.
func genFormatXLInvalidDisksOrder() []*formatConfigV1 {
jbod := make([]string, 8)
formatConfigs := make([]*formatConfigV1, 8)
for index := range jbod {
jbod[index] = getUUID()
}
for index := range jbod {
formatConfigs[index] = &formatConfigV1{
Version: "1",
Format: "xl",
XL: &xlFormat{
Version: "1",
Disk: jbod[index],
JBOD: jbod,
},
}
}
// Re order jbod for failure case.
var jbod1 = make([]string, 8)
for i, j := range jbod {
jbod1[i] = j
}
jbod1[1], jbod1[2] = jbod[2], jbod[1]
formatConfigs[2].XL.JBOD = jbod1
return formatConfigs
}
func TestFormatXLHealFreshDisks(t *testing.T) {
// Create an instance of xl backend.
obj, fsDirs, err := getXLObjectLayer()
if err != nil {
t.Fatal(err)
}
xl := obj.(xlObjects)
err = obj.MakeBucket("bucket")
if err != nil {
t.Fatal(err)
}
bucket := "bucket"
object := "object"
_, err = obj.PutObject(bucket, object, int64(len("abcd")), bytes.NewReader([]byte("abcd")), nil)
if err != nil {
t.Fatal(err)
}
/* // Now, remove two format files.. Load them and reorder
if err = xl.storageDisks[3].DeleteFile(".minio.sys", "format.json"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[11].DeleteFile(".minio.sys", "format.json"); err != nil {
t.Fatal(err)
} */
// Remove the content of export dir 10 but preserve .minio.sys because it is automatically
// created when minio starts
for i := 3; i <= 5; i++ {
if err = xl.storageDisks[i].DeleteFile(".minio.sys", "format.json"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[i].DeleteFile(".minio.sys", "tmp"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[i].DeleteFile(bucket, object+"/xl.json"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[i].DeleteFile(bucket, object+"/part.1"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[i].DeleteVol(bucket); err != nil {
t.Fatal(err)
}
}
permutedStorageDisks := []StorageAPI{xl.storageDisks[1], xl.storageDisks[4],
xl.storageDisks[2], xl.storageDisks[8], xl.storageDisks[6], xl.storageDisks[7],
xl.storageDisks[0], xl.storageDisks[15], xl.storageDisks[13], xl.storageDisks[14],
xl.storageDisks[3], xl.storageDisks[10], xl.storageDisks[12], xl.storageDisks[9],
xl.storageDisks[5], xl.storageDisks[11]}
// Start healing disks
err = healFormatXLFreshDisks(permutedStorageDisks)
if err != nil {
t.Fatal("healing corrupted disk failed: ", err)
}
// Load again XL format.json to validate it
_, err = loadFormatXL(permutedStorageDisks)
if err != nil {
t.Fatal("loading healed disk failed: ", err)
}
// Clean all
removeRoots(fsDirs)
}
// Simulate XL disks creation, delete some format.json and remove the content of
// a given disk to test healing a corrupted disk
func TestFormatXLHealCorruptedDisks(t *testing.T) {
// Create an instance of xl backend.
obj, fsDirs, err := getXLObjectLayer()
if err != nil {
t.Fatal(err)
}
xl := obj.(xlObjects)
err = obj.MakeBucket("bucket")
if err != nil {
t.Fatal(err)
}
bucket := "bucket"
object := "object"
_, err = obj.PutObject(bucket, object, int64(len("abcd")), bytes.NewReader([]byte("abcd")), nil)
if err != nil {
t.Fatal(err)
}
// Now, remove two format files.. Load them and reorder
if err = xl.storageDisks[3].DeleteFile(".minio.sys", "format.json"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[11].DeleteFile(".minio.sys", "format.json"); err != nil {
t.Fatal(err)
}
// Remove the content of export dir 10 but preserve .minio.sys because it is automatically
// created when minio starts
if err = xl.storageDisks[10].DeleteFile(".minio.sys", "format.json"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[10].DeleteFile(".minio.sys", "tmp"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[10].DeleteFile(bucket, object+"/xl.json"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[10].DeleteFile(bucket, object+"/part.1"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[10].DeleteVol(bucket); err != nil {
t.Fatal(err)
}
permutedStorageDisks := []StorageAPI{xl.storageDisks[1], xl.storageDisks[4],
xl.storageDisks[2], xl.storageDisks[8], xl.storageDisks[6], xl.storageDisks[7],
xl.storageDisks[0], xl.storageDisks[15], xl.storageDisks[13], xl.storageDisks[14],
xl.storageDisks[3], xl.storageDisks[10], xl.storageDisks[12], xl.storageDisks[9],
xl.storageDisks[5], xl.storageDisks[11]}
// Start healing disks
err = healFormatXLCorruptedDisks(permutedStorageDisks)
if err != nil {
t.Fatal("healing corrupted disk failed: ", err)
}
// Load again XL format.json to validate it
_, err = loadFormatXL(permutedStorageDisks)
if err != nil {
t.Fatal("loading healed disk failed: ", err)
}
// Clean all
removeRoots(fsDirs)
}
// Test on ReorderByInspection by simulating creating disks and removing
// some of format.json
func TestFormatXLReorderByInspection(t *testing.T) {
// Create an instance of xl backend.
obj, fsDirs, err := getXLObjectLayer()
if err != nil {
t.Fatal(err)
}
xl := obj.(xlObjects)
err = obj.MakeBucket("bucket")
if err != nil {
t.Fatal(err)
}
bucket := "bucket"
object := "object"
_, err = obj.PutObject(bucket, object, int64(len("abcd")), bytes.NewReader([]byte("abcd")), nil)
if err != nil {
t.Fatal(err)
}
// Now, remove two format files.. Load them and reorder
if err = xl.storageDisks[3].DeleteFile(".minio.sys", "format.json"); err != nil {
t.Fatal(err)
}
if err = xl.storageDisks[5].DeleteFile(".minio.sys", "format.json"); err != nil {
t.Fatal(err)
}
permutedStorageDisks := []StorageAPI{xl.storageDisks[1], xl.storageDisks[4],
xl.storageDisks[2], xl.storageDisks[8], xl.storageDisks[6], xl.storageDisks[7],
xl.storageDisks[0], xl.storageDisks[15], xl.storageDisks[13], xl.storageDisks[14],
xl.storageDisks[3], xl.storageDisks[10], xl.storageDisks[12], xl.storageDisks[9],
xl.storageDisks[5], xl.storageDisks[11]}
permutedFormatConfigs, _ := loadAllFormats(permutedStorageDisks)
orderedDisks, err := reorderDisks(permutedStorageDisks, permutedFormatConfigs)
if err != nil {
t.Fatal("error reordering disks\n")
}
orderedDisks, err = reorderDisksByInspection(orderedDisks, permutedStorageDisks, permutedFormatConfigs)
if err != nil {
t.Fatal("failed to reorder disk by inspection")
}
// Check disks reordering
for i := 0; i <= 15; i++ {
if orderedDisks[i] == nil && i != 3 && i != 5 {
t.Fatal("should not be nil")
}
if orderedDisks[i] != nil && orderedDisks[i] != xl.storageDisks[i] {
t.Fatal("Disks were not ordered correctly.")
}
}
removeRoots(fsDirs)
}
// Wrapper for calling FormatXL tests - currently validates
// - valid format
// - unrecognized version number
// - invalid JBOD
// - invalid Disk uuid
func TestFormatXL(t *testing.T) {
formatInputCases := [][]*formatConfigV1{
genFormatXLValid(),
genFormatXLInvalidVersion(),
genFormatXLInvalidJBOD(),
genFormatXLInvalidDisks(),
genFormatXLInvalidDisksOrder(),
}
testCases := []struct {
formatConfigs []*formatConfigV1
shouldPass bool
}{
{
formatConfigs: formatInputCases[0],
shouldPass: true,
},
{
formatConfigs: formatInputCases[1],
shouldPass: false,
},
{
formatConfigs: formatInputCases[2],
shouldPass: false,
},
{
formatConfigs: formatInputCases[3],
shouldPass: false,
},
{
formatConfigs: formatInputCases[4],
shouldPass: false,
},
}
for i, testCase := range testCases {
err := checkFormatXL(testCase.formatConfigs)
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass but failed with %s", i+1, err)
}
if err == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail but passed instead", i+1)
}
}
}
// Tests uuid order verification function.
func TestSavedUUIDOrder(t *testing.T) {
uuidTestCases := make([]struct {
uuid string
shouldPass bool
}, 8)
jbod := make([]string, 8)
formatConfigs := make([]*formatConfigV1, 8)
for index := range jbod {
jbod[index] = getUUID()
uuidTestCases[index].uuid = jbod[index]
uuidTestCases[index].shouldPass = true
}
for index := range jbod {
formatConfigs[index] = &formatConfigV1{
Version: "1",
Format: "xl",
XL: &xlFormat{
Version: "1",
Disk: jbod[index],
JBOD: jbod,
},
}
}
// Re order jbod for failure case.
var jbod1 = make([]string, 8)
for i, j := range jbod {
jbod1[i] = j
}
jbod1[1], jbod1[2] = jbod[2], jbod[1]
formatConfigs[2].XL.JBOD = jbod1
uuidTestCases[1].shouldPass = false
uuidTestCases[2].shouldPass = false
for i, testCase := range uuidTestCases {
// Is uuid present on all JBOD ?.
if testCase.shouldPass != isSavedUUIDInOrder(testCase.uuid, formatConfigs) {
t.Errorf("Test %d: Expected to pass but failed", i+1)
}
}
}
-182
View File
@@ -1,182 +0,0 @@
/*
* Minio Cloud Storage, (C) 2015-2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/minio/minio/pkg/probe"
)
const (
// listObjectsLimit - maximum list objects limit.
listObjectsLimit = 1000
)
// isDirExist - returns whether given directory is exist or not.
func isDirExist(dirname string) (bool, error) {
fi, e := os.Lstat(dirname)
if e != nil {
if os.IsNotExist(e) {
return false, nil
}
return false, e
}
return fi.IsDir(), nil
}
func (fs *Filesystem) saveTreeWalk(params listObjectParams, walker *treeWalker) {
fs.listObjectMapMutex.Lock()
defer fs.listObjectMapMutex.Unlock()
walkers, _ := fs.listObjectMap[params]
walkers = append(walkers, walker)
fs.listObjectMap[params] = walkers
}
func (fs *Filesystem) lookupTreeWalk(params listObjectParams) *treeWalker {
fs.listObjectMapMutex.Lock()
defer fs.listObjectMapMutex.Unlock()
if walkChs, ok := fs.listObjectMap[params]; ok {
for i, walkCh := range walkChs {
if !walkCh.timedOut {
newWalkChs := walkChs[i+1:]
if len(newWalkChs) > 0 {
fs.listObjectMap[params] = newWalkChs
} else {
delete(fs.listObjectMap, params)
}
return walkCh
}
}
// As all channels are timed out, delete the map entry
delete(fs.listObjectMap, params)
}
return nil
}
// ListObjects - lists all objects for a given prefix, returns up to
// maxKeys number of objects per call.
func (fs Filesystem) ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, *probe.Error) {
result := ListObjectsInfo{}
// Input validation.
bucket, e := fs.checkBucketArg(bucket)
if e != nil {
return result, probe.NewError(e)
}
bucketDir := filepath.Join(fs.diskPath, bucket)
if !IsValidObjectPrefix(prefix) {
return result, probe.NewError(ObjectNameInvalid{Bucket: bucket, Object: prefix})
}
// Verify if delimiter is anything other than '/', which we do not support.
if delimiter != "" && delimiter != "/" {
return result, probe.NewError(fmt.Errorf("delimiter '%s' is not supported. Only '/' is supported", delimiter))
}
// Verify if marker has prefix.
if marker != "" {
if !strings.HasPrefix(marker, prefix) {
return result, probe.NewError(fmt.Errorf("Invalid combination of marker '%s' and prefix '%s'", marker, prefix))
}
}
// Return empty response for a valid request when maxKeys is 0.
if maxKeys == 0 {
return result, nil
}
// Over flowing maxkeys - reset to listObjectsLimit.
if maxKeys < 0 || maxKeys > listObjectsLimit {
maxKeys = listObjectsLimit
}
// Verify if prefix exists.
prefixDir := filepath.Dir(filepath.FromSlash(prefix))
rootDir := filepath.Join(bucketDir, prefixDir)
if status, e := isDirExist(rootDir); !status {
if e == nil {
// Prefix does not exist, not an error just respond empty
// list response.
return result, nil
}
// Rest errors should be treated as failure.
return result, probe.NewError(e)
}
recursive := true
if delimiter == "/" {
recursive = false
}
// Maximum 1000 objects returned in a single to listObjects.
// Further calls will set right marker value to continue reading the rest of the objectList.
// popTreeWalker returns nil if the call to ListObject is done for the first time.
// On further calls to ListObjects to retrive more objects within the timeout period,
// popTreeWalker returns the channel from which rest of the objects can be retrieved.
walker := fs.lookupTreeWalk(listObjectParams{bucket, delimiter, marker, prefix})
if walker == nil {
walker = startTreeWalk(fs.diskPath, bucket, filepath.FromSlash(prefix), filepath.FromSlash(marker), recursive)
}
nextMarker := ""
for i := 0; i < maxKeys; {
walkResult, ok := <-walker.ch
if !ok {
// Closed channel.
return result, nil
}
// For any walk error return right away.
if walkResult.err != nil {
return ListObjectsInfo{}, probe.NewError(walkResult.err)
}
objInfo := walkResult.objectInfo
objInfo.Name = filepath.ToSlash(objInfo.Name)
// Skip temporary files.
if strings.Contains(objInfo.Name, "$multiparts") || strings.Contains(objInfo.Name, "$tmpobject") {
continue
}
// For objects being directory and delimited we set Prefixes.
if objInfo.IsDir {
result.Prefixes = append(result.Prefixes, objInfo.Name)
} else {
result.Objects = append(result.Objects, objInfo)
}
// We have listed everything return.
if walkResult.end {
return result, nil
}
nextMarker = objInfo.Name
i++
}
// We haven't exhaused the list yet, set IsTruncated to 'true' so
// that the client can send another request.
result.IsTruncated = true
result.NextMarker = nextMarker
fs.saveTreeWalk(listObjectParams{bucket, delimiter, nextMarker, prefix}, walker)
return result, nil
}
-163
View File
@@ -1,163 +0,0 @@
/*
* Minio Cloud Storage, (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/minio/minio/pkg/probe"
)
/// Bucket Operations
// DeleteBucket - delete a bucket.
func (fs Filesystem) DeleteBucket(bucket string) *probe.Error {
// Verify bucket is valid.
if !IsValidBucketName(bucket) {
return probe.NewError(BucketNameInvalid{Bucket: bucket})
}
bucket = getActualBucketname(fs.diskPath, bucket)
bucketDir := filepath.Join(fs.diskPath, bucket)
if e := os.Remove(bucketDir); e != nil {
// Error if there was no bucket in the first place.
if os.IsNotExist(e) {
return probe.NewError(BucketNotFound{Bucket: bucket})
}
// On windows the string is slightly different, handle it here.
if strings.Contains(e.Error(), "directory is not empty") {
return probe.NewError(BucketNotEmpty{Bucket: bucket})
}
// Hopefully for all other operating systems, this is
// assumed to be consistent.
if strings.Contains(e.Error(), "directory not empty") {
return probe.NewError(BucketNotEmpty{Bucket: bucket})
}
return probe.NewError(e)
}
return nil
}
// ListBuckets - Get service.
func (fs Filesystem) ListBuckets() ([]BucketInfo, *probe.Error) {
files, e := ioutil.ReadDir(fs.diskPath)
if e != nil {
return []BucketInfo{}, probe.NewError(e)
}
var buckets []BucketInfo
for _, file := range files {
if !file.IsDir() {
// If not directory, ignore all file types.
continue
}
// If directories are found with odd names, skip them.
dirName := strings.ToLower(file.Name())
if !IsValidBucketName(dirName) {
continue
}
bucket := BucketInfo{
Name: dirName,
Created: file.ModTime(),
}
buckets = append(buckets, bucket)
}
// Remove duplicated entries.
buckets = removeDuplicateBuckets(buckets)
return buckets, nil
}
// removeDuplicateBuckets - remove duplicate buckets.
func removeDuplicateBuckets(buckets []BucketInfo) []BucketInfo {
length := len(buckets) - 1
for i := 0; i < length; i++ {
for j := i + 1; j <= length; j++ {
if buckets[i].Name == buckets[j].Name {
if buckets[i].Created.Sub(buckets[j].Created) > 0 {
buckets[i] = buckets[length]
} else {
buckets[j] = buckets[length]
}
buckets = buckets[0:length]
length--
j--
}
}
}
return buckets
}
// MakeBucket - PUT Bucket
func (fs Filesystem) MakeBucket(bucket string) *probe.Error {
if _, e := fs.checkBucketArg(bucket); e == nil {
return probe.NewError(BucketExists{Bucket: bucket})
} else if _, ok := e.(BucketNameInvalid); ok {
return probe.NewError(BucketNameInvalid{Bucket: bucket})
}
bucketDir := filepath.Join(fs.diskPath, bucket)
// Make bucket.
if e := os.Mkdir(bucketDir, 0700); e != nil {
return probe.NewError(e)
}
return nil
}
// getActualBucketname - will convert incoming bucket names to
// corresponding actual bucketnames on the backend in a platform
// compatible way for all operating systems.
func getActualBucketname(fsPath, bucket string) string {
fd, e := os.Open(fsPath)
if e != nil {
return bucket
}
buckets, e := fd.Readdirnames(-1)
if e != nil {
return bucket
}
for _, b := range buckets {
// Verify if lowercase version of the bucket is equal
// to the incoming bucket, then use the proper name.
if strings.ToLower(b) == bucket {
return b
}
}
return bucket
}
// GetBucketInfo - get bucket metadata.
func (fs Filesystem) GetBucketInfo(bucket string) (BucketInfo, *probe.Error) {
if !IsValidBucketName(bucket) {
return BucketInfo{}, probe.NewError(BucketNameInvalid{Bucket: bucket})
}
bucket = getActualBucketname(fs.diskPath, bucket)
// Get bucket path.
bucketDir := filepath.Join(fs.diskPath, bucket)
fi, e := os.Stat(bucketDir)
if e != nil {
// Check if bucket exists.
if os.IsNotExist(e) {
return BucketInfo{}, probe.NewError(BucketNotFound{Bucket: bucket})
}
return BucketInfo{}, probe.NewError(e)
}
bucketMetadata := BucketInfo{}
bucketMetadata.Name = fi.Name()
bucketMetadata.Created = fi.ModTime()
return bucketMetadata, nil
}
-256
View File
@@ -1,256 +0,0 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"io/ioutil"
"os"
"strconv"
"strings"
"testing"
)
// The test not just includes asserting the correctness of the output,
// But also includes test cases for which the function should fail.
// For those cases for which it fails, its also asserted whether the function fails as expected.
func TestGetBucketInfo(t *testing.T) {
// Make a temporary directory to use as the fs.
directory, e := ioutil.TempDir("", "minio-metadata-test")
if e != nil {
t.Fatal(e)
}
defer os.RemoveAll(directory)
// Create the fs.
fs, err := newFS(directory)
if err != nil {
t.Fatal(err)
}
// Creating few buckets.
for i := 0; i < 4; i++ {
err = fs.MakeBucket("meta-test-bucket." + strconv.Itoa(i))
if err != nil {
t.Fatal(err)
}
}
testCases := []struct {
bucketName string
metaData BucketInfo
e error
shouldPass bool
}{
// Test cases with invalid bucket names.
{".test", BucketInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
{"Test", BucketInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
{"---", BucketInfo{}, BucketNameInvalid{Bucket: "---"}, false},
{"ad", BucketInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
// Test cases with non-existent buckets.
{"volatile-bucket-1", BucketInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
{"volatile-bucket-2", BucketInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
// Test cases with existing buckets.
{"meta-test-bucket.0", BucketInfo{Name: "meta-test-bucket.0"}, nil, true},
{"meta-test-bucket.1", BucketInfo{Name: "meta-test-bucket.1"}, nil, true},
{"meta-test-bucket.2", BucketInfo{Name: "meta-test-bucket.2"}, nil, true},
{"meta-test-bucket.3", BucketInfo{Name: "meta-test-bucket.3"}, nil, true},
}
for i, testCase := range testCases {
// The err returned is of type *probe.Error.
bucketInfo, err := fs.GetBucketInfo(testCase.bucketName)
if err != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Cause.Error())
}
if err == nil && !testCase.shouldPass {
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.e.Error())
}
// Failed as expected, but does it fail for the expected reason.
if err != nil && !testCase.shouldPass {
if testCase.e.Error() != err.Cause.Error() {
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, testCase.e.Error(), err.Cause.Error())
}
}
// Since there are cases for which GetBucketInfo fails, this is necessary.
// Test passes as expected, but the output values are verified for correctness here.
if err == nil && testCase.shouldPass {
if testCase.bucketName != bucketInfo.Name {
t.Errorf("Test %d: Expected the bucket name to be \"%s\", but found \"%s\" instead", i+1, testCase.bucketName, bucketInfo.Name)
}
}
}
}
func TestListBuckets(t *testing.T) {
// Make a temporary directory to use as the fs.
directory, e := ioutil.TempDir("", "minio-benchmark")
if e != nil {
t.Fatal(e)
}
defer os.RemoveAll(directory)
// Create the fs.
fs, err := newFS(directory)
if err != nil {
t.Fatal(err)
}
// Create a few buckets.
for i := 0; i < 10; i++ {
err = fs.MakeBucket("testbucket." + strconv.Itoa(i))
if err != nil {
t.Fatal(err)
}
}
// List, and ensure that they are all there.
metadatas, err := fs.ListBuckets()
if err != nil {
t.Fatal(err)
}
if len(metadatas) != 10 {
t.Errorf("incorrect length of metadatas (%d)\n", len(metadatas))
}
// Iterate over the buckets, ensuring that the name is correct.
for i := 0; i < len(metadatas); i++ {
if !strings.Contains(metadatas[i].Name, "testbucket") {
t.Fail()
}
}
}
func TestDeleteBucket(t *testing.T) {
// Make a temporary directory to use as the fs.
directory, e := ioutil.TempDir("", "minio-benchmark")
if e != nil {
t.Fatal(e)
}
defer os.RemoveAll(directory)
// Create the fs.
fs, err := newFS(directory)
if err != nil {
t.Fatal(err)
}
// Deleting a bucket that doesn't exist should error.
err = fs.DeleteBucket("bucket")
if !strings.Contains(err.Cause.Error(), "Bucket not found:") {
t.Fail()
}
}
func BenchmarkListBuckets(b *testing.B) {
// Make a temporary directory to use as the fs.
directory, e := ioutil.TempDir("", "minio-benchmark")
if e != nil {
b.Fatal(e)
}
defer os.RemoveAll(directory)
// Create the fs.
fs, err := newFS(directory)
if err != nil {
b.Fatal(err)
}
// Create a few buckets.
for i := 0; i < 20; i++ {
err = fs.MakeBucket("bucket." + strconv.Itoa(i))
if err != nil {
b.Fatal(err)
}
}
b.ResetTimer()
// List the buckets over and over and over.
for i := 0; i < b.N; i++ {
_, err = fs.ListBuckets()
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkDeleteBucket(b *testing.B) {
// Make a temporary directory to use as the fs.
directory, e := ioutil.TempDir("", "minio-benchmark")
if e != nil {
b.Fatal(e)
}
defer os.RemoveAll(directory)
// Create the fs.
fs, err := newFS(directory)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Creating buckets takes time, so stop and start the timer.
b.StopTimer()
// Create and delete the bucket over and over.
err = fs.MakeBucket("bucket")
if err != nil {
b.Fatal(err)
}
b.StartTimer()
err = fs.DeleteBucket("bucket")
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkGetBucketInfo(b *testing.B) {
// Make a temporary directory to use as the fs.
directory, e := ioutil.TempDir("", "minio-benchmark")
if e != nil {
b.Fatal(e)
}
defer os.RemoveAll(directory)
// Create the fs.
fs, err := newFS(directory)
if err != nil {
b.Fatal(err)
}
// Put up a bucket with some metadata.
err = fs.MakeBucket("bucket")
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Retrieve the metadata!
_, err := fs.GetBucketInfo("bucket")
if err != nil {
b.Fatal(err)
}
}
}
+41
View File
@@ -0,0 +1,41 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import "io"
func fsCreateFile(disk StorageAPI, reader io.Reader, buf []byte, tmpBucket, tempObj string) (int64, error) {
bytesWritten := int64(0)
// Read the buffer till io.EOF and append the read data to the temporary file.
for {
n, rErr := reader.Read(buf)
if rErr != nil && rErr != io.EOF {
return 0, rErr
}
bytesWritten += int64(n)
if n > 0 {
wErr := disk.AppendFile(tmpBucket, tempObj, buf[0:n])
if wErr != nil {
return 0, wErr
}
}
if rErr == io.EOF {
break
}
}
return bytesWritten, nil
}
-217
View File
@@ -1,217 +0,0 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// fsDirent carries directory entries.
type fsDirent struct {
name string
modTime time.Time // On Solaris and older unix distros this is empty.
size int64 // On Solaris and older unix distros this is empty.
mode os.FileMode
}
// IsDir - returns true if fsDirent is a directory
func (ent fsDirent) IsDir() bool {
return ent.mode.IsDir()
}
// IsSymlink - returns true if fsDirent is a symbolic link
func (ent fsDirent) IsSymlink() bool {
return ent.mode&os.ModeSymlink == os.ModeSymlink
}
// IsRegular - returns true if fsDirent is a regular file
func (ent fsDirent) IsRegular() bool {
return ent.mode.IsRegular()
}
// byDirentName is a collection satisfying sort.Interface.
type byDirentName []fsDirent
func (d byDirentName) Len() int { return len(d) }
func (d byDirentName) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
func (d byDirentName) Less(i, j int) bool { return d[i].name < d[j].name }
// Using sort.Search() internally to jump to the file entry containing the prefix.
func searchDirents(dirents []fsDirent, x string) int {
processFunc := func(i int) bool {
return dirents[i].name >= x
}
return sort.Search(len(dirents), processFunc)
}
// Tree walk result carries results of tree walking.
type treeWalkResult struct {
objectInfo ObjectInfo
err error
end bool
}
// Tree walk notify carries a channel which notifies tree walk
// results, additionally it also carries information if treeWalk
// should be timedOut.
type treeWalker struct {
ch <-chan treeWalkResult
timedOut bool
}
// treeWalk walks FS directory tree recursively pushing ObjectInfo into the channel as and when it encounters files.
func treeWalk(bucketDir, prefixDir, entryPrefixMatch, marker string, recursive bool, send func(treeWalkResult) bool, count *int) bool {
// Example:
// if prefixDir="one/two/three/" and marker="four/five.txt" treeWalk is recursively
// called with prefixDir="one/two/three/four/" and marker="five.txt"
// Convert dirent to ObjectInfo
direntToObjectInfo := func(dirent fsDirent) (ObjectInfo, error) {
objectInfo := ObjectInfo{}
// Convert to full object name.
objectInfo.Name = filepath.Join(prefixDir, dirent.name)
if dirent.modTime.IsZero() && dirent.size == 0 {
// ModifiedTime and Size are zero, Stat() and figure out
// the actual values that need to be set.
fi, err := os.Stat(filepath.Join(bucketDir, prefixDir, dirent.name))
if err != nil {
return ObjectInfo{}, err
}
// Fill size and modtime.
objectInfo.ModifiedTime = fi.ModTime()
objectInfo.Size = fi.Size()
objectInfo.IsDir = fi.IsDir()
} else {
// If ModifiedTime or Size are set then use them
// without attempting another Stat operation.
objectInfo.ModifiedTime = dirent.modTime
objectInfo.Size = dirent.size
objectInfo.IsDir = dirent.IsDir()
}
if objectInfo.IsDir {
// Add os.PathSeparator suffix again for directories as
// filepath.Join would have removed it.
objectInfo.Size = 0
objectInfo.Name += string(os.PathSeparator)
}
return objectInfo, nil
}
var markerBase, markerDir string
if marker != "" {
// Ex: if marker="four/five.txt", markerDir="four/" markerBase="five.txt"
markerSplit := strings.SplitN(marker, string(os.PathSeparator), 2)
markerDir = markerSplit[0]
if len(markerSplit) == 2 {
markerDir += string(os.PathSeparator)
markerBase = markerSplit[1]
}
}
// readDirAll returns entries that begins with entryPrefixMatch
dirents, err := readDirAll(filepath.Join(bucketDir, prefixDir), entryPrefixMatch)
if err != nil {
send(treeWalkResult{err: err})
return false
}
// example:
// If markerDir="four/" searchDirents() returns the index of "four/" in the sorted
// dirents list. We skip all the dirent entries till "four/"
dirents = dirents[searchDirents(dirents, markerDir):]
*count += len(dirents)
for i, dirent := range dirents {
if i == 0 && markerDir == dirent.name && !dirent.IsDir() {
// If the first entry is not a directory
// we need to skip this entry.
*count--
continue
}
if dirent.IsDir() && recursive {
// If the entry is a directory, we will need recurse into it.
markerArg := ""
if dirent.name == markerDir {
// We need to pass "five.txt" as marker only if we are
// recursing into "four/"
markerArg = markerBase
}
*count--
if !treeWalk(bucketDir, filepath.Join(prefixDir, dirent.name), "", markerArg, recursive, send, count) {
return false
}
continue
}
objectInfo, err := direntToObjectInfo(dirent)
if err != nil {
send(treeWalkResult{err: err})
return false
}
*count--
if !send(treeWalkResult{objectInfo: objectInfo}) {
return false
}
}
return true
}
// Initiate a new treeWalk in a goroutine.
func startTreeWalk(fsPath, bucket, prefix, marker string, recursive bool) *treeWalker {
// Example 1
// If prefix is "one/two/three/" and marker is "one/two/three/four/five.txt"
// treeWalk is called with prefixDir="one/two/three/" and marker="four/five.txt"
// and entryPrefixMatch=""
// Example 2
// if prefix is "one/two/th" and marker is "one/two/three/four/five.txt"
// treeWalk is called with prefixDir="one/two/" and marker="three/four/five.txt"
// and entryPrefixMatch="th"
ch := make(chan treeWalkResult, listObjectsLimit)
walkNotify := treeWalker{ch: ch}
entryPrefixMatch := prefix
prefixDir := ""
lastIndex := strings.LastIndex(prefix, string(os.PathSeparator))
if lastIndex != -1 {
entryPrefixMatch = prefix[lastIndex+1:]
prefixDir = prefix[:lastIndex+1]
}
count := 0
marker = strings.TrimPrefix(marker, prefixDir)
go func() {
defer close(ch)
send := func(walkResult treeWalkResult) bool {
// Add the bucket.
walkResult.objectInfo.Bucket = bucket
if count == 0 {
walkResult.end = true
}
timer := time.After(time.Second * 60)
select {
case ch <- walkResult:
return true
case <-timer:
walkNotify.timedOut = true
return false
}
}
bucketDir := filepath.Join(fsPath, bucket)
treeWalk(bucketDir, prefixDir, entryPrefixMatch, marker, recursive, send, &count)
}()
return &walkNotify
}
-108
View File
@@ -1,108 +0,0 @@
// +build !linux,!darwin,!openbsd,!freebsd,!netbsd,!dragonfly
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"io"
"os"
"path/filepath"
"sort"
"strings"
)
// Read all directory entries, returns a list of lexically sorted entries.
func readDirAll(readDirPath, entryPrefixMatch string) ([]fsDirent, error) {
f, err := os.Open(readDirPath)
if err != nil {
return nil, err
}
defer f.Close()
var dirents []fsDirent
for {
fis, err := f.Readdir(1000)
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
for _, fi := range fis {
dirent := fsDirent{
name: fi.Name(),
modTime: fi.ModTime(),
size: fi.Size(),
mode: fi.Mode(),
}
if dirent.IsDir() {
dirent.name += string(os.PathSeparator)
dirent.size = 0
}
if strings.HasPrefix(fi.Name(), entryPrefixMatch) {
dirents = append(dirents, dirent)
}
}
}
// Sort dirents.
sort.Sort(byDirentName(dirents))
return dirents, nil
}
// scans the directory dirPath, calling filter() on each directory
// entry. Entries for which filter() returns true are stored, lexically
// sorted using sort.Sort(). If filter is NULL, all entries are selected.
// If namesOnly is true, dirPath is not appended into entry name.
func scandir(dirPath string, filter func(fsDirent) bool, namesOnly bool) ([]fsDirent, error) {
d, err := os.Open(dirPath)
if err != nil {
return nil, err
}
defer d.Close()
var dirents []fsDirent
for {
fis, err := d.Readdir(1000)
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
for _, fi := range fis {
dirent := fsDirent{
name: fi.Name(),
modTime: fi.ModTime(),
size: fi.Size(),
mode: fi.Mode(),
}
if !namesOnly {
dirent.name = filepath.Join(dirPath, dirent.name)
}
if dirent.IsDir() {
dirent.name += string(os.PathSeparator)
}
if filter == nil || filter(dirent) {
dirents = append(dirents, dirent)
}
}
}
sort.Sort(byDirentName(dirents))
return dirents, nil
}
-295
View File
@@ -1,295 +0,0 @@
/*
* Minio Cloud Storage, (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import "fmt"
// InvalidArgument invalid argument
type InvalidArgument struct{}
func (e InvalidArgument) Error() string {
return "Invalid argument"
}
// UnsupportedFilesystem unsupported filesystem type
type UnsupportedFilesystem struct {
Type string
}
func (e UnsupportedFilesystem) Error() string {
return "Unsupported filesystem: " + e.Type
}
// RootPathFull root path out of space
type RootPathFull struct {
Path string
}
func (e RootPathFull) Error() string {
return "Root path " + e.Path + " reached its minimum free disk threshold."
}
// BucketNotFound bucket does not exist
type BucketNotFound struct {
Bucket string
}
func (e BucketNotFound) Error() string {
return "Bucket not found: " + e.Bucket
}
// BucketNotEmpty bucket is not empty
type BucketNotEmpty struct {
Bucket string
}
func (e BucketNotEmpty) Error() string {
return "Bucket not empty: " + e.Bucket
}
// ObjectNotFound object does not exist
type ObjectNotFound struct {
Bucket string
Object string
}
func (e ObjectNotFound) Error() string {
return "Object not found: " + e.Bucket + "#" + e.Object
}
// ObjectExistsAsPrefix object already exists with a requested prefix.
type ObjectExistsAsPrefix struct {
Bucket string
Prefix string
}
func (e ObjectExistsAsPrefix) Error() string {
return "Object exists on : " + e.Bucket + " as prefix " + e.Prefix
}
// ObjectCorrupted object found to be corrupted
type ObjectCorrupted struct {
Object string
}
func (e ObjectCorrupted) Error() string {
return "Object found corrupted: " + e.Object
}
// BucketExists bucket exists
type BucketExists struct {
Bucket string
}
func (e BucketExists) Error() string {
return "Bucket exists: " + e.Bucket
}
// CorruptedBackend backend found to be corrupted
type CorruptedBackend struct {
Backend string
}
func (e CorruptedBackend) Error() string {
return "Corrupted backend: " + e.Backend
}
// NotImplemented function not implemented
type NotImplemented struct {
Function string
}
func (e NotImplemented) Error() string {
return "Not implemented: " + e.Function
}
// InvalidDisksArgument invalid number of disks per node
type InvalidDisksArgument struct{}
func (e InvalidDisksArgument) Error() string {
return "Invalid number of disks per node"
}
// BadDigest - Content-MD5 you specified did not match what we received.
type BadDigest struct {
ExpectedMD5 string
CalculatedMD5 string
}
func (e BadDigest) Error() string {
return "Bad digest expected " + e.ExpectedMD5 + " is not valid with what we calculated " + e.CalculatedMD5
}
// InternalError - generic internal error
type InternalError struct{}
// BackendError - generic disk backend error
type BackendError struct {
Path string
}
// BackendCorrupted - path has corrupted data
type BackendCorrupted BackendError
// APINotImplemented - generic API not implemented error
type APINotImplemented struct {
API string
}
// GenericBucketError - generic bucket error
type GenericBucketError struct {
Bucket string
}
// BucketPolicyNotFound - no bucket policy found.
type BucketPolicyNotFound GenericBucketError
func (e BucketPolicyNotFound) Error() string {
return "No bucket policy found for bucket: " + e.Bucket
}
// GenericObjectError - generic object error
type GenericObjectError struct {
Bucket string
Object string
}
// ImplementationError - generic implementation error
type ImplementationError struct {
Bucket string
Object string
Err error
}
/// Bucket related errors
// BucketNameInvalid - bucketname provided is invalid
type BucketNameInvalid GenericBucketError
/// Object related errors
// ObjectNameInvalid - object name provided is invalid
type ObjectNameInvalid GenericObjectError
// Return string an error formatted as the given text
func (e ImplementationError) Error() string {
error := ""
if e.Bucket != "" {
error = error + "Bucket: " + e.Bucket + " "
}
if e.Object != "" {
error = error + "Object: " + e.Object + " "
}
error = error + "Error: " + e.Err.Error()
return error
}
// EmbedError - wrapper function for error object
func EmbedError(bucket, object string, err error) ImplementationError {
return ImplementationError{
Bucket: bucket,
Object: object,
Err: err,
}
}
// Return string an error formatted as the given text
func (e InternalError) Error() string {
return "Internal error occured"
}
// Return string an error formatted as the given text
func (e APINotImplemented) Error() string {
return "Api not implemented: " + e.API
}
// Return string an error formatted as the given text
func (e BucketNameInvalid) Error() string {
return "Bucket name invalid: " + e.Bucket
}
// Return string an error formatted as the given text
func (e ObjectNameInvalid) Error() string {
return "Object name invalid: " + e.Bucket + "#" + e.Object
}
// IncompleteBody You did not provide the number of bytes specified by the Content-Length HTTP header
type IncompleteBody GenericObjectError
// Return string an error formatted as the given text
func (e IncompleteBody) Error() string {
return e.Bucket + "#" + e.Object + "has incomplete body"
}
// Return string an error formatted as the given text
func (e BackendCorrupted) Error() string {
return "Backend corrupted: " + e.Path
}
// OperationNotPermitted - operation not permitted
type OperationNotPermitted struct {
Op string
Reason string
}
func (e OperationNotPermitted) Error() string {
return "Operation " + e.Op + " not permitted for reason: " + e.Reason
}
// InvalidRange - invalid range
type InvalidRange struct {
Start int64
Length int64
}
func (e InvalidRange) Error() string {
return fmt.Sprintf("Invalid range start:%d length:%d", e.Start, e.Length)
}
/// Multipart related errors
// InvalidUploadID invalid upload id
type InvalidUploadID struct {
UploadID string
}
func (e InvalidUploadID) Error() string {
return "Invalid upload id " + e.UploadID
}
// InvalidPart One or more of the specified parts could not be found
type InvalidPart struct{}
func (e InvalidPart) Error() string {
return "One or more of the specified parts could not be found"
}
// InvalidPartOrder parts are not ordered as Requested
type InvalidPartOrder struct {
UploadID string
}
func (e InvalidPartOrder) Error() string {
return "Invalid part order sent for " + e.UploadID
}
// MalformedXML invalid xml format
type MalformedXML struct{}
func (e MalformedXML) Error() string {
return "Malformed XML"
}
-280
View File
@@ -1,280 +0,0 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"errors"
"os"
"path/filepath"
"strings"
"time"
)
func scanMultipartDir(bucketDir, prefixPath, markerPath, uploadIDMarker string, recursive bool) <-chan multipartObjectInfo {
objectInfoCh := make(chan multipartObjectInfo, listObjectsLimit)
// TODO: check if bucketDir is absolute path
scanDir := bucketDir
dirDepth := bucketDir
if prefixPath != "" {
if !filepath.IsAbs(prefixPath) {
tmpPrefixPath := filepath.Join(bucketDir, prefixPath)
if strings.HasSuffix(prefixPath, string(os.PathSeparator)) {
tmpPrefixPath += string(os.PathSeparator)
}
prefixPath = tmpPrefixPath
}
// TODO: check if prefixPath starts with bucketDir
// Case #1: if prefixPath is /mnt/mys3/mybucket/2012/photos/paris, then
// dirDepth is /mnt/mys3/mybucket/2012/photos
// Case #2: if prefixPath is /mnt/mys3/mybucket/2012/photos/, then
// dirDepth is /mnt/mys3/mybucket/2012/photos
dirDepth = filepath.Dir(prefixPath)
scanDir = dirDepth
} else {
prefixPath = bucketDir
}
if markerPath != "" {
if !filepath.IsAbs(markerPath) {
tmpMarkerPath := filepath.Join(bucketDir, markerPath)
if strings.HasSuffix(markerPath, string(os.PathSeparator)) {
tmpMarkerPath += string(os.PathSeparator)
}
markerPath = tmpMarkerPath
}
// TODO: check markerPath must be a file
if uploadIDMarker != "" {
markerPath = filepath.Join(markerPath, uploadIDMarker+multipartUploadIDSuffix)
}
// TODO: check if markerPath starts with bucketDir
// TODO: check if markerPath starts with prefixPath
// Case #1: if markerPath is /mnt/mys3/mybucket/2012/photos/gophercon.png, then
// scanDir is /mnt/mys3/mybucket/2012/photos
// Case #2: if markerPath is /mnt/mys3/mybucket/2012/photos/gophercon.png/1fbd117a-268a-4ed0-85c9-8cc3888cbf20.uploadid, then
// scanDir is /mnt/mys3/mybucket/2012/photos/gophercon.png
// Case #3: if markerPath is /mnt/mys3/mybucket/2012/photos/, then
// scanDir is /mnt/mys3/mybucket/2012/photos
scanDir = filepath.Dir(markerPath)
} else {
markerPath = bucketDir
}
// Have bucketDir ends with os.PathSeparator
if !strings.HasSuffix(bucketDir, string(os.PathSeparator)) {
bucketDir += string(os.PathSeparator)
}
// Remove os.PathSeparator if scanDir ends with
if strings.HasSuffix(scanDir, string(os.PathSeparator)) {
scanDir = filepath.Dir(scanDir)
}
// goroutine - retrieves directory entries, makes ObjectInfo and sends into the channel.
go func() {
defer close(objectInfoCh)
// send function - returns true if ObjectInfo is sent
// within (time.Second * 15) else false on timeout.
send := func(oi multipartObjectInfo) bool {
timer := time.After(time.Second * 15)
select {
case objectInfoCh <- oi:
return true
case <-timer:
return false
}
}
// filter function - filters directory entries matching multipart uploadids, prefix and marker
direntFilterFn := func(dirent fsDirent) bool {
// check if dirent is a directory (or) dirent is a regular file and it's name ends with Upload ID suffix string
if dirent.IsDir() || (dirent.IsRegular() && strings.HasSuffix(dirent.name, multipartUploadIDSuffix)) {
// return if dirent's name starts with prefixPath and lexically higher than markerPath
return strings.HasPrefix(dirent.name, prefixPath) && dirent.name > markerPath
}
return false
}
// filter function - filters directory entries matching multipart uploadids
subDirentFilterFn := func(dirent fsDirent) bool {
// check if dirent is a directory (or) dirent is a regular file and it's name ends with Upload ID suffix string
return dirent.IsDir() || (dirent.IsRegular() && strings.HasSuffix(dirent.name, multipartUploadIDSuffix))
}
// lastObjInfo is used to save last object info which is sent at last with End=true
var lastObjInfo *multipartObjectInfo
sendError := func(err error) {
if lastObjInfo != nil {
if !send(*lastObjInfo) {
// as we got error sending lastObjInfo, we can't send the error
return
}
}
send(multipartObjectInfo{Err: err, End: true})
return
}
for {
dirents, err := scandir(scanDir, direntFilterFn, false)
if err != nil {
sendError(err)
return
}
var dirent fsDirent
for len(dirents) > 0 {
dirent, dirents = dirents[0], dirents[1:]
if dirent.IsRegular() {
// Handle uploadid file
name := strings.Replace(filepath.Dir(dirent.name), bucketDir, "", 1)
if name == "" {
// This should not happen ie uploadid file should not be in bucket directory
sendError(errors.New("Corrupted metadata"))
return
}
uploadID := strings.Split(filepath.Base(dirent.name), multipartUploadIDSuffix)[0]
// Solaris and older unixes have modTime to be
// empty, fallback to os.Stat() to fill missing values.
if dirent.modTime.IsZero() {
if fi, e := os.Stat(dirent.name); e == nil {
dirent.modTime = fi.ModTime()
} else {
sendError(e)
return
}
}
objInfo := multipartObjectInfo{
Name: name,
UploadID: uploadID,
ModifiedTime: dirent.modTime,
}
// as we got new object info, send last object info and keep new object info as last object info
if lastObjInfo != nil {
if !send(*lastObjInfo) {
return
}
}
lastObjInfo = &objInfo
continue
}
// Fetch sub dirents.
subDirents, err := scandir(dirent.name, subDirentFilterFn, false)
if err != nil {
sendError(err)
return
}
subDirFound := false
uploadIDDirents := []fsDirent{}
// If subDirents has a directory, then current dirent needs to be sent
for _, subdirent := range subDirents {
if subdirent.IsDir() {
subDirFound = true
if recursive {
break
}
}
if !recursive && subdirent.IsRegular() {
uploadIDDirents = append(uploadIDDirents, subdirent)
}
}
// Send directory only for non-recursive listing
if !recursive && (subDirFound || len(subDirents) == 0) {
// Solaris and older unixes have modTime to be
// empty, fallback to os.Stat() to fill missing values.
if dirent.modTime.IsZero() {
if fi, e := os.Stat(dirent.name); e == nil {
dirent.modTime = fi.ModTime()
} else {
sendError(e)
return
}
}
objInfo := multipartObjectInfo{
Name: strings.Replace(dirent.name, bucketDir, "", 1),
ModifiedTime: dirent.modTime,
IsDir: true,
}
// as we got new object info, send last object info and keep new object info as last object info
if lastObjInfo != nil {
if !send(*lastObjInfo) {
return
}
}
lastObjInfo = &objInfo
}
if recursive {
dirents = append(subDirents, dirents...)
} else {
dirents = append(uploadIDDirents, dirents...)
}
}
if !recursive {
break
}
markerPath = scanDir + string(os.PathSeparator)
if scanDir = filepath.Dir(scanDir); scanDir < dirDepth {
break
}
}
if lastObjInfo != nil {
// we got last object
lastObjInfo.End = true
if !send(*lastObjInfo) {
return
}
}
}()
return objectInfoCh
}
// multipartObjectInfo - Multipart object info
type multipartObjectInfo struct {
Name string
UploadID string
ModifiedTime time.Time
IsDir bool
Err error
End bool
}

Some files were not shown because too many files have changed in this diff Show More