Merge remote-tracking branch 'origin/master' into release

This commit is contained in:
Minio Trusted
2017-01-24 18:42:52 -08:00
501 changed files with 69348 additions and 7769 deletions

View File

@@ -21,4 +21,4 @@ after_success:
- bash <(curl -s https://codecov.io/bash)
go:
- 1.7.3
- 1.7.4

View File

@@ -71,64 +71,64 @@ verifiers: vet fmt lint cyclo spelling
vet:
@echo "Running $@:"
@GO15VENDOREXPERIMENT=1 go tool vet -all ./cmd
@GO15VENDOREXPERIMENT=1 go tool vet -all ./pkg
@GO15VENDOREXPERIMENT=1 go tool vet -shadow=true ./cmd
@GO15VENDOREXPERIMENT=1 go tool vet -shadow=true ./pkg
@go tool vet -all ./cmd
@go tool vet -all ./pkg
@go tool vet -shadow=true ./cmd
@go tool vet -shadow=true ./pkg
fmt:
@echo "Running $@:"
@GO15VENDOREXPERIMENT=1 gofmt -s -l cmd
@GO15VENDOREXPERIMENT=1 gofmt -s -l pkg
@gofmt -s -l cmd
@gofmt -s -l pkg
lint:
@echo "Running $@:"
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/cmd...
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/pkg...
@${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/cmd...
@${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/pkg...
ineffassign:
@echo "Running $@:"
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/ineffassign .
@${GOPATH}/bin/ineffassign .
cyclo:
@echo "Running $@:"
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/gocyclo -over 100 cmd
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/gocyclo -over 100 pkg
@${GOPATH}/bin/gocyclo -over 100 cmd
@${GOPATH}/bin/gocyclo -over 100 pkg
build: getdeps verifiers $(UI_ASSETS)
deadcode:
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/deadcode
@${GOPATH}/bin/deadcode
spelling:
@-GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell -error `find cmd/`
@-GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell -error `find pkg/`
@-GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell -error `find docs/`
@${GOPATH}/bin/misspell -error `find cmd/`
@${GOPATH}/bin/misspell -error `find pkg/`
@${GOPATH}/bin/misspell -error `find docs/`
test: build
@echo "Running all minio testing:"
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) github.com/minio/minio/cmd...
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) github.com/minio/minio/pkg...
@go test $(GOFLAGS) github.com/minio/minio/cmd...
@go test $(GOFLAGS) github.com/minio/minio/pkg...
coverage: build
@echo "Running all coverage for minio:"
@GO15VENDOREXPERIMENT=1 ./buildscripts/go-coverage.sh
@./buildscripts/go-coverage.sh
gomake-all: build
@echo "Installing minio:"
@GO15VENDOREXPERIMENT=1 go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
@go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
pkg-add:
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/govendor add $(PKG)
${GOPATH}/bin/govendor add $(PKG)
pkg-update:
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/govendor update $(PKG)
${GOPATH}/bin/govendor update $(PKG)
pkg-remove:
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/govendor remove $(PKG)
${GOPATH}/bin/govendor remove $(PKG)
pkg-list:
@GO15VENDOREXPERIMENT=1 $(GOPATH)/bin/govendor list
@$(GOPATH)/bin/govendor list
install: gomake-all

View File

@@ -1,4 +1,4 @@
# 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) [![Go Report Card](https://goreportcard.com/badge/minio/minio)](https://goreportcard.com/report/minio/minio) [![Docker Pulls](https://img.shields.io/docker/pulls/minio/minio.svg?maxAge=604800)](https://hub.docker.com/r/minio/minio/) [![codecov](https://codecov.io/gh/minio/minio/branch/master/graph/badge.svg)](https://codecov.io/gh/minio/minio)
# Minio Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) [![Go Report Card](https://goreportcard.com/badge/minio/minio)](https://goreportcard.com/report/minio/minio) [![Docker Pulls](https://img.shields.io/docker/pulls/minio/minio.svg?maxAge=604800)](https://hub.docker.com/r/minio/minio/) [![codecov](https://codecov.io/gh/minio/minio/branch/master/graph/badge.svg)](https://codecov.io/gh/minio/minio)
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.
@@ -6,15 +6,15 @@ Minio server is light enough to be bundled with the application stack, similar t
## Docker Container
### Stable
```sh
$ docker pull minio/minio
$ docker run -p 9000:9000 minio/minio server /export
```
docker pull minio/minio
docker run -p 9000:9000 minio/minio server /export
```
### Edge
```sh
$ docker pull minio/minio:edge
$ docker run -p 9000:9000 minio/minio:edge server /export
```
docker pull minio/minio:edge
docker run -p 9000:9000 minio/minio:edge server /export
```
Please visit Minio Docker quickstart guide for more [here](https://docs.minio.io/docs/minio-docker-quickstart-guide)
@@ -23,8 +23,8 @@ Please visit Minio Docker quickstart guide for more [here](https://docs.minio.io
Install minio packages using [Homebrew](http://brew.sh/)
```sh
$ brew install minio
$ minio server ~/Photos
brew install minio
minio server ~/Photos
```
### Binary Download
@@ -32,8 +32,8 @@ $ minio server ~/Photos
| ----------| -------- | ------|
|Apple OS X|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio|
```sh
$ chmod 755 minio
$ ./minio server ~/Photos
chmod 755 minio
./minio server ~/Photos
```
## GNU/Linux
@@ -44,8 +44,8 @@ $ ./minio server ~/Photos
||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|
```sh
$ chmod +x minio
$ ./minio server ~/Photos
chmod +x minio
./minio server ~/Photos
```
## Microsoft Windows
@@ -55,7 +55,7 @@ $ ./minio server ~/Photos
|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|
```sh
C:\Users\Username\Downloads> minio.exe server D:\Photos
minio.exe server D:\Photos
```
## FreeBSD
@@ -64,10 +64,11 @@ C:\Users\Username\Downloads> minio.exe server D:\Photos
| ----------| -------- | ------|
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio|
```sh
$ chmod 755 minio
$ ./minio server ~/Photos
chmod 755 minio
./minio server ~/Photos
```
Please visit official zfs FreeBSD guide for more details [here](https://www.freebsd.org/doc/handbook/zfs-quickstart.html)
You can run Minio on FreeBSD with FreeNAS storage-backend - see [here](https://docs.minio.io/docs/how-to-run-minio-in-freenas) for more details.
## Install from Source
@@ -75,7 +76,7 @@ Source installation is only intended for developers and advanced users. If you d
```sh
$ go get -u github.com/minio/minio
go get -u github.com/minio/minio
```

View File

@@ -1,4 +1,4 @@
# 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) [![Go Report Card](https://goreportcard.com/badge/minio/minio)](https://goreportcard.com/report/minio/minio) [![codecov](https://codecov.io/gh/minio/minio/branch/master/graph/badge.svg)](https://codecov.io/gh/minio/minio)
# Minio 快速入门 [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) [![Go Report Card](https://goreportcard.com/badge/minio/minio)](https://goreportcard.com/report/minio/minio) [![codecov](https://codecov.io/gh/minio/minio/branch/master/graph/badge.svg)](https://codecov.io/gh/minio/minio)
Minio是一个对象存储服务基于Apache License v2.0协议. 它完全兼容亚马逊的S3云储存服务非常适合于存储很多非结构化的数据例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等而一个对象文件可以是任意大小从几kb到最大5T不等。

View File

@@ -12,7 +12,6 @@ clone_folder: c:\gopath\src\github.com\minio\minio
# Environment variables
environment:
GOPATH: c:\gopath
GO15VENDOREXPERIMENT: 1
# scripts that run after cloning repository
install:
@@ -36,8 +35,8 @@ test_script:
# Unit tests
- ps: Add-AppveyorTest "Unit Tests" -Outcome Running
- mkdir build\coverage
- go test -race github.com/minio/minio/cmd...
- go test -race github.com/minio/minio/pkg...
- go test -timeout 15m -v -race github.com/minio/minio/cmd...
- go test -v -race github.com/minio/minio/pkg...
- go test -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
- ps: Update-AppveyorTest "Unit Tests" -Outcome Passed

8
browser/.babelrc Normal file
View File

@@ -0,0 +1,8 @@
{
"presets": [
"es2015",
"react"
],
"plugins": ["transform-object-rest-spread"]
}

16
browser/.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.json]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

23
browser/.esformatter Normal file
View File

@@ -0,0 +1,23 @@
{
"plugins": [
"esformatter-jsx"
],
// Copied from https://github.com/royriojas/esformatter-jsx
"jsx": {
"formatJSX": true, //Duh! that's the default
"attrsOnSameLineAsTag": false, // move each attribute to its own line
"maxAttrsOnTag": 3, // if lower or equal than 3 attributes, they will be kept on a single line
"firstAttributeOnSameLine": true, // keep the first attribute in the same line as the tag
"formatJSXExpressions": true, // default true, if false jsxExpressions won't be recursively formatted
"JSXExpressionsSingleLine": true, // default true, if false the JSXExpressions might span several lines
"alignWithFirstAttribute": false, // do not align attributes with the first tag
"spaceInJSXExpressionContainers": " ", // default to one space. Make it empty if you don't like spaces between JSXExpressionContainers
"removeSpaceBeforeClosingJSX": false, // default false. if true <React.Something /> => <React.Something/>
"closingTagOnNewLine": false, // default false. if true attributes on multiple lines will close the tag on a new line
"JSXAttributeQuotes": "", // possible values "single" or "double". Leave it as empty string if you don't want to modify the attributes' quotes
"htmlOptions": {
// put here the options for js-beautify.html
}
}
}

20
browser/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
**/*.swp
cover.out
*~
minio
!*/
site/
**/*.test
**/*.sublime-workspace
/.idea/
/Minio.iml
**/access.log
build
vendor/**/*.js
vendor/**/*.json
release
.DS_Store
*.syso
coverage.txt
node_modules
production

37
browser/README.md Normal file
View File

@@ -0,0 +1,37 @@
# Minio File Browser
``Minio Browser`` provides minimal set of UI to manage buckets and objects on ``minio`` server. ``Minio Browser`` is written in javascript and released under [Apache 2.0 License](./LICENSE).
## Installation
### Install yarn:
```sh
$ curl -o- -L https://yarnpkg.com/install.sh | bash
$ yarn
```
### Install `go-bindata` and `go-bindata-assetfs`.
If you do not have a working Golang environment, please follow [Install Golang](https://docs.minio.io/docs/how-to-install-golang)
```sh
$ go get github.com/jteeuwen/go-bindata/...
$ go get github.com/elazarl/go-bindata-assetfs/...
```
## Generating Assets.
### Generate ui-assets.go
```sh
$ yarn release
```
This generates ui-assets.go in the current direcotry. Now do `make` in the parent directory to build the minio binary with the newly generated ui-assets.go
### Run Minio Browser with live reload.
```sh
$ yarn dev
```
Open [http://localhost:8080/minio/](http://localhost:8080/minio/) in your browser to play with the application

View File

@@ -0,0 +1,98 @@
.page-load {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: #32393F;
z-index: 100;
transition: opacity 200ms;
-webkit-transition: opacity 200ms;
}
.pl-0{
opacity: 0;
}
.pl-1 {
display: none;
}
.pl-inner {
position: absolute;
width: 100px;
height: 100px;
left: 50%;
margin-left: -50px;
top: 50%;
margin-top: -50px;
text-align: center;
-webkit-animation: fade-in 500ms;
animation: fade-in 500ms;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
animation-delay: 350ms;
-webkit-animation-delay: 350ms;
-webkit-backface-visibility: visible;
backface-visibility: visible;
}
.pl-inner:before {
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
display: block;
-webkit-animation: spin 1000ms infinite linear;
animation: spin 1000ms infinite linear;
border: 1px solid rgba(255, 255, 255, 0.2);;
border-left-color: #fff;
border-radius: 50%;
}
.pl-inner > img {
width: 30px;
margin-top: 28px;
}
@-webkit-keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="139.0389584668397 284.78404581828653 12.617622649141168 6.417622649141265"><defs><path d="M139.04 290.7L144.95 284.78L145.46 285.29L139.54 291.2L139.04 290.7Z" id="NsdmgIWbGe"></path><path d="M145.24 285.29L151.15 291.2L151.66 290.7L145.74 284.78L145.24 285.29Z" id="VqPWmhvQEo"></path></defs><g visibility="inherit"><g><use xlink:href="#NsdmgIWbGe" opacity="1" fill="#000000" fill-opacity="1"></use></g><g><use xlink:href="#VqPWmhvQEo" opacity="1" fill="#000000" fill-opacity="1"></use></g></g></svg>

After

Width:  |  Height:  |  Size: 797 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
browser/app/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

57
browser/app/img/logo.svg Normal file
View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 160 256"
version="1.1"
id="svg3092"
height="218.14844"
width="137">
<defs
id="defs3094" />
<metadata
id="metadata3097">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0.99999967,-982.85149)"
id="layer1">
<g
transform="matrix(1.0112586,0,0,1.0112586,5.4603732,-13.223714)"
id="g4144">
<g
id="g4140">
<g
style="image-rendering:auto"
id="minio"
transform="matrix(1.0000023,0,0,0.99999799,-739.31646,295.2269)">
<title
id="title3337">Minio Logo</title>
<path
d="m 803.42903,801.80813 c 12.40802,4.17067 27.0499,9.11665 37.95186,12.80906 -12.01295,-21.20683 -27.84305,-34.11687 -37.95186,-40.78578 l 0,27.97672 z m 0,93.72113 -14.22303,8.96217 0,-92.45864 c -1.52985,-0.5139 -2.97948,-0.9981 -4.33405,-1.45259 -19.98593,-6.67189 -32.7207,-17.95703 -35.85168,-31.77904 -2.54577,-11.21386 1.55064,-23.02184 11.24654,-32.39691 8.84929,-8.55225 21.22761,-18.39964 31.17304,-26.31619 3.60329,-2.86658 6.73129,-5.3173 9.2028,-7.39669 2.31406,-1.93977 1.61598,-4.95488 0.57033,-6.21441 -1.74073,-2.09127 -4.61921,-1.74669 -6.56195,-0.32379 -0.10398,0.0802 -5.65595,4.40832 -5.65595,4.40832 l -8.58195,-11.57033 c 0,0 5.60843,-4.14096 5.8223,-4.30137 8.39777,-6.155 19.54034,-4.98758 25.92406,2.71509 3.19039,3.84093 4.68459,8.69779 4.20929,13.67051 -0.47232,4.9549 -2.84579,9.43153 -6.68078,12.5922 -2.58439,2.12988 -5.73912,4.64298 -9.39291,7.54522 -9.70779,7.72641 -21.78905,17.33915 -30.14226,25.41907 -6.1253,5.9233 -8.70671,12.67834 -7.26896,19.02345 1.9873,8.75424 11.33268,16.34105 26.32213,21.37911 l 0,-46.22486 c 40.29563,13.62298 68.76248,61.22321 78.20589,87.64039 0,0 -41.76308,-14.15768 -63.98286,-21.64051 l 0,78.7198"
style="fill:#f8f8f8;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path48" />
<path
d="m 803.42903,826.12513 -14.22303,-4.78261 0,-9.30973 14.22303,4.77667 0,9.31567"
style="fill:#cdccca;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path50" />
<path
d="m 734.75566,745.06155 c -0.23469,0.16636 -0.54956,0.14853 -0.73077,-0.0743 -0.17823,-0.22576 -0.13063,-0.53766 0.0802,-0.73373 4.93113,-4.51525 24.45661,-23.86844 46.30805,-45.2624 l 8.58193,11.57033 c 0,0 -53.54135,34.01288 -54.23942,34.50007"
style="fill:#f14621;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path52" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" width="16" height="4" viewBox="4 10 16 4"><defs><path d="M4 12C4 13.1 4.9 14 6 14C7.1 14 8 13.1 8 12C8 10.9 7.1 10 6 10C4.9 10 4 10.9 4 12ZM16 12C16 13.1 16.9 14 18 14C19.1 14 20 13.1 20 12C20 10.9 19.1 10 18 10C16.9 10 16 10.9 16 12ZM10 12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12C14 10.9 13.1 10 12 10C10.9 10 10 10.9 10 12Z" id="mccsKZxKL3"></path></defs><g visibility="visible"><g><use xlink:href="#mccsKZxKL3" opacity="1" fill="#eaeaea" fill-opacity="1"></use><g><use xlink:href="#mccsKZxKL3" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g></g></svg>

After

Width:  |  Height:  |  Size: 894 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 10c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2zm12 0c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2zm-6 0c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" width="9" height="9" viewBox="326.76441742035513 536.0133077721175 13 13"><defs><path d="M339.76 536.01L326.76 549.01L339.76 549.01L339.76 536.01Z" id="kt3PSf43ua"></path></defs><g visibility="visible"><g><use xlink:href="#kt3PSf43ua" opacity="1" fill="#dadada" fill-opacity="1"></use></g></g></svg>

After

Width:  |  Height:  |  Size: 586 B

56
browser/app/index.html Normal file
View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Minio Browser</title>
<link rel="stylesheet" href="/minio/loader.css" type="text/css">
</head>
<body>
<div class="page-load">
<div class="pl-inner">
<img src="/minio/logo.svg" alt="">
</div>
</div>
<div id="root"></div>
<!--[if lt IE 11]>
<div class="ie-warning">
<div class="iw-inner">
<i class="iwi-icon fa fa-warning"></i>
You are using Internet Explorer version 12.0 or lower. Due to security issues and lack of support for Web Standards it is highly recommended that you upgrade to a modern browser
<ul>
<li>
<a href="http://www.google.com/chrome/">
<img src="/minio/chrome.png" alt="">
<div>Chrome</div>
</a>
</li>
<li>
<a href="https://www.mozilla.org/en-US/firefox/new/">
<img src="/minio/firefox.png" alt="">
<div>Firefox</div>
</a>
</li>
<li>
<a href="https://www.apple.com/safari/">
<img src="/minio/safari.png" alt="">
<div>Safari</div>
</a>
</li>
</ul>
<div class="iwi-skip">Skip & Continue</div>
</div>
</div>
<![endif]-->
<script>currentUiVersion = 'MINIO_UI_VERSION'</script>
<script src="/minio/index_bundle.js"></script>
</body>
</html>

116
browser/app/index.js Normal file
View File

@@ -0,0 +1,116 @@
/*
* Minio Browser (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.
*/
import './less/main.less'
import React from 'react'
import ReactDOM from 'react-dom'
import thunkMiddleware from 'redux-thunk'
import createStore from 'redux/lib/createStore'
import applyMiddleware from 'redux/lib/applyMiddleware'
import Route from 'react-router/lib/Route'
import Router from 'react-router/lib/Router'
import browserHistory from 'react-router/lib/browserHistory'
import IndexRoute from 'react-router/lib/IndexRoute'
import Provider from 'react-redux/lib/components/Provider'
import connect from 'react-redux/lib/components/connect'
import Moment from 'moment'
import { minioBrowserPrefix } from './js/constants.js'
import * as actions from './js/actions.js'
import reducer from './js/reducers.js'
import _Login from './js/components/Login.js'
import _Browse from './js/components/Browse.js'
import fontAwesome from 'font-awesome/css/font-awesome.css'
import Web from './js/web'
window.Web = Web
import storage from 'local-storage-fallback'
const store = applyMiddleware(thunkMiddleware)(createStore)(reducer)
const Browse = connect(state => state)(_Browse)
const Login = connect(state => state)(_Login)
let web = new Web(`${window.location.protocol}//${window.location.host}${minioBrowserPrefix}/webrpc`, store.dispatch)
window.web = web
store.dispatch(actions.setWeb(web))
function authNeeded(nextState, replace, cb) {
if (web.LoggedIn()) {
return cb()
}
if (location.pathname === minioBrowserPrefix || location.pathname === minioBrowserPrefix + '/') {
replace(`${minioBrowserPrefix}/login`)
}
return cb()
}
function authNotNeeded(nextState, replace) {
if (web.LoggedIn()) {
replace(`${minioBrowserPrefix}`)
}
}
const App = (props) => {
return <div>
{ props.children }
</div>
}
ReactDOM.render((
<Provider store={ store } web={ web }>
<Router history={ browserHistory }>
<Route path='/' component={ App }>
<Route path='minio' component={ App }>
<IndexRoute component={ Browse } onEnter={ authNeeded } />
<Route path='login' component={ Login } onEnter={ authNotNeeded } />
<Route path=':bucket' component={ Browse } onEnter={ authNeeded } />
<Route path=':bucket/*' component={ Browse } onEnter={ authNeeded } />
</Route>
</Route>
</Router>
</Provider>
), document.getElementById('root'))
//Page loader
let delay = [0, 400]
let i = 0
function handleLoader() {
if (i < 2) {
setTimeout(function() {
document.querySelector('.page-load').classList.add('pl-' + i)
i++
handleLoader()
}, delay[i])
}
}
handleLoader()
if (storage.getItem('newlyUpdated')) {
store.dispatch(actions.showAlert({
type: 'success',
message: "Updated to the latest UI Version."
}))
storage.removeItem('newlyUpdated')
}

View File

@@ -0,0 +1,43 @@
/*
* Minio Browser (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.
*/
import expect from 'expect';
import JSONrpc from '../jsonrpc';
describe('jsonrpc', () => {
it('should fail with invalid endpoint', (done) => {
try {
let jsonRPC = new JSONrpc({
endpoint: 'htt://localhost:9000',
namespace: 'Test'
});
} catch (e) {
done();
}
});
it('should succeed with valid endpoint', () => {
let jsonRPC = new JSONrpc({
endpoint: 'http://localhost:9000/webrpc',
namespace: 'Test'
});
expect(jsonRPC.version).toEqual('2.0');
expect(jsonRPC.host).toEqual('localhost');
expect(jsonRPC.port).toEqual('9000');
expect(jsonRPC.path).toEqual('/webrpc');
expect(jsonRPC.scheme).toEqual('http');
});
});

514
browser/app/js/actions.js Normal file
View File

@@ -0,0 +1,514 @@
/*
* Minio Browser (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.
*/
import url from 'url'
import Moment from 'moment'
import browserHistory from 'react-router/lib/browserHistory'
import web from './web'
import * as utils from './utils'
import storage from 'local-storage-fallback'
import { minioBrowserPrefix } from './constants'
export const SET_WEB = 'SET_WEB'
export const SET_CURRENT_BUCKET = 'SET_CURRENT_BUCKET'
export const SET_CURRENT_PATH = 'SET_CURRENT_PATH'
export const SET_BUCKETS = 'SET_BUCKETS'
export const ADD_BUCKET = 'ADD_BUCKET'
export const ADD_OBJECT = 'ADD_OBJECT'
export const SET_VISIBLE_BUCKETS = 'SET_VISIBLE_BUCKETS'
export const SET_OBJECTS = 'SET_OBJECTS'
export const SET_STORAGE_INFO = 'SET_STORAGE_INFO'
export const SET_SERVER_INFO = 'SET_SERVER_INFO'
export const SHOW_MAKEBUCKET_MODAL = 'SHOW_MAKEBUCKET_MODAL'
export const ADD_UPLOAD = 'ADD_UPLOAD'
export const STOP_UPLOAD = 'STOP_UPLOAD'
export const UPLOAD_PROGRESS = 'UPLOAD_PROGRESS'
export const SET_ALERT = 'SET_ALERT'
export const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR'
export const SET_SHOW_ABORT_MODAL = 'SET_SHOW_ABORT_MODAL'
export const SHOW_ABOUT = 'SHOW_ABOUT'
export const SET_SORT_NAME_ORDER = 'SET_SORT_NAME_ORDER'
export const SET_SORT_SIZE_ORDER = 'SET_SORT_SIZE_ORDER'
export const SET_SORT_DATE_ORDER = 'SET_SORT_DATE_ORDER'
export const SET_LATEST_UI_VERSION = 'SET_LATEST_UI_VERSION'
export const SET_SIDEBAR_STATUS = 'SET_SIDEBAR_STATUS'
export const SET_LOGIN_REDIRECT_PATH = 'SET_LOGIN_REDIRECT_PATH'
export const SET_LOAD_BUCKET = 'SET_LOAD_BUCKET'
export const SET_LOAD_PATH = 'SET_LOAD_PATH'
export const SHOW_SETTINGS = 'SHOW_SETTINGS'
export const SET_SETTINGS = 'SET_SETTINGS'
export const SHOW_BUCKET_POLICY = 'SHOW_BUCKET_POLICY'
export const SET_POLICIES = 'SET_POLICIES'
export const SET_SHARE_OBJECT = 'SET_SHARE_OBJECT'
export const DELETE_CONFIRMATION = 'DELETE_CONFIRMATION'
export const SET_PREFIX_WRITABLE = 'SET_PREFIX_WRITABLE'
export const showDeleteConfirmation = (object) => {
return {
type: DELETE_CONFIRMATION,
payload: {
object,
show: true
}
}
}
export const hideDeleteConfirmation = () => {
return {
type: DELETE_CONFIRMATION,
payload: {
object: '',
show: false
}
}
}
export const showShareObject = url => {
return {
type: SET_SHARE_OBJECT,
shareObject: {
url: url,
show: true
}
}
}
export const hideShareObject = () => {
return {
type: SET_SHARE_OBJECT,
shareObject: {
url: '',
show: false
}
}
}
export const shareObject = (object, expiry) => (dispatch, getState) => {
const {currentBucket, web} = getState()
let host = location.host
let bucket = currentBucket
if (!web.LoggedIn()) {
dispatch(showShareObject(`${host}/${bucket}/${object}`))
return
}
web.PresignedGet({
host,
bucket,
object,
expiry
})
.then(obj => {
dispatch(showShareObject(obj.url))
})
.catch(err => {
dispatch(showAlert({
type: 'danger',
message: err.message
}))
})
}
export const setLoginRedirectPath = (path) => {
return {
type: SET_LOGIN_REDIRECT_PATH,
path
}
}
export const setLoadPath = (loadPath) => {
return {
type: SET_LOAD_PATH,
loadPath
}
}
export const setLoadBucket = (loadBucket) => {
return {
type: SET_LOAD_BUCKET,
loadBucket
}
}
export const setWeb = web => {
return {
type: SET_WEB,
web
}
}
export const setBuckets = buckets => {
return {
type: SET_BUCKETS,
buckets
}
}
export const addBucket = bucket => {
return {
type: ADD_BUCKET,
bucket
}
}
export const showMakeBucketModal = () => {
return {
type: SHOW_MAKEBUCKET_MODAL,
showMakeBucketModal: true
}
}
export const hideAlert = () => {
return {
type: SET_ALERT,
alert: {
show: false,
message: '',
type: ''
}
}
}
export const showAlert = alert => {
return (dispatch, getState) => {
let alertTimeout = null
if (alert.type !== 'danger') {
alertTimeout = setTimeout(() => {
dispatch({
type: SET_ALERT,
alert: {
show: false
}
})
}, 5000)
}
dispatch({
type: SET_ALERT,
alert: Object.assign({}, alert, {
show: true,
alertTimeout
})
})
}
}
export const setSidebarStatus = (status) => {
return {
type: SET_SIDEBAR_STATUS,
sidebarStatus: status
}
}
export const hideMakeBucketModal = () => {
return {
type: SHOW_MAKEBUCKET_MODAL,
showMakeBucketModal: false
}
}
export const setVisibleBuckets = visibleBuckets => {
return {
type: SET_VISIBLE_BUCKETS,
visibleBuckets
}
}
export const setObjects = (objects) => {
return {
type: SET_OBJECTS,
objects
}
}
export const setCurrentBucket = currentBucket => {
return {
type: SET_CURRENT_BUCKET,
currentBucket
}
}
export const setCurrentPath = currentPath => {
return {
type: SET_CURRENT_PATH,
currentPath
}
}
export const setStorageInfo = storageInfo => {
return {
type: SET_STORAGE_INFO,
storageInfo
}
}
export const setServerInfo = serverInfo => {
return {
type: SET_SERVER_INFO,
serverInfo
}
}
const setPrefixWritable = prefixWritable => {
return {
type: SET_PREFIX_WRITABLE,
prefixWritable,
}
}
export const selectBucket = (newCurrentBucket, prefix) => {
if (!prefix)
prefix = ''
return (dispatch, getState) => {
let web = getState().web
let currentBucket = getState().currentBucket
if (currentBucket !== newCurrentBucket) dispatch(setLoadBucket(newCurrentBucket))
dispatch(setCurrentBucket(newCurrentBucket))
dispatch(selectPrefix(prefix))
return
}
}
export const selectPrefix = prefix => {
return (dispatch, getState) => {
const {currentBucket, web} = getState()
dispatch(setLoadPath(prefix))
web.ListObjects({
bucketName: currentBucket,
prefix
})
.then(res => {
let objects = res.objects
if (!objects)
objects = []
dispatch(setObjects(
utils.sortObjectsByName(objects.map(object => {
object.name = object.name.replace(`${prefix}`, ''); return object
}))
))
dispatch(setPrefixWritable(res.writable))
dispatch(setSortNameOrder(false))
dispatch(setCurrentPath(prefix))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
})
.catch(err => {
dispatch(showAlert({
type: 'danger',
message: err.message
}))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
// Use browserHistory.replace instead of push so that browser back button works fine.
browserHistory.replace(`${minioBrowserPrefix}/login`)
})
}
}
export const addUpload = options => {
return {
type: ADD_UPLOAD,
slug: options.slug,
size: options.size,
xhr: options.xhr,
name: options.name
}
}
export const stopUpload = options => {
return {
type: STOP_UPLOAD,
slug: options.slug
}
}
export const uploadProgress = options => {
return {
type: UPLOAD_PROGRESS,
slug: options.slug,
loaded: options.loaded
}
}
export const setShowAbortModal = showAbortModal => {
return {
type: SET_SHOW_ABORT_MODAL,
showAbortModal
}
}
export const setLoginError = () => {
return {
type: SET_LOGIN_ERROR,
loginError: true
}
}
export const uploadFile = (file, xhr) => {
return (dispatch, getState) => {
const {currentBucket, currentPath} = getState()
const objectName = `${currentPath}${file.name}`
const uploadUrl = `${window.location.origin}/minio/upload/${currentBucket}/${objectName}`
// The slug is a unique identifer for the file upload.
const slug = `${currentBucket}-${currentPath}-${file.name}`
xhr.open('PUT', uploadUrl, true)
xhr.withCredentials = false
const token = storage.getItem('token')
if (token) xhr.setRequestHeader("Authorization", 'Bearer ' + storage.getItem('token'))
xhr.setRequestHeader('x-amz-date', Moment().utc().format('YYYYMMDDTHHmmss') + 'Z')
dispatch(addUpload({
slug,
xhr,
size: file.size,
name: file.name
}))
xhr.onload = function(event) {
if (xhr.status == 401 || xhr.status == 403 || xhr.status == 500) {
setShowAbortModal(false)
dispatch(stopUpload({
slug
}))
dispatch(showAlert({
type: 'danger',
message: 'Unauthorized request.'
}))
}
if (xhr.status == 200) {
setShowAbortModal(false)
dispatch(stopUpload({
slug
}))
dispatch(showAlert({
type: 'success',
message: 'File \'' + file.name + '\' uploaded successfully.'
}))
dispatch(selectPrefix(currentPath))
}
}
xhr.upload.addEventListener('error', event => {
dispatch(showAlert({
type: 'danger',
message: 'Error occurred uploading \'' + file.name + '\'.'
}))
dispatch(stopUpload({
slug
}))
})
xhr.upload.addEventListener('progress', event => {
if (event.lengthComputable) {
let loaded = event.loaded
let total = event.total
// Update the counter.
dispatch(uploadProgress({
slug,
loaded
}))
}
})
xhr.send(file)
}
}
export const showAbout = () => {
return {
type: SHOW_ABOUT,
showAbout: true
}
}
export const hideAbout = () => {
return {
type: SHOW_ABOUT,
showAbout: false
}
}
export const setSortNameOrder = (sortNameOrder) => {
return {
type: SET_SORT_NAME_ORDER,
sortNameOrder
}
}
export const setSortSizeOrder = (sortSizeOrder) => {
return {
type: SET_SORT_SIZE_ORDER,
sortSizeOrder
}
}
export const setSortDateOrder = (sortDateOrder) => {
return {
type: SET_SORT_DATE_ORDER,
sortDateOrder
}
}
export const setLatestUIVersion = (latestUiVersion) => {
return {
type: SET_LATEST_UI_VERSION,
latestUiVersion
}
}
export const showSettings = () => {
return {
type: SHOW_SETTINGS,
showSettings: true
}
}
export const hideSettings = () => {
return {
type: SHOW_SETTINGS,
showSettings: false
}
}
export const setSettings = (settings) => {
return {
type: SET_SETTINGS,
settings
}
}
export const showBucketPolicy = () => {
return {
type: SHOW_BUCKET_POLICY,
showBucketPolicy: true
}
}
export const hideBucketPolicy = () => {
return {
type: SHOW_BUCKET_POLICY,
showBucketPolicy: false
}
}
export const setPolicies = (policies) => {
return {
type: SET_POLICIES,
policies
}
}

View File

@@ -0,0 +1,734 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import classNames from 'classnames'
import browserHistory from 'react-router/lib/browserHistory'
import humanize from 'humanize'
import Moment from 'moment'
import Modal from 'react-bootstrap/lib/Modal'
import ModalBody from 'react-bootstrap/lib/ModalBody'
import ModalHeader from 'react-bootstrap/lib/ModalHeader'
import Alert from 'react-bootstrap/lib/Alert'
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
import Tooltip from 'react-bootstrap/lib/Tooltip'
import Dropdown from 'react-bootstrap/lib/Dropdown'
import MenuItem from 'react-bootstrap/lib/MenuItem'
import InputGroup from '../components/InputGroup'
import Dropzone from '../components/Dropzone'
import ObjectsList from '../components/ObjectsList'
import SideBar from '../components/SideBar'
import Path from '../components/Path'
import BrowserUpdate from '../components/BrowserUpdate'
import UploadModal from '../components/UploadModal'
import SettingsModal from '../components/SettingsModal'
import PolicyInput from '../components/PolicyInput'
import Policy from '../components/Policy'
import BrowserDropdown from '../components/BrowserDropdown'
import ConfirmModal from './ConfirmModal'
import logo from '../../img/logo.svg'
import * as actions from '../actions'
import * as utils from '../utils'
import * as mime from '../mime'
import { minioBrowserPrefix } from '../constants'
import CopyToClipboard from 'react-copy-to-clipboard'
import storage from 'local-storage-fallback'
export default class Browse extends React.Component {
componentDidMount() {
const {web, dispatch, currentBucket} = this.props
if (!web.LoggedIn()) return
web.StorageInfo()
.then(res => {
let storageInfo = Object.assign({}, {
total: res.storageInfo.Total,
free: res.storageInfo.Free
})
storageInfo.used = storageInfo.total - storageInfo.free
dispatch(actions.setStorageInfo(storageInfo))
return web.ServerInfo()
})
.then(res => {
let serverInfo = Object.assign({}, {
version: res.MinioVersion,
memory: res.MinioMemory,
platform: res.MinioPlatform,
runtime: res.MinioRuntime,
envVars: res.MinioEnvVars
})
dispatch(actions.setServerInfo(serverInfo))
})
.catch(err => {
dispatch(actions.showAlert({
type: 'danger',
message: err.message
}))
})
}
componentWillMount() {
const {dispatch} = this.props
// Clear out any stale message in the alert of Login page
dispatch(actions.showAlert({
type: 'danger',
message: ''
}))
if (web.LoggedIn()) {
web.ListBuckets()
.then(res => {
let buckets
if (!res.buckets)
buckets = []
else
buckets = res.buckets.map(bucket => bucket.name)
if (buckets.length) {
dispatch(actions.setBuckets(buckets))
dispatch(actions.setVisibleBuckets(buckets))
if (location.pathname === minioBrowserPrefix || location.pathname === minioBrowserPrefix + '/') {
browserHistory.push(utils.pathJoin(buckets[0]))
}
}
})
}
this.history = browserHistory.listen(({pathname}) => {
let decPathname = decodeURI(pathname)
if (decPathname === `${minioBrowserPrefix}/login`) return // FIXME: better organize routes and remove this
if (!decPathname.endsWith('/'))
decPathname += '/'
if (decPathname === minioBrowserPrefix + '/') {
dispatch(actions.setCurrentBucket(''))
dispatch(actions.setCurrentPath(''))
dispatch(actions.setObjects([]))
return
}
let obj = utils.pathSlice(decPathname)
if (!web.LoggedIn()) {
dispatch(actions.setBuckets([obj.bucket]))
dispatch(actions.setVisibleBuckets([obj.bucket]))
}
dispatch(actions.selectBucket(obj.bucket, obj.prefix))
})
}
componentWillUnmount() {
this.history()
}
selectBucket(e, bucket) {
e.preventDefault()
if (bucket === this.props.currentBucket) return
browserHistory.push(utils.pathJoin(bucket))
}
searchBuckets(e) {
e.preventDefault()
let {buckets} = this.props
this.props.dispatch(actions.setVisibleBuckets(buckets.filter(bucket => bucket.indexOf(e.target.value) > -1)))
}
selectPrefix(e, prefix) {
e.preventDefault()
const {dispatch, currentPath, web, currentBucket} = this.props
const encPrefix = encodeURI(prefix)
if (prefix.endsWith('/') || prefix === '') {
if (prefix === currentPath) return
browserHistory.push(utils.pathJoin(currentBucket, encPrefix))
} else {
window.location = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=${storage.getItem('token')}`
}
}
makeBucket(e) {
e.preventDefault()
const bucketName = this.refs.makeBucketRef.value
this.refs.makeBucketRef.value = ''
const {web, dispatch} = this.props
this.hideMakeBucketModal()
web.MakeBucket({
bucketName
})
.then(() => {
dispatch(actions.addBucket(bucketName))
dispatch(actions.selectBucket(bucketName))
})
.catch(err => dispatch(actions.showAlert({
type: 'danger',
message: err.message
})))
}
hideMakeBucketModal() {
const {dispatch} = this.props
dispatch(actions.hideMakeBucketModal())
}
showMakeBucketModal(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.showMakeBucketModal())
}
showAbout(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.showAbout())
}
hideAbout(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.hideAbout())
}
showBucketPolicy(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.showBucketPolicy())
}
hideBucketPolicy(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.hideBucketPolicy())
}
uploadFile(e) {
e.preventDefault()
const {dispatch, buckets} = this.props
if (buckets.length === 0) {
dispatch(actions.showAlert({
type: 'danger',
message: "Bucket needs to be created before trying to upload files."
}))
return
}
let file = e.target.files[0]
e.target.value = null
this.xhr = new XMLHttpRequest()
dispatch(actions.uploadFile(file, this.xhr))
}
removeObject() {
const {web, dispatch, currentPath, currentBucket, deleteConfirmation} = this.props
web.RemoveObject({
bucketName: currentBucket,
objectName: deleteConfirmation.object
})
.then(() => {
this.hideDeleteConfirmation()
dispatch(actions.selectPrefix(currentPath))
})
.catch(e => dispatch(actions.showAlert({
type: 'danger',
message: e.message
})))
}
hideAlert(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.hideAlert())
}
showDeleteConfirmation(e, object) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.showDeleteConfirmation(object))
}
hideDeleteConfirmation() {
const {dispatch} = this.props
dispatch(actions.hideDeleteConfirmation())
}
shareObject(e, object) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.shareObject(object))
}
hideShareObjectModal() {
const {dispatch} = this.props
dispatch(actions.hideShareObject())
}
dataType(name, contentType) {
return mime.getDataType(name, contentType)
}
sortObjectsByName(e) {
const {dispatch, objects, sortNameOrder} = this.props
dispatch(actions.setObjects(utils.sortObjectsByName(objects, !sortNameOrder)))
dispatch(actions.setSortNameOrder(!sortNameOrder))
}
sortObjectsBySize() {
const {dispatch, objects, sortSizeOrder} = this.props
dispatch(actions.setObjects(utils.sortObjectsBySize(objects, !sortSizeOrder)))
dispatch(actions.setSortSizeOrder(!sortSizeOrder))
}
sortObjectsByDate() {
const {dispatch, objects, sortDateOrder} = this.props
dispatch(actions.setObjects(utils.sortObjectsByDate(objects, !sortDateOrder)))
dispatch(actions.setSortDateOrder(!sortDateOrder))
}
logout(e) {
const {web} = this.props
e.preventDefault()
web.Logout()
browserHistory.push(`${minioBrowserPrefix}/login`)
}
landingPage(e) {
e.preventDefault()
this.props.dispatch(actions.selectBucket(this.props.buckets[0]))
}
fullScreen(e) {
e.preventDefault()
let el = document.documentElement
if (el.requestFullscreen) {
el.requestFullscreen()
}
if (el.mozRequestFullScreen) {
el.mozRequestFullScreen()
}
if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen()
}
if (el.msRequestFullscreen) {
el.msRequestFullscreen()
}
}
toggleSidebar(status) {
this.props.dispatch(actions.setSidebarStatus(status))
}
hideSidebar(event) {
let e = event || window.event;
// Support all browsers.
let target = e.srcElement || e.target;
if (target.nodeType === 3) // Safari support.
target = target.parentNode;
let targetID = target.id;
if (!(targetID === 'feh-trigger')) {
this.props.dispatch(actions.setSidebarStatus(false))
}
}
showSettings(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.showSettings())
}
showMessage() {
const {dispatch} = this.props
dispatch(actions.showAlert({
type: 'success',
message: 'Link copied to clipboard!'
}))
this.hideShareObjectModal()
}
selectTexts() {
this.refs.copyTextInput.select()
}
handleExpireValue(targetInput, inc) {
inc === -1 ? this.refs[targetInput].stepDown(1) : this.refs[targetInput].stepUp(1)
if (this.refs.expireDays.value == 7) {
this.refs.expireHours.value = 0
this.refs.expireMins.value = 0
}
}
render() {
const {total, free} = this.props.storageInfo
const {showMakeBucketModal, alert, sortNameOrder, sortSizeOrder, sortDateOrder, showAbout, showBucketPolicy} = this.props
const {version, memory, platform, runtime} = this.props.serverInfo
const {sidebarStatus} = this.props
const {showSettings} = this.props
const {policies, currentBucket, currentPath} = this.props
const {deleteConfirmation} = this.props
const {shareObject} = this.props
const {web, prefixWritable} = this.props
// Don't always show the SettingsModal. This is done here instead of in
// SettingsModal.js so as to allow for #componentWillMount to handle
// the loading of the settings.
let settingsModal = showSettings ? <SettingsModal /> : <noscript></noscript>
let alertBox = <Alert className={ classNames({
'alert': true,
'animated': true,
'fadeInDown': alert.show,
'fadeOutUp': !alert.show
}) } bsStyle={ alert.type } onDismiss={ this.hideAlert.bind(this) }>
<div className='text-center'>
{ alert.message }
</div>
</Alert>
// Make sure you don't show a fading out alert box on the initial web-page load.
if (!alert.message)
alertBox = ''
let signoutTooltip = <Tooltip id="tt-sign-out">
Sign out
</Tooltip>
let uploadTooltip = <Tooltip id="tt-upload-file">
Upload file
</Tooltip>
let makeBucketTooltip = <Tooltip id="tt-create-bucket">
Create bucket
</Tooltip>
let loginButton = ''
let browserDropdownButton = ''
let storageUsageDetails = ''
let used = total - free
let usedPercent = (used / total) * 100 + '%'
let freePercent = free * 100 / total
if (web.LoggedIn()) {
browserDropdownButton = <BrowserDropdown fullScreen={ this.fullScreen.bind(this) }
showAbout={ this.showAbout.bind(this) }
showSettings={ this.showSettings.bind(this) }
logout={ this.logout.bind(this) } />
} else {
loginButton = <a className='btn btn-danger' href='/minio/login'>Login</a>
}
if (web.LoggedIn()) {
storageUsageDetails = <div className="feh-usage">
<div className="fehu-chart">
<div style={ { width: usedPercent } }></div>
</div>
<ul>
<li>
Used:
{ humanize.filesize(total - free) }
</li>
<li className="pull-right">
Free:
{ humanize.filesize(total - used) }
</li>
</ul>
</div>
}
let createButton = ''
if (web.LoggedIn()) {
createButton = <Dropdown dropup className="feb-actions" id="fe-action-toggle">
<Dropdown.Toggle noCaret className="feba-toggle">
<span><i className="fa fa-plus"></i></span>
</Dropdown.Toggle>
<Dropdown.Menu>
<OverlayTrigger placement="left" overlay={ uploadTooltip }>
<a href="#" className="feba-btn feba-upload">
<input type="file"
onChange={ this.uploadFile.bind(this) }
style={ { display: 'none' } }
id="file-input"></input>
<label htmlFor="file-input"> <i className="fa fa-cloud-upload"></i> </label>
</a>
</OverlayTrigger>
<OverlayTrigger placement="left" overlay={ makeBucketTooltip }>
<a href="#" className="feba-btn feba-bucket" onClick={ this.showMakeBucketModal.bind(this) }><i className="fa fa-hdd-o"></i></a>
</OverlayTrigger>
</Dropdown.Menu>
</Dropdown>
} else {
if (prefixWritable)
createButton = <Dropdown dropup className="feb-actions" id="fe-action-toggle">
<Dropdown.Toggle noCaret className="feba-toggle">
<span><i className="fa fa-plus"></i></span>
</Dropdown.Toggle>
<Dropdown.Menu>
<OverlayTrigger placement="left" overlay={ uploadTooltip }>
<a href="#" className="feba-btn feba-upload">
<input type="file"
onChange={ this.uploadFile.bind(this) }
style={ { display: 'none' } }
id="file-input"></input>
<label htmlFor="file-input"> <i className="fa fa-cloud-upload"></i> </label>
</a>
</OverlayTrigger>
</Dropdown.Menu>
</Dropdown>
}
return (
<div className={ classNames({
'file-explorer': true,
'toggled': sidebarStatus
}) }>
<SideBar landingPage={ this.landingPage.bind(this) }
searchBuckets={ this.searchBuckets.bind(this) }
selectBucket={ this.selectBucket.bind(this) }
clickOutside={ this.hideSidebar.bind(this) }
showPolicy={ this.showBucketPolicy.bind(this) } />
<div className="fe-body">
<Dropzone>
{ alertBox }
<header className="fe-header-mobile hidden-lg hidden-md">
<div id="feh-trigger" className={ 'feh-trigger ' + (classNames({
'feht-toggled': sidebarStatus
})) } onClick={ this.toggleSidebar.bind(this, !sidebarStatus) }>
<div className="feht-lines">
<div className="top"></div>
<div className="center"></div>
<div className="bottom"></div>
</div>
</div>
<img className="mh-logo" src={ logo } alt="" />
</header>
<header className="fe-header">
<Path selectPrefix={ this.selectPrefix.bind(this) } />
{ storageUsageDetails }
<ul className="feh-actions">
<BrowserUpdate />
{ loginButton }
{ browserDropdownButton }
</ul>
</header>
<div className="feb-container">
<header className="fesl-row" data-type="folder">
<div className="fesl-item fi-name" onClick={ this.sortObjectsByName.bind(this) } data-sort="name">
Name
<i className={ classNames({
'fesli-sort': true,
'fa': true,
'fa-sort-alpha-desc': sortNameOrder,
'fa-sort-alpha-asc': !sortNameOrder
}) } />
</div>
<div className="fesl-item fi-size" onClick={ this.sortObjectsBySize.bind(this) } data-sort="size">
Size
<i className={ classNames({
'fesli-sort': true,
'fa': true,
'fa-sort-amount-desc': sortSizeOrder,
'fa-sort-amount-asc': !sortSizeOrder
}) } />
</div>
<div className="fesl-item fi-modified" onClick={ this.sortObjectsByDate.bind(this) } data-sort="last-modified">
Last Modified
<i className={ classNames({
'fesli-sort': true,
'fa': true,
'fa-sort-numeric-desc': sortDateOrder,
'fa-sort-numeric-asc': !sortDateOrder
}) } />
</div>
<div className="fesl-item fi-actions"></div>
</header>
</div>
<div className="feb-container">
<ObjectsList dataType={ this.dataType.bind(this) }
selectPrefix={ this.selectPrefix.bind(this) }
showDeleteConfirmation={ this.showDeleteConfirmation.bind(this) }
shareObject={ this.shareObject.bind(this) } />
</div>
<UploadModal />
{ createButton }
<Modal className="modal-create-bucket"
bsSize="small"
animation={ false }
show={ showMakeBucketModal }
onHide={ this.hideMakeBucketModal.bind(this) }>
<button className="close close-alt" onClick={ this.hideMakeBucketModal.bind(this) }>
<span>×</span>
</button>
<ModalBody>
<form onSubmit={ this.makeBucket.bind(this) }>
<div className="input-group">
<input className="ig-text"
type="text"
ref="makeBucketRef"
placeholder="Bucket Name"
autoFocus/>
<i className="ig-helpers"></i>
</div>
</form>
</ModalBody>
</Modal>
<Modal className="modal-about modal-dark"
animation={ false }
show={ showAbout }
onHide={ this.hideAbout.bind(this) }>
<button className="close" onClick={ this.hideAbout.bind(this) }>
<span>×</span>
</button>
<div className="ma-inner">
<div className="mai-item hidden-xs">
<a href="https://minio.io" target="_blank"><img className="maii-logo" src={ logo } alt="" /></a>
</div>
<div className="mai-item">
<ul className="maii-list">
<li>
<div>
Version
</div>
<small>{ version }</small>
</li>
<li>
<div>
Memory
</div>
<small>{ memory }</small>
</li>
<li>
<div>
Platform
</div>
<small>{ platform }</small>
</li>
<li>
<div>
Runtime
</div>
<small>{ runtime }</small>
</li>
</ul>
</div>
</div>
</Modal>
<Modal className="modal-policy"
animation={ false }
show={ showBucketPolicy }
onHide={ this.hideBucketPolicy.bind(this) }>
<ModalHeader>
Bucket Policy (
{ currentBucket })
<button className="close close-alt" onClick={ this.hideBucketPolicy.bind(this) }>
<span>×</span>
</button>
</ModalHeader>
<div className="pm-body">
<PolicyInput bucket={ currentBucket } />
{ policies.map((policy, i) => <Policy key={ i } prefix={ policy.prefix } policy={ policy.policy } />
) }
</div>
</Modal>
<ConfirmModal show={ deleteConfirmation.show }
icon='fa fa-exclamation-triangle mci-red'
text='Are you sure you want to delete?'
sub='This cannot be undone!'
okText='Delete'
cancelText='Cancel'
okHandler={ this.removeObject.bind(this) }
cancelHandler={ this.hideDeleteConfirmation.bind(this) }>
</ConfirmModal>
<Modal show={ shareObject.show }
animation={ false }
onHide={ this.hideShareObjectModal.bind(this) }
bsSize="small">
<ModalHeader>
Share Object
</ModalHeader>
<ModalBody>
<div className="input-group copy-text">
<label>
Shareable Link
</label>
<input type="text"
ref="copyTextInput"
readOnly="readOnly"
value={ window.location.protocol + '//' + shareObject.url }
onClick={ this.selectTexts.bind(this) } />
</div>
<div className="input-group" style={ { display: web.LoggedIn() ? 'block' : 'none' } }>
<label>
Expires in
</label>
<div className="set-expire">
<div className="set-expire-item">
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireDays', 1) }></i>
<div className="set-expire-title">
Days
</div>
<div className="set-expire-value">
<input ref="expireDays"
type="number"
min={ 0 }
max={ 7 }
defaultValue={ 0 } />
</div>
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireDays', -1) }></i>
</div>
<div className="set-expire-item">
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireHours', 1) }></i>
<div className="set-expire-title">
Hours
</div>
<div className="set-expire-value">
<input ref="expireHours"
type="number"
min={ 0 }
max={ 24 }
defaultValue={ 0 } />
</div>
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireHours', -1) }></i>
</div>
<div className="set-expire-item">
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireMins', 1) }></i>
<div className="set-expire-title">
Minutes
</div>
<div className="set-expire-value">
<input ref="expireMins"
type="number"
min={ 1 }
max={ 60 }
defaultValue={ 45 } />
</div>
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireMins', -1) }></i>
</div>
</div>
</div>
</ModalBody>
<div className="modal-footer">
<CopyToClipboard text={ shareObject.url } onCopy={ this.showMessage.bind(this) }>
<button className="btn btn-success">
Copy Link
</button>
</CopyToClipboard>
<button className="btn btn-link" onClick={ this.hideShareObjectModal.bind(this) }>
Cancel
</button>
</div>
</Modal>
{ settingsModal }
</Dropzone>
</div>
</div>
)
}
}

View File

@@ -0,0 +1,56 @@
/*
* Minio Browser (C) 2016, 2017 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.
*/
import React from 'react'
import connect from 'react-redux/lib/components/connect'
import Dropdown from 'react-bootstrap/lib/Dropdown'
let BrowserDropdown = ({fullScreen, showAbout, showSettings, logout}) => {
return (
<li>
<Dropdown pullRight id="top-right-menu">
<Dropdown.Toggle noCaret>
<i className="fa fa-reorder"></i>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
<li>
<a target="_blank" href="https://github.com/minio/miniobrowser">Github <i className="fa fa-github"></i></a>
</li>
<li>
<a href="" onClick={ fullScreen }>Fullscreen <i className="fa fa-expand"></i></a>
</li>
<li>
<a target="_blank" href="https://docs.minio.io/">Documentation <i className="fa fa-book"></i></a>
</li>
<li>
<a target="_blank" href="https://slack.minio.io">Ask for help <i className="fa fa-question-circle"></i></a>
</li>
<li>
<a href="" onClick={ showAbout }>About <i className="fa fa-info-circle"></i></a>
</li>
<li>
<a href="" onClick={ showSettings }>Settings <i className="fa fa-cog"></i></a>
</li>
<li>
<a href="" onClick={ logout }>Sign Out <i className="fa fa-sign-out"></i></a>
</li>
</Dropdown.Menu>
</Dropdown>
</li>
)
}
export default connect(state => state)(BrowserDropdown)

View File

@@ -0,0 +1,42 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import connect from 'react-redux/lib/components/connect'
import Tooltip from 'react-bootstrap/lib/Tooltip'
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
let BrowserUpdate = ({latestUiVersion}) => {
// Don't show an update if we're already updated!
if (latestUiVersion === currentUiVersion) return ( <noscript></noscript> )
return (
<li className="hidden-xs hidden-sm">
<a href="">
<OverlayTrigger placement="left" overlay={ <Tooltip id="tt-version-update">
New update available. Click to refresh.
</Tooltip> }> <i className="fa fa-refresh"></i> </OverlayTrigger>
</a>
</li>
)
}
export default connect(state => {
return {
latestUiVersion: state.latestUiVersion
}
})(BrowserUpdate)

View File

@@ -0,0 +1,50 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import Modal from 'react-bootstrap/lib/Modal'
import ModalBody from 'react-bootstrap/lib/ModalBody'
let ConfirmModal = ({baseClass, icon, text, sub, okText, cancelText, okHandler, cancelHandler, show}) => {
return (
<Modal bsSize="small"
animation={ false }
show={ show }
className={ "modal-confirm " + (baseClass || '') }>
<ModalBody>
<div className="mc-icon">
<i className={ icon }></i>
</div>
<div className="mc-text">
{ text }
</div>
<div className="mc-sub">
{ sub }
</div>
</ModalBody>
<div className="modal-footer">
<button className="btn btn-danger" onClick={ okHandler }>
{ okText }
</button>
<button className="btn btn-link" onClick={ cancelHandler }>
{ cancelText }
</button>
</div>
</Modal>
)
}
export default ConfirmModal

View File

@@ -0,0 +1,65 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import ReactDropzone from 'react-dropzone'
import * as actions from '../actions'
// Dropzone is a drag-and-drop element for uploading files. It will create a
// landing zone of sorts that automatically receives the files.
export default class Dropzone extends React.Component {
onDrop(files) {
// FIXME: Currently you can upload multiple files, but only one abort
// modal will be shown, and progress updates will only occur for one
// file at a time. See #171.
files.forEach(file => {
let req = new XMLHttpRequest()
// Dispatch the upload.
web.dispatch(actions.uploadFile(file, req))
})
}
render() {
// Overwrite the default styling from react-dropzone; otherwise it
// won't handle child elements correctly.
const style = {
height: '100%',
borderWidth: '2px',
borderStyle: 'dashed',
borderColor: '#fff'
}
const activeStyle = {
borderColor: '#777'
}
const rejectStyle = {
backgroundColor: '#ffdddd'
}
// disableClick means that it won't trigger a file upload box when
// the user clicks on a file.
return (
<ReactDropzone style={ style }
activeStyle={ activeStyle }
rejectStyle={ rejectStyle }
disableClick={ true }
onDrop={ this.onDrop }>
{ this.props.children }
</ReactDropzone>
)
}
}

View File

@@ -0,0 +1,49 @@
/*
* Minio Browser (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.
*/
import React from 'react'
let InputGroup = ({label, id, name, value, onChange, type, spellCheck, required, readonly, autoComplete, align, className}) => {
var input = <input id={ id }
name={ name }
value={ value }
onChange={ onChange }
className="ig-text"
type={ type }
spellCheck={ spellCheck }
required={ required }
autoComplete={ autoComplete } />
if (readonly)
input = <input id={ id }
name={ name }
value={ value }
onChange={ onChange }
className="ig-text"
type={ type }
spellCheck={ spellCheck }
required={ required }
autoComplete={ autoComplete }
disabled />
return <div className={ "input-group " + align + ' ' + className }>
{ input }
<i className="ig-helpers"></i>
<label className="ig-label">
{ label }
</label>
</div>
}
export default InputGroup

View File

@@ -0,0 +1,133 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import classNames from 'classnames'
import logo from '../../img/logo.svg'
import Alert from 'react-bootstrap/lib/Alert'
import * as actions from '../actions'
import InputGroup from '../components/InputGroup'
export default class Login extends React.Component {
handleSubmit(event) {
event.preventDefault()
const {web, dispatch, loginRedirectPath} = this.props
let message = ''
if (!document.getElementById('accessKey').value) {
message = 'Secret Key cannot be empty'
}
if (!document.getElementById('secretKey').value) {
message = 'Access Key cannot be empty'
}
if (message) {
dispatch(actions.showAlert({
type: 'danger',
message
}))
return
}
web.Login({
username: document.getElementById('accessKey').value,
password: document.getElementById('secretKey').value
})
.then((res) => {
this.context.router.push(loginRedirectPath)
})
.catch(e => {
dispatch(actions.setLoginError())
dispatch(actions.showAlert({
type: 'danger',
message: e.message
}))
})
}
componentWillMount() {
const {dispatch} = this.props
// Clear out any stale message in the alert of previous page
dispatch(actions.showAlert({
type: 'danger',
message: ''
}))
document.body.classList.add('is-guest')
}
componentWillUnmount() {
document.body.classList.remove('is-guest')
}
hideAlert() {
const {dispatch} = this.props
dispatch(actions.hideAlert())
}
render() {
const {alert} = this.props
let alertBox = <Alert className={ 'alert animated ' + (alert.show ? 'fadeInDown' : 'fadeOutUp') } bsStyle={ alert.type } onDismiss={ this.hideAlert.bind(this) }>
<div className='text-center'>
{ alert.message }
</div>
</Alert>
// Make sure you don't show a fading out alert box on the initial web-page load.
if (!alert.message)
alertBox = ''
return (
<div className="login">
{ alertBox }
<div className="l-wrap">
<form onSubmit={ this.handleSubmit.bind(this) }>
<input name="fixBrowser"
autoComplete="username"
type="text"
style={ { display: 'none' } } />
<InputGroup className="ig-dark"
label="Access Key"
id="accessKey"
name="username"
type="text"
spellCheck="false"
required="required"
autoComplete="username">
</InputGroup>
<input type="text" autoComplete="new-password" style={ { display: 'none' } } />
<InputGroup className="ig-dark"
label="Secret Key"
id="secretKey"
name="password"
type="password"
spellCheck="false"
required="required"
autoComplete="new-password">
</InputGroup>
<button className="lw-btn" type="submit">
<i className="fa fa-sign-in"></i>
</button>
</form>
</div>
<div className="l-footer">
<a className="lf-logo" href=""><img src={ logo } alt="" /></a>
<div className="lf-server">
{ window.location.host }
</div>
</div>
</div>
)
}
}
Login.contextTypes = {
router: React.PropTypes.object.isRequired
}

View File

@@ -0,0 +1,75 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import Moment from 'moment'
import humanize from 'humanize'
import connect from 'react-redux/lib/components/connect'
import Dropdown from 'react-bootstrap/lib/Dropdown'
let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConfirmation, shareObject, loadPath}) => {
const list = objects.map((object, i) => {
let size = object.name.endsWith('/') ? '-' : humanize.filesize(object.size)
let lastModified = object.name.endsWith('/') ? '-' : Moment(object.lastModified).format('lll')
let loadingClass = loadPath === `${currentPath}${object.name}` ? 'fesl-loading' : ''
let actionButtons = ''
let deleteButton = ''
if (web.LoggedIn())
deleteButton = <a href="" className="fiad-action" onClick={ (e) => showDeleteConfirmation(e, `${currentPath}${object.name}`) }><i className="fa fa-trash"></i></a>
if (!object.name.endsWith('/')) {
actionButtons = <Dropdown id="fia-dropdown">
<Dropdown.Toggle noCaret className="fia-toggle"></Dropdown.Toggle>
<Dropdown.Menu>
<a href="" className="fiad-action" onClick={ (e) => shareObject(e, `${currentPath}${object.name}`) }><i className="fa fa-copy"></i></a>
{ deleteButton }
</Dropdown.Menu>
</Dropdown>
}
return (
<div key={ i } className={ "fesl-row " + loadingClass } data-type={ dataType(object.name, object.contentType) }>
<div className="fesl-item fi-name">
<a href="" onClick={ (e) => selectPrefix(e, `${currentPath}${object.name}`) }>
{ object.name }
</a>
</div>
<div className="fesl-item fi-size">
{ size }
</div>
<div className="fesl-item fi-modified">
{ lastModified }
</div>
<div className="fesl-item fi-actions">
{ actionButtons }
</div>
</div>
)
})
return (
<div>
{ list }
</div>
)
}
// Subscribe it to state changes.
export default connect(state => {
return {
objects: state.objects,
currentPath: state.currentPath,
loadPath: state.loadPath
}
})(ObjectsList)

View File

@@ -0,0 +1,41 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import connect from 'react-redux/lib/components/connect'
let Path = ({currentBucket, currentPath, selectPrefix}) => {
let dirPath = []
let path = ''
if (currentPath) {
path = currentPath.split('/').map((dir, i) => {
dirPath.push(dir)
let dirPath_ = dirPath.join('/') + '/'
return <span key={ i }><a href="" onClick={ (e) => selectPrefix(e, dirPath_) }>{ dir }</a></span>
})
}
return (
<h2><span className="main"><a onClick={ (e) => selectPrefix(e, '') } href="">{ currentBucket }</a></span>{ path }</h2>
)
}
export default connect(state => {
return {
currentBucket: state.currentBucket,
currentPath: state.currentPath
}
})(Path)

View File

@@ -0,0 +1,80 @@
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants'
import React, { Component, PropTypes } from 'react'
import connect from 'react-redux/lib/components/connect'
import classnames from 'classnames'
import * as actions from '../actions'
class Policy extends Component {
constructor(props, context) {
super(props, context)
this.state = {}
}
handlePolicyChange(e) {
this.setState({
policy: {
policy: e.target.value
}
})
}
removePolicy(e) {
e.preventDefault()
const {dispatch, currentBucket, prefix} = this.props
let newPrefix = prefix.replace(currentBucket + '/', '')
newPrefix = newPrefix.replace('*', '')
web.SetBucketPolicy({
bucketName: currentBucket,
prefix: newPrefix,
policy: 'none'
})
.then(() => {
dispatch(actions.setPolicies(this.props.policies.filter(policy => policy.prefix != prefix)))
})
.catch(e => dispatch(actions.showAlert({
type: 'danger',
message: e.message,
})))
}
render() {
const {policy, prefix, currentBucket} = this.props
let newPrefix = prefix.replace(currentBucket + '/', '')
newPrefix = newPrefix.replace('*', '')
if (!newPrefix)
newPrefix = '*'
return (
<div className="pmb-list">
<div className="pmbl-item">
{ newPrefix }
</div>
<div className="pmbl-item">
<select className="form-control"
disabled
value={ policy }
onChange={ this.handlePolicyChange.bind(this) }>
<option value={ READ_ONLY }>
Read Only
</option>
<option value={ WRITE_ONLY }>
Write Only
</option>
<option value={ READ_WRITE }>
Read and Write
</option>
</select>
</div>
<div className="pmbl-item">
<button className="btn btn-block btn-danger" onClick={ this.removePolicy.bind(this) }>
Remove
</button>
</div>
</div>
)
}
}
export default connect(state => state)(Policy)

View File

@@ -0,0 +1,83 @@
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants'
import React, { Component, PropTypes } from 'react'
import connect from 'react-redux/lib/components/connect'
import classnames from 'classnames'
import * as actions from '../actions'
class PolicyInput extends Component {
componentDidMount() {
const {web, dispatch} = this.props
web.ListAllBucketPolicies({
bucketName: this.props.currentBucket
}).then(res => {
let policies = res.policies
if (policies) dispatch(actions.setPolicies(policies))
}).catch(err => {
dispatch(actions.showAlert({
type: 'danger',
message: err.message
}))
})
}
componentWillUnmount() {
const {dispatch} = this.props
dispatch(actions.setPolicies([]))
}
handlePolicySubmit(e) {
e.preventDefault()
const {web, dispatch} = this.props
web.SetBucketPolicy({
bucketName: this.props.currentBucket,
prefix: this.prefix.value,
policy: this.policy.value
})
.then(() => {
dispatch(actions.setPolicies([{
policy: this.policy.value,
prefix: this.prefix.value + '*',
}, ...this.props.policies]))
this.prefix.value = ''
})
.catch(e => dispatch(actions.showAlert({
type: 'danger',
message: e.message,
})))
}
render() {
return (
<header className="pmb-list">
<div className="pmbl-item">
<input type="text"
ref={ prefix => this.prefix = prefix }
className="form-control"
placeholder="Prefix"
editable={ true } />
</div>
<div className="pmbl-item">
<select ref={ policy => this.policy = policy } className="form-control">
<option value={ READ_ONLY }>
Read Only
</option>
<option value={ WRITE_ONLY }>
Write Only
</option>
<option value={ READ_WRITE }>
Read and Write
</option>
</select>
</div>
<div className="pmbl-item">
<button className="btn btn-block btn-primary" onClick={ this.handlePolicySubmit.bind(this) }>
Add
</button>
</div>
</header>
)
}
}
export default connect(state => state)(PolicyInput)

View File

@@ -0,0 +1,215 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import connect from 'react-redux/lib/components/connect'
import * as actions from '../actions'
import Tooltip from 'react-bootstrap/lib/Tooltip'
import Modal from 'react-bootstrap/lib/Modal'
import ModalBody from 'react-bootstrap/lib/ModalBody'
import ModalHeader from 'react-bootstrap/lib/ModalHeader'
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
import InputGroup from './InputGroup'
class SettingsModal extends React.Component {
// When the settings are shown, it loads the access key and secret key.
componentWillMount() {
const {web, dispatch} = this.props
const {serverInfo} = this.props
let accessKeyEnv = ''
let secretKeyEnv = ''
// Check environment variables first. They may or may not have been
// loaded already; they load in Browse#componentDidMount.
if (serverInfo.envVars) {
serverInfo.envVars.forEach(envVar => {
let keyVal = envVar.split('=')
if (keyVal[0] == 'MINIO_ACCESS_KEY') {
accessKeyEnv = keyVal[1]
} else if (keyVal[0] == 'MINIO_SECRET_KEY') {
secretKeyEnv = keyVal[1]
}
})
}
if (accessKeyEnv != '' || secretKeyEnv != '') {
dispatch(actions.setSettings({
accessKey: accessKeyEnv,
secretKey: secretKeyEnv,
keysReadOnly: true
}))
} else {
web.GetAuth()
.then(data => {
dispatch(actions.setSettings({
accessKey: data.accessKey,
secretKey: data.secretKey
}))
})
}
}
// When they are re-hidden, the keys are unloaded from memory.
componentWillUnmount() {
const {dispatch} = this.props
dispatch(actions.setSettings({
accessKey: '',
secretKey: '',
secretKeyVisible: false
}))
dispatch(actions.hideSettings())
}
// Handle field changes from inside the modal.
accessKeyChange(e) {
const {dispatch} = this.props
dispatch(actions.setSettings({
accessKey: e.target.value
}))
}
secretKeyChange(e) {
const {dispatch} = this.props
dispatch(actions.setSettings({
secretKey: e.target.value
}))
}
secretKeyVisible(secretKeyVisible) {
const {dispatch} = this.props
dispatch(actions.setSettings({
secretKeyVisible
}))
}
// Save the auth params and set them.
setAuth(e) {
e.preventDefault()
const {web, dispatch} = this.props
let accessKey = document.getElementById('accessKey').value
let secretKey = document.getElementById('secretKey').value
web.SetAuth({
accessKey,
secretKey
})
.then(data => {
dispatch(actions.setSettings({
accessKey: '',
secretKey: '',
secretKeyVisible: false
}))
dispatch(actions.hideSettings())
dispatch(actions.showAlert({
type: 'success',
message: 'Changed credentials'
}))
})
.catch(err => {
dispatch(actions.setSettings({
accessKey: '',
secretKey: '',
secretKeyVisible: false
}))
dispatch(actions.hideSettings())
dispatch(actions.showAlert({
type: 'danger',
message: err.message
}))
})
}
generateAuth(e) {
e.preventDefault()
const {dispatch} = this.props
web.GenerateAuth()
.then(data => {
dispatch(actions.setSettings({
secretKeyVisible: true
}))
dispatch(actions.setSettings({
accessKey: data.accessKey,
secretKey: data.secretKey
}))
})
}
hideSettings(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.hideSettings())
}
render() {
let {settings} = this.props
return (
<Modal bsSize="sm" animation={ false } show={ true }>
<ModalHeader>
Change Password
</ModalHeader>
<ModalBody className="m-t-20">
<InputGroup value={ settings.accessKey }
onChange={ this.accessKeyChange.bind(this) }
id="accessKey"
label="Access Key"
name="accesskey"
type="text"
spellCheck="false"
required="required"
autoComplete="false"
align="ig-left"
readonly={ settings.keysReadOnly }></InputGroup>
<i onClick={ this.secretKeyVisible.bind(this, !settings.secretKeyVisible) } className={ "toggle-password fa fa-eye " + (settings.secretKeyVisible ? "toggled" : "") } />
<InputGroup value={ settings.secretKey }
onChange={ this.secretKeyChange.bind(this) }
id="secretKey"
label="Secret Key"
name="accesskey"
type={ settings.secretKeyVisible ? "text" : "password" }
spellCheck="false"
required="required"
autoComplete="false"
align="ig-left"
readonly={ settings.keysReadOnly }></InputGroup>
</ModalBody>
<div className="modal-footer">
<button className={ "btn btn-primary " + (settings.keysReadOnly ? "hidden" : "") } onClick={ this.generateAuth.bind(this) }>
Generate
</button>
<button href="" className={ "btn btn-success " + (settings.keysReadOnly ? "hidden" : "") } onClick={ this.setAuth.bind(this) }>
Update
</button>
<button href="" className="btn btn-link" onClick={ this.hideSettings.bind(this) }>
Cancel
</button>
</div>
</Modal>
)
}
}
export default connect(state => {
return {
web: state.web,
settings: state.settings,
serverInfo: state.serverInfo
}
})(SettingsModal)

View File

@@ -0,0 +1,85 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import classNames from 'classnames'
import ClickOutHandler from 'react-onclickout'
import Scrollbars from 'react-custom-scrollbars/lib/Scrollbars'
import connect from 'react-redux/lib/components/connect'
import logo from '../../img/logo.svg'
let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, landingPage, sidebarStatus, clickOutside, showPolicy}) => {
const list = visibleBuckets.map((bucket, i) => {
return <li className={ classNames({
'active': bucket === currentBucket
}) } key={ i } onClick={ (e) => selectBucket(e, bucket) }>
<a href="" className={ classNames({
'fesli-loading': bucket === loadBucket
}) }>
{ bucket }
</a>
<i className="fesli-trigger" onClick={ showPolicy }></i>
</li>
})
return (
<ClickOutHandler onClickOut={ clickOutside }>
<div className={ classNames({
'fe-sidebar': true,
'toggled': sidebarStatus
}) }>
<div className="fes-header clearfix hidden-sm hidden-xs">
<a href="" onClick={ landingPage }><img src={ logo } alt="" />
<h2>Minio Browser</h2></a>
</div>
<div className="fes-list">
<div className="input-group ig-dark ig-left ig-search" style={ { display: web.LoggedIn() ? 'block' : 'none' } }>
<input className="ig-text"
type="text"
onChange={ searchBuckets }
placeholder="Search Buckets..." />
<i className="ig-helpers"></i>
</div>
<div className="fesl-inner">
<Scrollbars renderScrollbarVertical={ props => <div className="scrollbar-vertical" /> }>
<ul>
{ list }
</ul>
</Scrollbars>
</div>
</div>
<div className="fes-host">
<i className="fa fa-globe"></i>
<a href="/">
{ window.location.host }
</a>
</div>
</div>
</ClickOutHandler>
)
}
// Subscribe it to state changes that affect only the sidebar.
export default connect(state => {
return {
visibleBuckets: state.visibleBuckets,
loadBucket: state.loadBucket,
currentBucket: state.currentBucket,
sidebarStatus: state.sidebarStatus
}
})(SideBar)

View File

@@ -0,0 +1,141 @@
/*
* Minio Browser (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.
*/
import React from 'react'
import humanize from 'humanize'
import classNames from 'classnames'
import connect from 'react-redux/lib/components/connect'
import ProgressBar from 'react-bootstrap/lib/ProgressBar'
import ConfirmModal from './ConfirmModal'
import * as actions from '../actions'
// UploadModal is a modal that handles multiple file uploads.
// During the upload, it displays a progress bar, and can transform into an
// abort modal if the user decides to abort the uploads.
class UploadModal extends React.Component {
// Abort all the current uploads.
abortUploads(e) {
e.preventDefault()
const {dispatch, uploads} = this.props
for (var slug in uploads) {
let upload = uploads[slug]
upload.xhr.abort()
dispatch(actions.stopUpload({
slug
}))
}
this.hideAbort(e)
}
// Show the abort modal instead of the progress modal.
showAbort(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.setShowAbortModal(true))
}
// Show the progress modal instead of the abort modal.
hideAbort(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.setShowAbortModal(false))
}
render() {
const {uploads, showAbortModal} = this.props
// Show the abort modal.
if (showAbortModal) {
let baseClass = classNames({
'abort-upload': true
})
let okIcon = classNames({
'fa': true,
'fa-times': true
})
let cancelIcon = classNames({
'fa': true,
'fa-cloud-upload': true
})
return (
<ConfirmModal show={ true }
baseClass={ baseClass }
text='Abort uploads in progress?'
icon='fa fa-info-circle mci-amber'
sub='This cannot be undone!'
okText='Abort'
okIcon={ okIcon }
cancelText='Upload'
cancelIcon={ cancelIcon }
okHandler={ this.abortUploads.bind(this) }
cancelHandler={ this.hideAbort.bind(this) }>
</ConfirmModal>
)
}
// If we don't have any files uploading, don't show anything.
let numberUploading = Object.keys(uploads).length
if (numberUploading == 0)
return ( <noscript></noscript> )
let totalLoaded = 0
let totalSize = 0
// Iterate over each upload, adding together the total size and that
// which has been uploaded.
for (var slug in uploads) {
let upload = uploads[slug]
totalLoaded += upload.loaded
totalSize += upload.size
}
let percent = (totalLoaded / totalSize) * 100
// If more than one: "Uploading files (5)..."
// If only one: "Uploading myfile.txt..."
let text = 'Uploading ' + (numberUploading == 1 ? `'${uploads[Object.keys(uploads)[0]].name}'` : `files (${numberUploading})`) + '...'
return (
<div className="alert alert-info progress animated fadeInUp ">
<button type="button" className="close" onClick={ this.showAbort.bind(this) }>
<span>×</span>
</button>
<div className="text-center">
<small>{ text }</small>
</div>
<ProgressBar now={ percent } />
<div className="text-center">
<small>{ humanize.filesize(totalLoaded) } ({ percent.toFixed(2) } %)</small>
</div>
</div>
)
}
}
export default connect(state => {
return {
uploads: state.uploads,
showAbortModal: state.showAbortModal
}
})(UploadModal)

View File

@@ -0,0 +1,54 @@
/*
* Minio Browser (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.
*/
/*
import React from 'react'
import ReactTestUtils, {renderIntoDocument} from 'react-addons-test-utils'
import expect from 'expect'
import Login from '../Login'
describe('Login', () => {
it('it should have empty credentials', () => {
const alert = {
show: false
}
const dispatch = () => {}
let loginComponent = renderIntoDocument(<Login alert={alert} dispatch={dispatch} />)
const accessKey = document.getElementById('accessKey')
const secretKey = document.getElementById('secretKey')
// Validate default value.
expect(accessKey.value).toEqual('')
expect(secretKey.value).toEqual('')
})
it('it should set accessKey and secretKey', () => {
const alert = {
show: false
}
const dispatch = () => {}
let loginComponent = renderIntoDocument(<Login alert={alert} dispatch={dispatch} />)
let accessKey = loginComponent.refs.accessKey
let secretKey = loginComponent.refs.secretKey
accessKey.value = 'demo-username'
secretKey.value = 'demo-password'
ReactTestUtils.Simulate.change(accessKey)
ReactTestUtils.Simulate.change(secretKey)
// Validate if the change has occurred.
expect(loginComponent.refs.accessKey.value).toEqual('demo-username')
expect(loginComponent.refs.secretKey.value).toEqual('demo-password')
})
});
*/

View File

@@ -0,0 +1,23 @@
/*
* Minio Browser (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 for all the browser constants.
// minioBrowserPrefix absolute path.
export const minioBrowserPrefix = '/minio'
export const READ_ONLY = 'readonly'
export const WRITE_ONLY = 'writeonly'
export const READ_WRITE = 'readwrite'

91
browser/app/js/jsonrpc.js Normal file
View File

@@ -0,0 +1,91 @@
/*
* Minio Browser (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.
*/
import SuperAgent from 'superagent-es6-promise';
import url from 'url'
import Moment from 'moment'
export default class JSONrpc {
constructor(params) {
this.endpoint = params.endpoint
this.namespace = params.namespace
this.version = '2.0';
const parsedUrl = url.parse(this.endpoint)
this.host = parsedUrl.hostname
this.path = parsedUrl.path
this.port = parsedUrl.port
switch (parsedUrl.protocol) {
case 'http:': {
this.scheme = 'http'
if (parsedUrl.port === 0) {
this.port = 80
}
break
}
case 'https:': {
this.scheme = 'https'
if (parsedUrl.port === 0) {
this.port = 443
}
break
}
default: {
throw new Error('Unknown protocol: ' + parsedUrl.protocol)
}
}
}
// call('Get', {id: NN, params: [...]}, function() {})
call(method, options, token) {
if (!options) {
options = {}
}
if (!options.id) {
options.id = 1;
}
if (!options.params) {
options.params = {};
}
const dataObj = {
id: options.id,
jsonrpc: this.version,
params: options.params ? options.params : {},
method: this.namespace ? this.namespace + '.' + method : method
}
let requestParams = {
host: this.host,
port: this.port,
path: this.path,
scheme: this.scheme,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-amz-date': Moment().utc().format('YYYYMMDDTHHmmss') + 'Z'
}
}
if (token) {
requestParams.headers.Authorization = 'Bearer ' + token
}
let req = SuperAgent.post(this.endpoint)
for (let key in requestParams.headers) {
req.set(key, requestParams.headers[key])
}
// req.set('Access-Control-Allow-Origin', 'http://localhost:8080')
return req.send(JSON.stringify(dataObj)).then(res => res)
}
}

106
browser/app/js/mime.js Normal file
View File

@@ -0,0 +1,106 @@
/*
* Minio Browser (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.
*/
import mimedb from 'mime-types'
const isFolder = (name, contentType) => {
if (name.endsWith('/')) return true
return false
}
const isPdf = (name, contentType) => {
if (contentType === 'application/pdf') return true
return false
}
const isZip = (name, contentType) => {
if (!contentType || !contentType.includes('/')) return false
if (contentType.split('/')[1].includes('zip')) return true
return false
}
const isCode = (name, contentType) => {
const codeExt = ['c', 'cpp', 'go', 'py', 'java', 'rb', 'js', 'pl', 'fs',
'php', 'css', 'less', 'scss', 'coffee', 'net', 'html',
'rs', 'exs', 'scala', 'hs', 'clj', 'el', 'scm', 'lisp',
'asp', 'aspx']
const ext = name.split('.').reverse()[0]
for (var i in codeExt) {
if (ext === codeExt[i]) return true
}
return false
}
const isExcel = (name, contentType) => {
if (!contentType || !contentType.includes('/')) return false
const types = ['excel', 'spreadsheet']
const subType = contentType.split('/')[1]
for (var i in types) {
if (subType.includes(types[i])) return true
}
return false
}
const isDoc = (name, contentType) => {
if (!contentType || !contentType.includes('/')) return false
const types = ['word', '.document']
const subType = contentType.split('/')[1]
for (var i in types) {
if (subType.includes(types[i])) return true
}
return false
}
const isPresentation = (name, contentType) => {
if (!contentType || !contentType.includes('/')) return false
var types = ['powerpoint', 'presentation']
const subType = contentType.split('/')[1]
for (var i in types) {
if (subType.includes(types[i])) return true
}
return false
}
const typeToIcon = (type) => {
return (name, contentType) => {
if (!contentType || !contentType.includes('/')) return false
if (contentType.split('/')[0] === type) return true
return false
}
}
export const getDataType = (name, contentType) => {
if (contentType === "") {
contentType = mimedb.lookup(name) || 'application/octet-stream'
}
const check = [
['folder', isFolder],
['code', isCode],
['audio', typeToIcon('audio')],
['image', typeToIcon('image')],
['video', typeToIcon('video')],
['text', typeToIcon('text')],
['pdf', isPdf],
['zip', isZip],
['excel', isExcel],
['doc', isDoc],
['presentation', isPresentation]
]
for (var i in check) {
if (check[i][1](name, contentType)) return check[i][0]
}
return 'other'
}

176
browser/app/js/reducers.js Normal file
View File

@@ -0,0 +1,176 @@
/*
* Minio Browser (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.
*/
import * as actions from './actions'
import { minioBrowserPrefix } from './constants'
export default (state = {
buckets: [],
visibleBuckets: [],
objects: [],
storageInfo: {},
serverInfo: {},
currentBucket: '',
currentPath: '',
showMakeBucketModal: false,
uploads: {},
alert: {
show: false,
type: 'danger',
message: ''
},
loginError: false,
sortNameOrder: false,
sortSizeOrder: false,
sortDateOrder: false,
latestUiVersion: currentUiVersion,
sideBarActive: false,
loginRedirectPath: minioBrowserPrefix,
settings: {
accessKey: '',
secretKey: '',
secretKeyVisible: false
},
showSettings: false,
policies: [],
deleteConfirmation: {
object: '',
show: false
},
shareObject: {
show: false,
url: '',
expiry: 604800
},
prefixWritable: false
}, action) => {
let newState = Object.assign({}, state)
switch (action.type) {
case actions.SET_WEB:
newState.web = action.web
break
case actions.SET_BUCKETS:
newState.buckets = action.buckets
break
case actions.ADD_BUCKET:
newState.buckets = [action.bucket, ...newState.buckets]
newState.visibleBuckets = [action.bucket, ...newState.visibleBuckets]
break
case actions.SET_VISIBLE_BUCKETS:
newState.visibleBuckets = action.visibleBuckets
break
case actions.SET_CURRENT_BUCKET:
newState.currentBucket = action.currentBucket
break
case actions.SET_OBJECTS:
newState.objects = action.objects
break
case actions.SET_CURRENT_PATH:
newState.currentPath = action.currentPath
break
case actions.SET_STORAGE_INFO:
newState.storageInfo = action.storageInfo
break
case actions.SET_SERVER_INFO:
newState.serverInfo = action.serverInfo
break
case actions.SHOW_MAKEBUCKET_MODAL:
newState.showMakeBucketModal = action.showMakeBucketModal
break
case actions.UPLOAD_PROGRESS:
newState.uploads = Object.assign({}, newState.uploads)
newState.uploads[action.slug].loaded = action.loaded
break
case actions.ADD_UPLOAD:
newState.uploads = Object.assign({}, newState.uploads, {
[action.slug]: {
loaded: 0,
size: action.size,
xhr: action.xhr,
name: action.name
}
})
break
case actions.STOP_UPLOAD:
newState.uploads = Object.assign({}, newState.uploads)
delete newState.uploads[action.slug]
break
case actions.SET_ALERT:
if (newState.alert.alertTimeout) clearTimeout(newState.alert.alertTimeout)
if (!action.alert.show) {
newState.alert = Object.assign({}, newState.alert, {
show: false
})
} else {
newState.alert = action.alert
}
break
case actions.SET_LOGIN_ERROR:
newState.loginError = true
break
case actions.SET_SHOW_ABORT_MODAL:
newState.showAbortModal = action.showAbortModal
break
case actions.SHOW_ABOUT:
newState.showAbout = action.showAbout
break
case actions.SET_SORT_NAME_ORDER:
newState.sortNameOrder = action.sortNameOrder
break
case actions.SET_SORT_SIZE_ORDER:
newState.sortSizeOrder = action.sortSizeOrder
break
case actions.SET_SORT_DATE_ORDER:
newState.sortDateOrder = action.sortDateOrder
break
case actions.SET_LATEST_UI_VERSION:
newState.latestUiVersion = action.latestUiVersion
break
case actions.SET_SIDEBAR_STATUS:
newState.sidebarStatus = action.sidebarStatus
break
case actions.SET_LOGIN_REDIRECT_PATH:
newState.loginRedirectPath = action.path
case actions.SET_LOAD_BUCKET:
newState.loadBucket = action.loadBucket
break
case actions.SET_LOAD_PATH:
newState.loadPath = action.loadPath
break
case actions.SHOW_SETTINGS:
newState.showSettings = action.showSettings
break
case actions.SET_SETTINGS:
newState.settings = Object.assign({}, newState.settings, action.settings)
break
case actions.SHOW_BUCKET_POLICY:
newState.showBucketPolicy = action.showBucketPolicy
break
case actions.SET_POLICIES:
newState.policies = action.policies
break
case actions.DELETE_CONFIRMATION:
newState.deleteConfirmation = Object.assign({}, action.payload)
break
case actions.SET_SHARE_OBJECT:
newState.shareObject = Object.assign({}, action.shareObject)
break
case actions.SET_PREFIX_WRITABLE:
newState.prefixWritable = action.prefixWritable
break
}
return newState
}

85
browser/app/js/utils.js Normal file
View File

@@ -0,0 +1,85 @@
/*
* Minio Browser (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.
*/
import { minioBrowserPrefix } from './constants.js'
export const sortObjectsByName = (objects, order) => {
let folders = objects.filter(object => object.name.endsWith('/'))
let files = objects.filter(object => !object.name.endsWith('/'))
folders = folders.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
return 0
})
files = files.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
return 0
})
if (order) {
folders = folders.reverse()
files = files.reverse()
}
return [...folders, ...files]
}
export const sortObjectsBySize = (objects, order) => {
let folders = objects.filter(object => object.name.endsWith('/'))
let files = objects.filter(object => !object.name.endsWith('/'))
files = files.sort((a, b) => a.size - b.size)
if (order)
files = files.reverse()
return [...folders, ...files]
}
export const sortObjectsByDate = (objects, order) => {
let folders = objects.filter(object => object.name.endsWith('/'))
let files = objects.filter(object => !object.name.endsWith('/'))
files = files.sort((a, b) => new Date(a.lastModified).getTime() - new Date(b.lastModified).getTime())
if (order)
files = files.reverse()
return [...folders, ...files]
}
export const pathSlice = (path) => {
path = path.replace(minioBrowserPrefix, '')
let prefix = ''
let bucket = ''
if (!path) return {
bucket,
prefix
}
let objectIndex = path.indexOf('/', 1)
if (objectIndex == -1) {
bucket = path.slice(1)
return {
bucket,
prefix
}
}
bucket = path.slice(1, objectIndex)
prefix = path.slice(objectIndex + 1)
return {
bucket,
prefix
}
}
export const pathJoin = (bucket, prefix) => {
if (!prefix)
prefix = ''
return minioBrowserPrefix + '/' + bucket + '/' + prefix
}

124
browser/app/js/web.js Normal file
View File

@@ -0,0 +1,124 @@
/*
* Minio Browser (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.
*/
import { browserHistory } from 'react-router'
import JSONrpc from './jsonrpc'
import * as actions from './actions'
import { minioBrowserPrefix } from './constants.js'
import Moment from 'moment'
import storage from 'local-storage-fallback'
export default class Web {
constructor(endpoint, dispatch) {
const namespace = 'Web'
this.dispatch = dispatch
this.JSONrpc = new JSONrpc({
endpoint,
namespace
})
}
makeCall(method, options) {
return this.JSONrpc.call(method, {
params: options
}, storage.getItem('token'))
.catch(err => {
if (err.status === 401) {
storage.removeItem('token')
browserHistory.push(`${minioBrowserPrefix}/login`)
throw new Error('Please re-login.')
}
if (err.status)
throw new Error(`Server returned error [${err.status}]`)
throw new Error('Minio server is unreachable')
})
.then(res => {
let json = JSON.parse(res.text)
let result = json.result
let error = json.error
if (error) {
throw new Error(error.message)
}
if (!Moment(result.uiVersion).isValid()) {
throw new Error("Invalid UI version in the JSON-RPC response")
}
if (result.uiVersion !== currentUiVersion
&& currentUiVersion !== 'MINIO_UI_VERSION') {
storage.setItem('newlyUpdated', true)
location.reload()
}
return result
})
}
LoggedIn() {
return !!storage.getItem('token')
}
Login(args) {
return this.makeCall('Login', args)
.then(res => {
storage.setItem('token', `${res.token}`)
return res
})
}
Logout() {
storage.removeItem('token')
}
ServerInfo() {
return this.makeCall('ServerInfo')
}
StorageInfo() {
return this.makeCall('StorageInfo')
}
ListBuckets() {
return this.makeCall('ListBuckets')
}
MakeBucket(args) {
return this.makeCall('MakeBucket', args)
}
ListObjects(args) {
return this.makeCall('ListObjects', args)
}
PresignedGet(args) {
return this.makeCall('PresignedGet', args)
}
PutObjectURL(args) {
return this.makeCall('PutObjectURL', args)
}
RemoveObject(args) {
return this.makeCall('RemoveObject', args)
}
GetAuth() {
return this.makeCall('GetAuth')
}
GenerateAuth() {
return this.makeCall('GenerateAuth')
}
SetAuth(args) {
return this.makeCall('SetAuth', args)
.then(res => {
storage.setItem('token', `${res.token}`)
return res
})
}
GetBucketPolicy(args) {
return this.makeCall('GetBucketPolicy', args)
}
SetBucketPolicy(args) {
return this.makeCall('SetBucketPolicy', args)
}
ListAllBucketPolicies(args) {
return this.makeCall('ListAllBucketPolicies', args)
}
}

View File

@@ -0,0 +1,68 @@
.alert {
border: 0;
position: fixed;
max-width: 500px;
margin: 0;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.1);
color: @white;
width: 100%;
right: 20px;
border-radius: 3px;
padding: 17px 50px 17px 17px;
z-index: 10010;
.animation-duration(800ms);
.animation-fill-mode(both);
&:not(.progress) {
top: 20px;
@media(min-width: (@screen-sm-min)) {
left: 50%;
margin-left: -250px;
}
}
&.progress {
bottom: 20px;
right: 20px;
}
&.alert-danger {
background: @red;
}
&.alert-success {
background: @green;
}
&.alert-info {
background: @blue;
}
@media(max-width: (@screen-xs-max)) {
left: 20px;
width: ~"calc(100% - 40px)";
max-width: 100%;
}
.progress {
margin: 10px 10px 8px 0;
height: 5px;
box-shadow: none;
border-radius: 1px;
background-color: @blue;
border-radius: 2px;
overflow: hidden;
}
.progress-bar {
box-shadow: none;
background-color: @white;
height: 100%;
}
.close {
position: absolute;
top: 15px;
}
}

View File

@@ -0,0 +1,13 @@
.animated{
&.infinite {
.animation-iteration-count(infinite);
}
}
@import 'fadeIn';
@import 'fadeInDown';
@import 'fadeInUp';
@import 'fadeOut';
@import 'fadeOutDown';
@import 'fadeOutUp';
@import 'zoomIn';

View File

@@ -0,0 +1,26 @@
@-webkit-keyframes fadeIn {
0% {opacity: 0;}
100% {opacity: 1;}
}
@-moz-keyframes fadeIn {
0% {opacity: 0;}
100% {opacity: 1;}
}
@-o-keyframes fadeIn {
0% {opacity: 0;}
100% {opacity: 1;}
}
@keyframes fadeIn {
0% {opacity: 0;}
100% {opacity: 1;}
}
.fadeIn {
-webkit-animation-name: fadeIn;
-moz-animation-name: fadeIn;
-o-animation-name: fadeIn;
animation-name: fadeIn;
}

View File

@@ -0,0 +1,54 @@
@-webkit-keyframes fadeInDown {
0% {
opacity: 0;
-webkit-transform: translateY(-20px);
}
100% {
opacity: 1;
-webkit-transform: translateY(0);
}
}
@-moz-keyframes fadeInDown {
0% {
opacity: 0;
-moz-transform: translateY(-20px);
}
100% {
opacity: 1;
-moz-transform: translateY(0);
}
}
@-o-keyframes fadeInDown {
0% {
opacity: 0;
-ms-transform: translateY(-20px);
}
100% {
opacity: 1;
-ms-transform: translateY(0);
}
}
@keyframes fadeInDown {
0% {
opacity: 0;
transform: translateY(-20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.fadeInDown {
-webkit-animation-name: fadeInDown;
-moz-animation-name: fadeInDown;
-o-animation-name: fadeInDown;
animation-name: fadeInDown;
}

View File

@@ -0,0 +1,54 @@
@-webkit-keyframes fadeInUp {
0% {
opacity: 0;
-webkit-transform: translateY(20px);
}
100% {
opacity: 1;
-webkit-transform: translateY(0);
}
}
@-moz-keyframes fadeInUp {
0% {
opacity: 0;
-moz-transform: translateY(20px);
}
100% {
opacity: 1;
-moz-transform: translateY(0);
}
}
@-o-keyframes fadeInUp {
0% {
opacity: 0;
-o-transform: translateY(20px);
}
100% {
opacity: 1;
-o-transform: translateY(0);
}
}
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.fadeInUp {
-webkit-animation-name: fadeInUp;
-moz-animation-name: fadeInUp;
-o-animation-name: fadeInUp;
animation-name: fadeInUp;
}

View File

@@ -0,0 +1,26 @@
@-webkit-keyframes fadeOut {
0% {opacity: 1;}
100% {opacity: 0;}
}
@-moz-keyframes fadeOut {
0% {opacity: 1;}
100% {opacity: 0;}
}
@-o-keyframes fadeOut {
0% {opacity: 1;}
100% {opacity: 0;}
}
@keyframes fadeOut {
0% {opacity: 1;}
100% {opacity: 0;}
}
.fadeOut {
-webkit-animation-name: fadeOut;
-moz-animation-name: fadeOut;
-o-animation-name: fadeOut;
animation-name: fadeOut;
}

View File

@@ -0,0 +1,54 @@
@-webkit-keyframes fadeOutDown {
0% {
opacity: 1;
-webkit-transform: translateY(0);
}
100% {
opacity: 0;
-webkit-transform: translateY(20px);
}
}
@-moz-keyframes fadeOutDown {
0% {
opacity: 1;
-moz-transform: translateY(0);
}
100% {
opacity: 0;
-moz-transform: translateY(20px);
}
}
@-o-keyframes fadeOutDown {
0% {
opacity: 1;
-o-transform: translateY(0);
}
100% {
opacity: 0;
-o-transform: translateY(20px);
}
}
@keyframes fadeOutDown {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(20px);
}
}
.fadeOutDown {
-webkit-animation-name: fadeOutDown;
-moz-animation-name: fadeOutDown;
-o-animation-name: fadeOutDown;
animation-name: fadeOutDown;
}

View File

@@ -0,0 +1,51 @@
@-webkit-keyframes fadeOutUp {
0% {
opacity: 1;
-webkit-transform: translateY(0);
}
100% {
opacity: 0;
-webkit-transform: translateY(-20px);
}
}
@-moz-keyframes fadeOutUp {
0% {
opacity: 1;
-moz-transform: translateY(0);
}
100% {
opacity: 0;
-moz-transform: translateY(-20px);
}
}
@-o-keyframes fadeOutUp {
0% {
opacity: 1;
-o-transform: translateY(0);
}
100% {
opacity: 0;
-o-transform: translateY(-20px);
}
}
@keyframes fadeOutUp {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-20px);
}
}
.fadeOutUp {
-webkit-animation-name: fadeOutUp;
-moz-animation-name: fadeOutUp;
-o-animation-name: fadeOutUp;
animation-name: fadeOutUp;
}

View File

@@ -0,0 +1,23 @@
@-webkit-keyframes zoomIn {
from {
opacity: 0;
-webkit-transform: scale3d(.3, .3, .3);
transform: scale3d(.3, .3, .3);
}
50% {
opacity: 1;
}
}
@keyframes zoomIn {
from {
opacity: 0;
-webkit-transform: scale3d(.3, .3, .3);
transform: scale3d(.3, .3, .3);
}
50% {
opacity: 1;
}
}

View File

@@ -0,0 +1,31 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
&:focus,
&:active {
outline: 0;
}
}
html {
font-size: 10px;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
html,
body {
min-height: 100%;
}
a {
.transition(color);
.transition-duration(300ms);
}
button {
border: 0;
}

View File

@@ -0,0 +1,53 @@
.btn {
border: 0;
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 2px;
text-align: center;
.transition(all);
.transition-duration(300ms);
&:hover,
&:focus {
.opacity(0.9);
}
}
/*-----------------------------------
Button Variants
------------------------------------*/
.btn-variant(@bg-color, @color) {
color: @color;
background-color: @bg-color;
&:hover,
&:focus {
color: @color;
background-color: darken(@bg-color, 6%);
}
}
.btn-block {
display: block;
width: 100%;
}
.btn-link {
.btn-variant(#eee, #545454);
}
.btn-danger {
.btn-variant(@red, @white);
}
.btn-primary {
.btn-variant(@blue, @white);
}
.btn-success {
.btn-variant(@green, @white);
}
//-----------------------------------

View File

@@ -0,0 +1,26 @@
.dropdown-menu {
padding: 15px 0;
top: 0;
margin-top: -1px;
& > li {
& > a {
padding: 8px 20px;
font-size: 15px;
& > i {
width: 20px;
position: relative;
top: 1px;
}
}
}
}
.dropdown-menu-right {
& > li {
& > a {
text-align: right;
}
}
}

View File

@@ -0,0 +1,160 @@
/*------------------------------
Layout
--------------------------------*/
.file-explorer {
background-color: @white;
position: relative;
height: 100%;
&.toggled {
height: 100vh;
overflow: hidden;
}
}
.fe-body {
@media(min-width: @screen-md-min) {
padding: 0 0 40px @fe-sidebar-width;
}
@media(max-width: @screen-sm-max) {
padding: 75px 0 80px;
}
min-height:100vh;
overflow: auto;
}
/*------------------------------
Create and Upload Button
--------------------------------*/
.feb-actions {
position: fixed;
bottom: 30px;
right: 30px;
.dropdown-menu {
min-width: 55px;
width: 55px;
text-align: center;
background: transparent;
box-shadow: none;
margin: 0;
}
&.open {
.feba-btn {
.scale(1);
&:first-child {
.animation-name(feba-btn-anim);
.animation-duration(300ms);
}
&:last-child {
.animation-name(feba-btn-anim);
.animation-duration(100ms);
}
}
.feba-toggle {
background: darken(@red, 10%);
& > span {
.rotate(135deg);
}
}
}
}
.feba-toggle {
width: 55px;
height: 55px;
line-height: 55px;
border-radius: 50%;
background: @red;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.15);
display: inline-block;
text-align: center;
border: 0;
padding: 0;
span {
display: inline-block;
height: 100%;
width: 100%;
}
i {
color: @white;
font-size: 17px;
line-height: 58px;
}
}
.feba-toggle,
.feba-toggle > span {
.transition(all);
.transition-duration(250ms);
.backface-visibility(hidden);
}
.feba-btn {
width: 40px;
margin-top: 10px;
height: 40px;
border-radius: 50%;
text-align: center;
display: inline-block;
color: @white;
line-height: 40px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.15);
-webkit-transform: scale(0);
transform: scale(0);
position: relative;
&:hover,
&:focus {
color: @white;
}
label {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
cursor: pointer;
}
}
.feba-bucket {
background: @orange;
}
.feba-upload {
background: @yellow;
}
@-webkit-keyframes feba-btn-anim {
from {
.scale(0);
.opacity(0);
}
to {
.scale(1);
.opacity(1);
}
}
@keyframes feba-btn-anim {
from {
.scale(0);
.opacity(0);
}
to {
.scale(1);
.opacity(1);
}
}

View File

@@ -0,0 +1,7 @@
@font-face {
font-family: Lato;
src: url('../../fonts/lato/lato-normal.woff2') format('woff2'),
url('../../fonts/lato/lato-normal.woff') format('woff');
font-weight: normal;
font-style: normal;
}

View File

@@ -0,0 +1,249 @@
.form-control {
border: 0;
border-bottom: 1px solid @input-border;
color: #32393F;
padding: 5px;
width: 100%;
font-size: 13px;
background-color: transparent;
}
select.form-control {
-webkit-appearance: none;
-moz-appearance: none;
border-radius: 0;
background: url(../../img/select-caret.svg) no-repeat bottom 7px right;
}
/*--------------------------
Input Group
----------------------------*/
.input-group {
position: relative;
&:not(:last-child) {
margin-bottom: 25px;
}
label:not(.ig-label) {
font-size: 13px;
display: block;
margin-bottom: 10px;
}
}
.ig-label {
position: absolute;
text-align: center;
bottom: 7px;
left: 0;
width: 100%;
.transition(all);
.transition-duration(250ms);
padding: 2px 0 3px;
border-radius: 2px;
font-weight: 400;
}
.ig-helpers {
z-index: 1;
width: 100%;
left: 0;
&,
&:before,
&:after {
position: absolute;
height: 2px;
bottom: 0;
}
&:before,
&:after {
content: '';
width: 0;
.transition(all);
.transition-duration(250ms);
background-color: #03A9F4;
}
&:before {
left: 50%;
}
&:after {
right: 50%;
}
}
.ig-text {
width: 100%;
height: 40px;
border: 0;
background: transparent;
text-align: center;
position: relative;
z-index: 1;
border-bottom: 1px solid #eee;
color: #32393F;
font-size: 13px;
&:focus + .ig-helpers {
&:before,
&:after {
width: 50%;
}
}
&:valid,
&:disabled,
&:focus {
& ~ .ig-label {
bottom: 35px;
font-size: 13px;
z-index: 1;
}
}
&:disabled {
.opacity(0.5);
}
}
.ig-dark {
.ig-text {
color: @white;
border-color: rgba(255,255,255,0.1);
}
.ig-helpers {
&:before,
&:after {
background-color: #dfdfdf;
height: 1px;
}
}
}
.ig-left {
.ig-label,
.ig-text {
text-align: left;
}
}
.ig-error {
.ig-label {
color: #E23F3F;
}
.ig-helpers i {
&:first-child,
&:first-child:before,
&:first-child:after {
background: rgba(226, 63, 63, 0.43);
}
&:last-child,
&:last-child:before,
&:last-child:after {
background: #E23F3F !important;
}
}
&:after {
content: "\f05a";
font-family: FontAwesome;
position: absolute;
top: 17px;
right: 9px;
font-size: 20px;
color: #D33D3E;
}
}
.ig-search {
&:before {
font-family: @font-family-icon;
content: '\f002';
font-size: 15px;
position: absolute;
left: 2px;
top: 8px;
}
.ig-text {
padding-left: 25px;
}
}
/*--------------------------
Share Spinners
----------------------------*/
.set-expire {
border: 1px solid @input-border;
margin: 35px 0 30px;
}
.set-expire-item {
padding: 9px 5px 3px;
position: relative;
display: table-cell;
width: 1%;
text-align: center;
&:not(:last-child) {
border-right: 1px solid @input-border;
}
}
.set-expire-title {
font-size: 10px;
text-transform: uppercase;
}
.set-expire-value {
display: inline-block;
overflow: hidden;
position: relative;
left: -8px;
input {
font-size: 20px;
text-align: center;
position: relative;
right: -15px;
border: 0;
color: @text-strong-color;
padding: 0;
height: 25px;
width: 100%;
font-weight: normal;
}
}
.set-expire-decrease,
.set-expire-increase {
position: absolute;
width: 20px;
height: 20px;
background: url(../../img/arrow.svg) no-repeat center;
background-size: 85%;
left: 50%;
margin-left: -10px;
.opacity(0.2);
cursor: pointer;
&:hover {
.opacity(0.5);
}
}
.set-expire-increase {
top: -25px;
}
.set-expire-decrease {
bottom: -27px;
.rotate(-180deg);
}

View File

@@ -0,0 +1,83 @@
/*----------------------------
Text Alignment
-----------------------------*/
.text-center { text-align: center !important; }
.text-left { text-align: left !important; }
.text-right { text-align: right !important; }
/*----------------------------
Float
-----------------------------*/
.clearfix { .clearfix(); }
.pull-right { float: right !important; }
.pull-left { float: left !important; }
/*----------------------------
Position
-----------------------------*/
.p-relative { position: relative; }
/*---------------------------------------------------------------------------
Generate Margin Class
margin, margin-top, margin-bottom, margin-left, margin-right
----------------------------------------------------------------------------*/
.margin (@label, @size: 1, @key:1) when (@size =< 30){
.m-@{key} {
margin: @size !important;
}
.m-t-@{key} {
margin-top: @size !important;
}
.m-b-@{key} {
margin-bottom: @size !important;
}
.m-l-@{key} {
margin-left: @size !important;
}
.m-r-@{key} {
margin-right: @size !important;
}
.margin(@label - 5; @size + 5; @key + 5);
}
.margin(25, 0px, 0);
/*---------------------------------------------------------------------------
Generate Padding Class
padding, padding-top, padding-bottom, padding-left, padding-right
----------------------------------------------------------------------------*/
.padding (@label, @size: 1, @key:1) when (@size =< 30){
.p-@{key} {
padding: @size !important;
}
.p-t-@{key} {
padding-top: @size !important;
}
.p-b-@{key} {
padding-bottom: @size !important;
}
.p-l-@{key} {
padding-left: @size !important;
}
.p-r-@{key} {
padding-right: @size !important;
}
.padding(@label - 5; @size + 5; @key + 5);
}
.padding(25, 0px, 0);

View File

@@ -0,0 +1,242 @@
/*--------------------------
Header
----------------------------*/
.fe-header {
padding: 45px 55px 20px;
@media(min-width: @screen-md-min) {
position: relative;
}
@media(max-width: (@screen-xs-max - 100)) {
padding: 25px 25px 20px;
}
h2 {
font-size: 16px;
font-weight: normal;
margin: 0;
& > span {
margin-bottom: 7px;
display: inline-block;
&:not(:first-child) {
&:before {
content: '/';
margin: 0 4px;
color: @text-color;
}
}
}
}
p {
margin-top: 7px;
}
}
/*--------------------------
Disk usage
----------------------------*/
.feh-usage {
margin-top: 12px;
max-width: 285px;
@media(max-width: (@screen-xs-max - 100px)) {
max-width: 100%;
font-size: 12px;
}
& > ul {
margin-top: 7px;
list-style: none;
padding: 0;
& > li {
padding-right: 0;
display: inline-block;
}
}
}
.fehu-chart {
height: 5px;
background: #eee;
position: relative;
border-radius: 2px;
overflow: hidden;
& > div {
position: absolute;
left: 0;
height: 100%;
background: @link-color;
}
}
/*--------------------------
Header Actions
----------------------------*/
.feh-actions {
list-style: none;
padding: 0;
margin: 0;
position: absolute;
right: 35px;
top: 30px;
z-index: 11;
@media(max-width: (@screen-sm-max)) {
top: 7px;
right: 10px;
position: fixed;
}
& > li {
display: inline-block;
text-align: right;
vertical-align: top;
line-height: 100%;
& > a,
& > .btn-group > button {
display: block;
height: 45px;
min-width: 45px;
text-align: center;
border-radius: 50%;
padding: 0;
border: 0;
background: none;
@media(min-width: @screen-md-min) {
color: #7B7B7B;
font-size: 21px;
line-height: 45px;
.transition(all);
.transition-duration(300ms);
&:hover {
background: rgba(0,0,0,0.09);
}
}
@media(max-width: (@screen-sm-max)) {
background: url(../../img/more-h-light.svg) no-repeat center;
.fa-reorder {
display: none;
}
}
}
}
}
/*--------------------------
Mobile Header
----------------------------*/
@media(max-width: @screen-sm-max) {
.fe-header-mobile {
background-color: @dark-gray;
padding: 10px 50px 9px 12px;
text-align: center;
position: fixed;
z-index: 10;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
left: 0;
top: 0;
width: 100%;
.mh-logo {
height: 35px;
position: relative;
top: 4px;
}
}
.feh-trigger {
width: 41px;
height: 41px;
cursor: pointer;
float: left;
position: relative;
text-align: center;
&:before,
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
}
&:after {
z-index: 1;
}
&:before {
background: rgba(255, 255, 255, 0.1);
.transition(all);
.transition-duration(300ms);
.scale(0);
}
}
.feht-toggled {
&:before {
.scale(1);
}
.feht-lines {
.rotate(180deg);
& > div {
&.top {
width: 12px;
transform: translateX(8px) translateY(1px) rotate(45deg);
-webkit-transform: translateX(8px) translateY(1px) rotate(45deg);
}
&.bottom {
width: 12px;
transform: translateX(8px) translateY(-1px) rotate(-45deg);
-webkit-transform: translateX(8px) translateY(-1px) rotate(-45deg);
}
}
}
}
.feht-lines,
.feht-lines > div {
.transition(all);
.transition-duration(300ms);
}
.feht-lines {
width: 18px;
height: 12px;
display: inline-block;
margin-top: 14px;
& > div {
background-color: #EAEAEA;
width: 18px;
height: 2px;
&.center {
margin: 3px 0;
}
}
}
}

View File

@@ -0,0 +1,81 @@
.ie-warning {
background-color: #ff5252;
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
text-align: center;
&:before {
width: 1px;
content: '';
height: 100%;
}
&:before,
.iw-inner {
display: inline-block;
vertical-align: middle;
}
}
.iw-inner {
width: 470px;
height: 300px;
background-color: @white;
border-radius: 5px;
padding: 40px;
position: relative;
ul {
list-style: none;
padding: 0;
margin: 0;
width: 230px;
margin-left: 80px;
margin-top: 16px;
& > li {
float: left;
& > a {
display: block;
padding: 10px 15px 7px;
font-size: 14px;
margin: 0 1px;
border-radius: 3px;
&:hover {
background: #eee;
}
img {
height: 40px;
margin-bottom: 5px;
}
}
}
}
}
.iwi-icon {
color: #ff5252;
font-size: 40px;
display: block;
line-height: 100%;
margin-bottom: 15px;
}
.iwi-skip {
position: absolute;
left: 0;
bottom: -35px;
width: 100%;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
&:hover {
color: @white;
}
}

View File

@@ -0,0 +1,352 @@
/*--------------------------
Row
----------------------------*/
.fesl-row {
padding-right: 40px;
padding-top: 5px;
padding-bottom: 5px;
position: relative;
@media (min-width: (@screen-sm-min - 100px)) {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
}
.clearfix();
}
header.fesl-row {
@media (min-width:(@screen-sm-min - 100px)) {
margin-bottom: 20px;
border-bottom: 1px solid lighten(@text-muted-color, 20%);
padding-left: 40px;
.fesl-item,
.fesli-sort {
.transition(all);
.transition-duration(300ms);
}
.fesl-item {
cursor: pointer;
color: @text-color;
font-weight: 500;
margin-bottom: -5px;
& > .fesli-sort {
float: right;
margin: 4px 0 0;
.opacity(0);
color: @dark-gray;
font-size: 14px;
}
&:hover:not(.fi-actions) {
background: lighten(@text-muted-color, 22%);
color: @dark-gray;
& > .fesli-sort {
.opacity(0.5);
}
}
}
}
@media (max-width:(@screen-xs-max - 100px)) {
display: none;
}
}
div.fesl-row {
padding-left: 85px;
border-bottom: 1px solid transparent;
cursor: default;
@media (max-width: (@screen-xs-max - 100px)) {
padding-left: 70px;
padding-right: 45px;
}
&:nth-child(even) {
background-color: #fafafa;
}
&:hover {
background-color: #fbf7dc;
}
&[data-type]:before {
font-family: @font-family-icon;
width: 35px;
height: 35px;
text-align: center;
line-height: 35px;
position: absolute;
border-radius: 50%;
font-size: 16px;
left: 50px;
top: 9px;
color: @white;
@media (max-width: (@screen-xs-max - 100px)) {
left: 20px;
}
}
&[data-type="folder"] {
@media (max-width: (@screen-xs-max - 100px)) {
.fesl-item {
&.fi-name {
padding-top: 10px;
padding-bottom: 7px;
}
&.fi-size,
&.fi-modified {
display: none;
}
}
}
}
/*--------------------------
Icons
----------------------------*/
&[data-type=folder]:before {
content: '\f114';
background-color: #a1d6dd;
}
&[data-type=pdf]:before {
content: "\f1c1";
background-color: #fa7775;
}
&[data-type=zip]:before {
content: "\f1c6";
background-color: #427089;
}
&[data-type=audio]:before {
content: "\f1c7";
background-color: #009688
}
&[data-type=code]:before {
content: "\f1c9";
background-color: #997867;
}
&[data-type=excel]:before {
content: "\f1c3";
background-color: #64c866;
}
&[data-type=image]:before {
content: "\f1c5";
background-color: #f06292;
}
&[data-type=video]:before {
content: "\f1c8";
background-color: #f8c363;
}
&[data-type=other]:before {
content: "\f016";
background-color: #afafaf;
}
&[data-type=text]:before {
content: "\f0f6";
background-color: #8a8a8a;
}
&[data-type=doc]:before {
content: "\f1c2";
background-color: #2196f5;
}
&[data-type=presentation]:before {
content: "\f1c4";
background-color: #896ea6;
}
&.fesl-loading{
&:before {
content: '';
}
&:after {
.list-loader(20px, 20px, rgba(255, 255, 255, 0.5), @white);
left: 57px;
top: 17px;
@media (max-width: (@screen-xs-max - 100px)) {
left: 27px;
}
}
}
}
/*--------------------------
Files and Folders
----------------------------*/
.fesl-item {
display: block;
a {
color: darken(@text-color, 5%);
}
@media(min-width: (@screen-sm-min - 100px)) {
&:not(.fi-actions) {
text-overflow: ellipsis;
padding: 10px 15px;
white-space: nowrap;
overflow: hidden;
}
&.fi-name {
flex: 3;
}
&.fi-size {
width: 140px;
}
&.fi-modified {
width: 190px;
}
&.fi-actions {
width: 40px;
}
}
@media(max-width: (@screen-xs-max - 100px)) {
padding: 0;
&.fi-name {
width: 100%;
margin-bottom: 3px;
}
&.fi-size,
&.fi-modified {
font-size: 12px;
color: #B5B5B5;
float: left;
}
&.fi-modified {
max-width: 72px;
white-space: nowrap;
overflow: hidden;
}
&.fi-size {
margin-right: 10px;
}
&.fi-actions {
position: absolute;
top: 5px;
right: 10px;
}
}
}
/*--------------------------
Action buttons
----------------------------*/
.fia-toggle {
height: 36px;
width: 36px;
background: transparent url(../../img/more-h.svg) no-repeat center;
position: relative;
top: 3px;
.opacity(0.4);
&:hover {
.opacity(0.7);
}
}
.fi-actions {
.dropdown-menu {
background-color: transparent;
box-shadow: none;
padding: 0;
right: 38px;
left: auto;
margin: 0;
height: 100%;
text-align: right;
}
.dropdown {
&.open {
.dropdown-menu {
.fiad-action {
right: 0;
}
}
}
}
}
.fiad-action {
height: 35px;
width: 35px;
background: @amber;
display: inline-block;
border-radius: 50%;
text-align: center;
line-height: 35px;
font-weight: normal;
position: relative;
top: 4px;
margin-left: 5px;
.animation-name(fiad-action-anim);
.transform-origin(center center);
.backface-visibility(none);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:nth-child(2) {
.animation-duration(100ms);
}
&:nth-child(1) {
.animation-duration(250ms);
}
& > i {
font-size: 14px;
color: @white;
}
&:hover {
background-color: darken(@amber, 3%);
}
}
@-webkit-keyframes fiad-action-anim {
from {
.scale(0);
.opacity(0);
right: -20px;
}
to {
.scale(1);
.opacity(1);
right: 0;
}
}
@keyframes fiad-action-anim {
from {
.scale(0);
.opacity(0);
right: -20px;
}
to {
.scale(1);
.opacity(1);
right: 0;
}
}

View File

@@ -0,0 +1,104 @@
.login {
height: 100vh;
min-height: 500px;
background: @dark-gray;
text-align: center;
&:before {
height: ~"calc(100% - 110px)";
width: 1px;
content: "";
}
}
.l-wrap,
.login:before {
display: inline-block;
vertical-align: middle;
}
.l-wrap {
width: 80%;
max-width: 500px;
margin-top: -50px;
&.toggled {
display: inline-block;
}
.input-group:not(:last-child) {
margin-bottom: 40px;
}
}
.l-footer {
height: 110px;
padding: 0 50px;
}
.lf-logo {
float: right;
img {
width: 40px;
}
}
.lf-server {
float: left;
color: rgba(255, 255, 255, 0.4);
font-size: 20px;
font-weight: 400;
padding-top: 40px;
}
@media (max-width: @screen-sm-min) {
.lf-logo,
.lf-server {
float: none;
display: block;
text-align: center;
width: 100%;
}
.lf-logo {
margin-bottom: 5px;
}
.lf-server {
font-size: 15px;
}
}
.lw-btn {
width: 50px;
height: 50px;
border: 1px solid @white;
display: inline-block;
border-radius: 50%;
font-size: 22px;
color: @white;
.transition(all);
.transition-duration(300ms);
opacity: 0.3;
background-color: transparent;
line-height: 45px;
padding: 0;
&:hover {
color: @white;
opacity: 0.8;
border-color: @white;
}
i {
display: block;
width: 100%;
padding-left: 3px;
}
}
/*------------------------------
Chrome autofill fix
-------------------------------*/
input:-webkit-autofill {
-webkit-box-shadow:0 0 0 50px @dark-gray inset !important;
-webkit-text-fill-color: @white !important;
}

View File

@@ -0,0 +1,102 @@
/*--------------------------
Close
----------------------------*/
.close-variant(@color, @bg-color, @color-hover, @bg-color-hover) {
span {
background-color: @bg-color;
color: @color;
}
&:hover,
&:focus {
span {
background-color: @bg-color-hover;
color: @color-hover;
}
}
}
.close {
right: 15px;
font-weight: normal;
opacity: 1;
font-size: 18px;
position: absolute;
text-align: center;
top: 16px;
z-index: 1;
padding: 0;
border: 0;
background-color: transparent;
span {
width: 25px;
height: 25px;
display: block;
border-radius: 50%;
line-height: 24px;
text-shadow: none;
}
&:not(.close-alt) {
.close-variant(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1), @white, rgba(255, 255, 255, 0.2));
}
}
.close-alt {
.close-variant(#989898, #efefef, #7b7b7b, #e8e8e8);
}
/*--------------------------
Hidden
----------------------------*/
.hidden {
display: none !important;
}
/*--------------------------
Copy text
----------------------------*/
.copy-text {
input {
width: 100%;
border-radius: 1px;
border: 1px solid @input-border;
padding: 7px 12px;
font-size: 13px;
line-height: 100%;
cursor: text;
.transition(border-color);
.transition-duration(300ms);
&:hover {
border-color: darken(@input-border, 5%);
}
}
}
/*--------------------------
Sharing
----------------------------*/
.share-availability {
margin-bottom: 40px;
&:before,
&:after {
position: absolute;
bottom: -30px;
font-size: 10px;
}
&:before {
content: '01 Sec';
left: 0;
}
&:after {
content: '7 days';
right: 0;
}
}

View File

@@ -0,0 +1,52 @@
/*--------------------------
User Select
----------------------------*/
.user-select(@value) {
-webkit-user-select: @value;
-moz-user-select: @value;
-ms-user-select: @value;
user-select: @value;
}
/*----------------------------------------
CSS Animations based on animate.css
-----------------------------------------*/
.animated(@name, @duration) {
-webkit-animation-name: @name;
animation-name: @name;
-webkit-animation-duration: @duration;
animation-duration: @duration;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
/*-------------------------------------------------
For loop mixin for generate custom classes
--------------------------------------------------*/
.for(@i, @n) {.-each(@i)}
.for(@n) when (isnumber(@n)) {.for(1, @n)}
.for(@i, @n) when not (@i = @n) {
.for((@i + (@n - @i) / abs(@n - @i)), @n);
}
.for(@array) when (default()) {.for-impl_(length(@array))}
.for-impl_(@i) when (@i > 1) {.for-impl_((@i - 1))}
.for-impl_(@i) when (@i > 0) {.-each(extract(@array, @i))}
/*----------------------------------------
List Loader
-----------------------------------------*/
.list-loader(@width, @height, @borderColor, @borderColorBottom) {
content: '';
width: @width;
height: @height;
border-radius: 50%;
.animated(zoomIn, 500ms);
border: 2px solid @borderColor;
border-bottom-color: @borderColorBottom;
position: absolute;
z-index: 1;
-webkit-animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
}

View File

@@ -0,0 +1,294 @@
/*--------------------------
Modal
----------------------------*/
.modal {
@media(min-width: @screen-sm-min) {
text-align: center;
&:before {
content: '';
height: 100%;
width: 1px;
display: inline-block;
vertical-align: middle;
}
.modal-dialog {
text-align: left;
margin: 10px auto;
display: inline-block;
vertical-align: middle;
}
}
}
.modal-dark {
.modal-header {
color: rgba(255, 255, 255, 0.4);
small {
color: rgba(255, 255, 255, 0.2);
}
}
.modal-content {
background-color: @dark-gray;
}
}
.modal-backdrop {
.animated(fadeIn, 200ms);
}
.modal-dialog {
.animated(zoomIn, 200ms);
}
.modal-header {
color: @text-strong-color;
position: relative;
small {
display: block;
text-transform: none;
font-size: 12px;
margin-top: 5px;
color: #a8a8a8;
}
}
.modal-content {
border-radius: 3px;
box-shadow: none;
}
.modal-footer {
padding: 0 30px 30px;
text-align: center;
}
/*--------------------------
Dialog
----------------------------*/
.modal-confirm {
.modal-dialog {
text-align: center;
}
}
.mc-icon {
margin: 0 0 10px;
& > i {
font-size: 60px;
}
}
.mci-red {
color: #ff8f8f;
}
.mci-amber {
color: @amber;
}
.mci-green {
color: #64e096;
}
.mc-text {
color: @text-strong-color;
}
.mc-sub {
color: @text-muted-color;
margin-top: 5px;
font-size: 13px;
}
//--------------------------
/*--------------------------
About
----------------------------*/
.modal-about {
@media (max-width: @screen-xs-max) {
text-align: center;
.modal-dialog {
max-width: 400px;
width: 90%;
margin: 20px auto 0;
}
}
}
.ma-inner {
display: flex;
flex-direction: row;
align-items: center;
min-height: 350px;
position: relative;
@media (min-width: @screen-sm-min) {
&:before {
content: '';
width: 150px;
height: 100%;
top: 0;
left: 0;
position: absolute;
border-radius: 3px 0 0px 3px;
background-color: #23282C;
}
}
}
.mai-item {
&:first-child {
width: 150px;
text-align: center;
}
&:last-child {
flex: 4;
padding: 30px;
}
}
.maii-logo {
width: 70px;
position: relative;
}
.maii-list {
list-style: none;
padding: 0;
& > li {
margin-bottom: 15px;
div {
color: rgba(255, 255, 255, 0.8);
text-transform: uppercase;
font-size: 14px;
}
small {
font-size: 13px;
color: rgba(255, 255, 255, 0.4);
}
}
}
//--------------------------
/*--------------------------
Preferences
----------------------------*/
.toggle-password {
position: absolute;
bottom: 30px;
right: 35px;
width: 30px;
height: 30px;
border: 1px solid #eee;
border-radius: 0;
text-align: center;
cursor: pointer;
z-index: 10;
background-color: @white;
padding-top: 5px;
&.toggled {
background: #eee;
}
}
//--------------------------
/*--------------------------
Policy
----------------------------*/
.pm-body {
padding-bottom: 30px;
}
.pmb-header {
margin-bottom: 35px;
}
.pmb-list {
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: center;
padding: 10px 35px;
&:nth-child(even) {
background-color: #F7F7F7;
}
.form-control {
padding-left: 0;
padding-right: 0;
}
}
header.pmb-list {
margin: 20px 0 10px;
}
.pmbl-item {
display: block;
font-size: 13px;
&:nth-child(1) {
flex: 2;
}
&:nth-child(2) {
margin: 0 25px;
width: 150px;
}
&:nth-child(3) {
width: 70px;
}
}
div.pmb-list {
select {
border: 0;
}
.pml-item {
&:not(:last-child) {
padding: 0 5px;
}
}
}
//--------------------------
/*--------------------------
Create Bucket
----------------------------*/
.modal-create-bucket {
.modal-dialog {
position: fixed;
right: 25px;
bottom: 95px;
margin: 0;
height: 110px;
}
.modal-content {
width: 100%;
height: 100%;
}
}
//--------------------------

View File

@@ -0,0 +1,187 @@
/*--------------------------
Sidebar
----------------------------*/
.fe-sidebar {
width: @fe-sidebar-width;
background-color: @dark-gray;
position: fixed;
height: 100%;
overflow: hidden;
padding: 35px;
@media(min-width: @screen-md-min) {
.translate3d(0, 0, 0);
}
@media(max-width: @screen-sm-max) {
padding-top: 85px;
z-index: 9;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.65);
.transition(all);
.transition-duration(300ms);
.translate3d((-@fe-sidebar-width - 15px), 0, 0);
&.toggled {
.translate3d(0, 0, 0);
}
}
a {
color: rgba(255, 255, 255, 0.58);
&:hover {
color: @white;
}
}
}
/*--------------------------
Header
----------------------------*/
.fes-header {
margin-bottom: 40px;
img,
h2 {
float: left;
}
h2 {
margin: 13px 0 0 10px;
font-weight: normal;
}
img {
width: 32px;
}
}
/*--------------------------
List
----------------------------*/
.fesl-inner {
height: ~"calc(100vh - 260px)";
overflow: auto;
padding: 0;
margin: 0 -35px;
& li {
position: relative;
& > a {
display: block;
padding: 10px 40px 12px 65px;
.text-overflow();
&:before {
font-family: FontAwesome;
content: '\f0a0';
font-size: 17px;
position: absolute;
top: 10px;
left: 35px;
.opacity(0.8);
}
&.fesli-loading {
&:before {
.list-loader(20px, 20px, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.5));
left: 32px;
top: 0;
bottom: 0;
margin: auto;
}
}
}
&.active {
background-color: rgba(0, 0, 0, 0.2);
& > a {
color: @white;
}
}
&:not(.active):hover {
background-color: rgba(0, 0, 0, 0.1);
& > a {
color: @white;
}
}
&:hover {
.fesli-trigger {
.opacity(0.6);
&:hover {
.opacity(1);
}
}
}
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
&:hover .scrollbar-vertical {
opacity: 1;
}
}
.fesli-trigger {
.opacity(0);
.transition(all);
.transition-duration(200ms);
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 100%;
cursor: pointer;
background: url(../../img/more-h-light.svg) no-repeat left;
}
/* Scrollbar */
.scrollbar-vertical {
position: absolute;
right: 5px;
width: 4px;
height: 100%;
opacity: 0;
.transition(opacity);
.transition-duration(300ms);
div {
border-radius: 1px !important;
background-color: #6a6a6a !important;
}
}
/*--------------------------
Host
----------------------------*/
.fes-host {
position: fixed;
left: 0;
bottom: 0;
z-index: 1;
background: @dark-gray;
color: rgba(255, 255, 255, 0.4);
font-size: 15px;
font-weight: 400;
width: @fe-sidebar-width;
padding: 20px;
.text-overflow();
& > i {
margin-right: 10px;
}
}

View File

@@ -0,0 +1,94 @@
/*--------------------------
Base
----------------------------*/
@font-family-sans-serif : 'Lato', sans-serif;
@font-family-icon : 'fontAwesome';
@body-bg : #edecec;
@text-color : #8e8e8e;
@font-size-base : 15px;
@link-color : #46a5e0;
@link-hover-decoration : none;
/*--------------------------
File Explorer
----------------------------*/
@fe-sidebar-width : 300px;
@text-muted-color : #BDBDBD;
@text-strong-color : #333;
/*--------------------------
Colors
----------------------------*/
@cyan : #2ED2FF;
@amber : #ffc107;
@red : #ff726f;
@grey : #f5f5f5;
@dark-blue : #0084d3;
@blue : #00a6f7;
@white : #ffffff;
@black : #1b1e25;
@blue : #50b2ff;
@light-blue : #c1d1e8;
@green : #33d46f;
@yellow : #FFC107;
@orange : #ffc155;
@purple : #9C27B0;
@teal : #009688;
@brown : #795548;
@blue-gray : #374952;
@dark-gray : #32393F;
/*--------------------------
Dropdown
----------------------------*/
@dropdown-fallback-border : transparent;
@dropdown-border : transparent;
@dropdown-divider-bg : '';
@dropdown-link-hover-bg : rgba(0,0,0,0.05);
@dropdown-link-color : @text-color;
@dropdown-link-hover-color : #333;
@dropdown-link-disabled-color : #e4e4e4;
@dropdown-divider-bg : rgba(0,0,0,0.08);
@dropdown-link-active-color : #333;
@dropdown-link-active-bg : rgba(0, 0, 0, 0.075);
@dropdown-shadow : 0 2px 10px rgba(0, 0, 0, 0.2);
/*--------------------------
Modal
----------------------------*/
@modal-content-fallback-border-color: transparent;
@modal-content-border-color: transparent;
@modal-backdrop-bg: rgba(0,0,0,0.1);
@modal-header-border-color: transparent;
@modal-title-line-height: transparent;
@modal-footer-border-color: transparent;
@modal-inner-padding: 30px 35px;
@modal-title-padding: 30px 35px 0px;
@modal-sm: 400px;
/*-------------------------
Buttons
--------------------------*/
@btn-border-radius-large: 2px;
@btn-border-radius-small: 2px;
@btn-border-radius-base: 2px;
/*-------------------------
Colors
--------------------------*/
@brand-primary: #2196F3;
@brand-success: #4CAF50;
@brand-info: #00BCD4;
@brand-warning: #FF9800;
@brand-danger: #FF5722;
/*-------------------------
Form
--------------------------*/
@input-border: #eee;

View File

@@ -0,0 +1,39 @@
/*----------------------------
Bootstrap
-----------------------------*/
@import "../../node_modules/bootstrap/less/scaffolding.less";
@import "../../node_modules/bootstrap/less/variables.less";
@import "../../node_modules/bootstrap/less/grid.less";
@import "../../node_modules/bootstrap/less/mixins.less";
@import "../../node_modules/bootstrap/less/normalize.less";
@import "../../node_modules/bootstrap/less/dropdowns.less";
@import "../../node_modules/bootstrap/less/modals.less";
@import "../../node_modules/bootstrap/less/tooltip.less";
@import "../../node_modules/bootstrap/less/responsive-utilities.less";
/*----------------------------
App
-----------------------------*/
@import 'inc/mixin';
@import 'inc/variables';
@import 'inc/base';
@import 'inc/animate/animate';
@import 'inc/generics';
@import 'inc/font';
@import 'inc/form';
@import 'inc/buttons';
@import 'inc/misc';
@import 'inc/login';
@import 'inc/header';
@import 'inc/sidebar';
@import 'inc/list';
@import 'inc/file-explorer';
@import 'inc/ie-warning';
/*----------------------------
Boostrap
-----------------------------*/
@import 'inc/dropdown';
@import 'inc/alert';
@import 'inc/modal';

126
browser/build.js Normal file
View File

@@ -0,0 +1,126 @@
/*
* Minio Browser (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.
*/
var moment = require('moment')
var async = require('async')
var exec = require('child_process').exec
var fs = require('fs')
var isProduction = process.env.NODE_ENV == 'production' ? true : false
var assetsFileName = ''
var commitId = ''
var date = moment.utc()
var version = date.format('YYYY-MM-DDTHH:mm:ss') + 'Z'
var releaseTag = date.format('YYYY-MM-DDTHH-mm-ss') + 'Z'
var buildType = 'DEVELOPMENT'
if (process.env.MINIO_UI_BUILD) buildType = process.env.MINIO_UI_BUILD
rmDir = function(dirPath) {
try { var files = fs.readdirSync(dirPath); }
catch(e) { return; }
if (files.length > 0)
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
if (fs.statSync(filePath).isFile())
fs.unlinkSync(filePath);
else
rmDir(filePath);
}
fs.rmdirSync(dirPath);
};
async.waterfall([
function(cb) {
rmDir('production');
rmDir('dev');
var cmd = 'webpack -p --config webpack.production.config.js'
if (!isProduction) {
cmd = 'webpack';
}
console.log('Running', cmd)
exec(cmd, cb)
},
function(stdout, stderr, cb) {
if (isProduction) {
fs.renameSync('production/index_bundle.js',
'production/index_bundle-' + releaseTag + '.js')
} else {
fs.renameSync('dev/index_bundle.js',
'dev/index_bundle-' + releaseTag + '.js')
}
var cmd = 'git log --format="%H" -n1'
console.log('Running', cmd)
exec(cmd, cb)
},
function(stdout, stderr, cb) {
if (!stdout) throw new Error('commitId is empty')
commitId = stdout.replace('\n', '')
if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
assetsFileName = 'ui-assets.go';
var cmd = 'go-bindata-assetfs -pkg miniobrowser -nocompress=true production/...'
if (!isProduction) {
cmd = 'go-bindata-assetfs -pkg miniobrowser -nocompress=true dev/...'
}
console.log('Running', cmd)
exec(cmd, cb)
},
function(stdout, stderr, cb) {
var cmd = 'gofmt -s -w -l bindata_assetfs.go'
console.log('Running', cmd)
exec(cmd, cb)
},
function(stdout, stderr, cb) {
fs.renameSync('bindata_assetfs.go', assetsFileName)
fs.appendFileSync(assetsFileName, '\n')
fs.appendFileSync(assetsFileName, 'var UIReleaseTag = "' + buildType + '.' +
releaseTag + '"\n')
fs.appendFileSync(assetsFileName, 'var UICommitID = "' + commitId + '"\n')
fs.appendFileSync(assetsFileName, 'var UIVersion = "' + version + '"')
fs.appendFileSync(assetsFileName, '\n')
var contents;
if (isProduction) {
contents = fs.readFileSync(assetsFileName, 'utf8')
.replace(/_productionIndexHtml/g, '_productionIndexHTML')
.replace(/productionIndexHtmlBytes/g, 'productionIndexHTMLBytes')
.replace(/productionIndexHtml/g, 'productionIndexHTML')
.replace(/_productionIndex_bundleJs/g, '_productionIndexBundleJs')
.replace(/productionIndex_bundleJsBytes/g, 'productionIndexBundleJsBytes')
.replace(/productionIndex_bundleJs/g, 'productionIndexBundleJs')
.replace(/_productionJqueryUiMinJs/g, '_productionJqueryUIMinJs')
.replace(/productionJqueryUiMinJsBytes/g, 'productionJqueryUIMinJsBytes')
.replace(/productionJqueryUiMinJs/g, 'productionJqueryUIMinJs');
} else {
contents = fs.readFileSync(assetsFileName, 'utf8')
.replace(/_devIndexHtml/g, '_devIndexHTML')
.replace(/devIndexHtmlBytes/g, 'devIndexHTMLBytes')
.replace(/devIndexHtml/g, 'devIndexHTML')
.replace(/_devIndex_bundleJs/g, '_devIndexBundleJs')
.replace(/devIndex_bundleJsBytes/g, 'devIndexBundleJsBytes')
.replace(/devIndex_bundleJs/g, 'devIndexBundleJs')
.replace(/_devJqueryUiMinJs/g, '_devJqueryUIMinJs')
.replace(/devJqueryUiMinJsBytes/g, 'devJqueryUIMinJsBytes')
.replace(/devJqueryUiMinJs/g, 'devJqueryUIMinJs');
}
contents = contents.replace(/MINIO_UI_VERSION/g, version)
contents = contents.replace(/index_bundle.js/g, 'index_bundle-' + releaseTag + '.js')
fs.writeFileSync(assetsFileName, contents, 'utf8')
console.log('UI assets file :', assetsFileName)
cb()
}
], function(err) {
if (err) return console.log(err)
})

40
browser/karma.conf.js Normal file
View File

@@ -0,0 +1,40 @@
var webpack = require('webpack');
module.exports = function (config) {
config.set({
browsers: [ process.env.CONTINUOUS_INTEGRATION ? 'Firefox' : 'Chrome' ],
singleRun: true,
frameworks: [ 'mocha' ],
files: [
'tests.webpack.js'
],
preprocessors: {
'tests.webpack.js': [ 'webpack' ]
},
reporters: [ 'dots' ],
webpack: {
module: {
loaders: [{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel',
query: {
presets: ['react', 'es2015']
}
}, {
test: /\.less$/,
loader: 'style!css!less'
}, {
test: /\.css$/,
loader: 'style!css'
}, {
test: /\.(eot|woff|woff2|ttf|svg|png)/,
loader: 'url'
}]
}
},
webpackServer: {
noInfo: true
}
});
};

82
browser/package.json Normal file
View File

@@ -0,0 +1,82 @@
{
"name": "minio-browser",
"version": "0.0.1",
"description": "Minio Browser",
"scripts": {
"test": "karma start",
"dev": "NODE_ENV=dev webpack-dev-server --devtool eval --progress --colors --hot --content-base dev",
"build": "NODE_ENV=dev node build.js",
"release": "NODE_ENV=production MINIO_UI_BUILD=RELEASE node build.js",
"format": "esformatter -i 'app/**/*.js'"
},
"repository": {
"type": "git",
"url": "https://github.com/minio/miniobrowser"
},
"author": "Minio Inc",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/minio/miniobrowser/issues"
},
"homepage": "https://github.com/minio/miniobrowser",
"devDependencies": {
"async": "^1.5.2",
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-loader": "^6.2.5",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-preset-es2015": "^6.14.0",
"babel-preset-react": "^6.11.1",
"babel-register": "^6.14.0",
"copy-webpack-plugin": "^0.3.3",
"css-loader": "^0.23.1",
"esformatter": "^0.10.0",
"esformatter-jsx-ignore": "^1.0.6",
"expect": "^1.20.2",
"history": "^1.17.0",
"html-webpack-plugin": "^2.22.0",
"json-loader": "^0.5.4",
"karma": "^0.13.22",
"karma-chrome-launcher": "^0.2.3",
"karma-cli": "^0.1.2",
"karma-firefox-launcher": "^0.1.7",
"karma-mocha": "^0.2.2",
"karma-webpack": "^1.7.0",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"mocha": "^2.5.3",
"moment": "^2.15.1",
"purifycss-webpack-plugin": "^2.0.3",
"react": "^0.14.8",
"react-addons-test-utils": "^0.14.8",
"react-bootstrap": "^0.28.5",
"react-custom-scrollbars": "^2.3.0",
"react-redux": "^4.4.5",
"react-router": "^2.8.1",
"redux": "^3.6.0",
"redux-thunk": "^1.0.3",
"style-loader": "^0.13.1",
"superagent": "^1.8.4",
"superagent-es6-promise": "^1.0.0",
"url-loader": "^0.5.7",
"webpack": "^1.12.11",
"webpack-dev-server": "^1.14.1"
},
"dependencies": {
"bootstrap": "^3.3.6",
"classnames": "^2.2.3",
"font-awesome": "^4.7.0",
"humanize": "0.0.9",
"json-loader": "^0.5.4",
"local-storage-fallback": "^1.3.0",
"mime-db": "^1.25.0",
"mime-types": "^2.1.13",
"react": "^0.14.8",
"react-copy-to-clipboard": "^4.2.3",
"react-custom-scrollbars": "^2.2.2",
"react-dom": "^0.14.6",
"react-dropzone": "^3.5.3",
"react-onclickout": "2.0.4"
}
}

2
browser/tests.webpack.js Normal file
View File

@@ -0,0 +1,2 @@
var context = require.context('./app', true, /-test\.js$/);
context.keys().forEach(context);

File diff suppressed because one or more lines are too long

105
browser/webpack.config.js Normal file
View File

@@ -0,0 +1,105 @@
/*
* Minio Browser (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.
*/
var webpack = require('webpack')
var path = require('path')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var purify = require("purifycss-webpack-plugin")
var exports = {
context: __dirname,
entry: [
path.resolve(__dirname, 'app/index.js')
],
output: {
path: path.resolve(__dirname, 'dev'),
filename: 'index_bundle.js',
publicPath: '/minio/'
},
module: {
loaders: [{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel',
query: {
presets: ['react', 'es2015']
}
}, {
test: /\.less$/,
loader: 'style!css!less'
}, {
test: /\.json$/,
loader: 'json-loader'
},{
test: /\.css$/,
loader: 'style!css'
}, {
test: /\.(eot|woff|woff2|ttf|svg|png)/,
loader: 'url'
}]
},
node:{
fs:'empty'
},
devServer: {
historyApiFallback: {
index: '/minio/'
},
proxy: {
'/minio/webrpc': {
target: 'http://localhost:9000',
secure: false
},
'/minio/upload/*': {
target: 'http://localhost:9000',
secure: false
},
'/minio/download/*': {
target: 'http://localhost:9000',
secure: false
},
}
},
plugins: [
new CopyWebpackPlugin([
{from: 'app/css/loader.css'},
{from: 'app/img/favicon.ico'},
{from: 'app/img/browsers/chrome.png'},
{from: 'app/img/browsers/firefox.png'},
{from: 'app/img/browsers/safari.png'},
{from: 'app/img/logo.svg'},
{from: 'app/index.html'}
]),
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
new purify({
basePath: __dirname,
paths: [
"app/index.html",
"app/js/*.js"
]
})
]
}
if (process.env.NODE_ENV === 'dev') {
exports.entry = [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
path.resolve(__dirname, 'app/index.js')
]
}
module.exports = exports

View File

@@ -0,0 +1,88 @@
/*
* Isomorphic Javascript library for Minio Browser JSON-RPC API, (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.
*/
var webpack = require('webpack')
var path = require('path')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var purify = require("purifycss-webpack-plugin")
var exports = {
context: __dirname,
entry: [
path.resolve(__dirname, 'app/index.js')
],
output: {
path: path.resolve(__dirname, 'production'),
filename: 'index_bundle.js'
},
module: {
loaders: [{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel',
query: {
presets: ['react', 'es2015']
}
}, {
test: /\.less$/,
loader: 'style!css!less'
}, {
test: /\.json$/,
loader: 'json-loader'
}, {
test: /\.css$/,
loader: 'style!css'
}, {
test: /\.(eot|woff|woff2|ttf|svg|png)/,
loader: 'url'
}]
},
node:{
fs:'empty'
},
plugins: [
new CopyWebpackPlugin([
{from: 'app/css/loader.css'},
{from: 'app/img/favicon.ico'},
{from: 'app/img/browsers/chrome.png'},
{from: 'app/img/browsers/firefox.png'},
{from: 'app/img/browsers/safari.png'},
{from: 'app/img/logo.svg'},
{from: 'app/index.html'}
]),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
new purify({
basePath: __dirname,
paths: [
"app/index.html",
"app/js/*.js"
]
})
]
}
if (process.env.NODE_ENV === 'dev') {
exports.entry = [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
path.resolve(__dirname, 'app/index.js')
]
}
module.exports = exports

4791
browser/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ _init() {
fi
# List of supported architectures
SUPPORTED_OSARCH='linux/386 linux/amd64 linux/arm windows/386 windows/amd64 darwin/amd64 freebsd/amd64'
SUPPORTED_OSARCH='linux/386 linux/amd64 linux/arm linux/arm64 windows/386 windows/amd64 darwin/amd64 freebsd/amd64'
## System binaries
CP=`which cp`
@@ -47,18 +47,54 @@ go_build() {
release_shasum="$release_str/$os-$arch/$(basename $package).shasum"
# Go build to build the binary.
GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin
if [ "${arch}" == "arm" ]; then
# Release binary downloadable name
release_real_bin_6="$release_str/$os-${arch}6vl/$(basename $package)"
# Create copy
if [ $os == "windows" ]; then
$CP -p $release_bin ${release_real_bin}.exe
release_bin_6="$release_str/$os-${arch}6vl/$(basename $package).$release_tag"
## Support building for ARM6vl
GOARM=6 GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin_6
## Copy
$CP -p $release_bin_6 $release_real_bin_6
# Release shasum name
release_shasum_6="$release_str/$os-${arch}6vl/$(basename $package).shasum"
# Calculate shasum
shasum_str=$(${SHASUM} ${release_bin_6})
echo ${shasum_str} | $SED "s/$release_str\/$os-${arch}6vl\///g" > $release_shasum_6
# Release binary downloadable name
release_real_bin_7="$release_str/$os-$arch/$(basename $package)"
release_bin_7="$release_str/$os-$arch/$(basename $package).$release_tag"
## Support building for ARM7vl
GOARM=7 GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin_7
## Copy
$CP -p $release_bin_7 $release_real_bin_7
# Release shasum name
release_shasum_7="$release_str/$os-$arch/$(basename $package).shasum"
# Calculate shasum
shasum_str=$(${SHASUM} ${release_bin_7})
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum_7
else
$CP -p $release_bin $release_real_bin
fi
GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin
# Calculate shasum
shasum_str=$(${SHASUM} ${release_bin})
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum
# Create copy
if [ $os == "windows" ]; then
$CP -p $release_bin ${release_real_bin}.exe
else
$CP -p $release_bin $release_real_bin
fi
# Calculate shasum
shasum_str=$(${SHASUM} ${release_bin})
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum
fi
}
main() {
@@ -70,12 +106,14 @@ main() {
done
read -p "If you want to build for all, Just press Enter: " chosen_osarch
if [ "$chosen_osarch" = "" ]; then
if [ "$chosen_osarch" = "" ] || [ "$chosen_osarch" = "all" ]; then
for each_osarch in ${SUPPORTED_OSARCH}; do
go_build ${each_osarch}
done
else
go_build ${chosen_osarch}
for each_osarch in $(echo $chosen_osarch | sed 's/,/ /g'); do
go_build ${each_osarch}
done
fi
}

View File

@@ -144,7 +144,8 @@ assert_check_golang_env() {
}
assert_check_deps() {
installed_git_version=$(git version | awk '{print $NF}')
# support unusual Git versions such as: 2.7.4 (Apple Git-66)
installed_git_version=$(git version | perl -ne '$_ =~ m/git version (.*?)( |$)/; print "$1\n";')
if ! check_minimum_version "${GIT_VERSION}" "${installed_git_version}"; then
echo "ERROR"
echo "Git version '${installed_git_version}' not supported."

View File

@@ -1,91 +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 cmd
import (
"crypto/rand"
"encoding/base64"
)
// credential container for access and secret keys.
type credential struct {
AccessKeyID string `json:"accessKey"`
SecretAccessKey string `json:"secretKey"`
}
const (
accessKeyMinLen = 5
accessKeyMaxLen = 20
secretKeyMinLen = 8
secretKeyMaxLen = 40
)
// isValidAccessKey - validate access key for right length.
func isValidAccessKey(accessKey string) bool {
return len(accessKey) >= accessKeyMinLen && len(accessKey) <= accessKeyMaxLen
}
// isValidSecretKey - validate secret key for right length.
func isValidSecretKey(secretKey string) bool {
return len(secretKey) >= secretKeyMinLen && len(secretKey) <= secretKeyMaxLen
}
// mustGenAccessKeys - must generate access credentials.
func mustGenAccessKeys() (creds credential) {
creds, err := genAccessKeys()
fatalIf(err, "Unable to generate access keys.")
return creds
}
// genAccessKeys - generate access credentials.
func genAccessKeys() (credential, error) {
accessKeyID, err := genAccessKeyID()
if err != nil {
return credential{}, err
}
secretAccessKey, err := genSecretAccessKey()
if err != nil {
return credential{}, err
}
creds := credential{
AccessKeyID: string(accessKeyID),
SecretAccessKey: string(secretAccessKey),
}
return creds, nil
}
// genAccessKeyID - generate random alpha numeric value using only uppercase characters
// takes input as size in integer
func genAccessKeyID() ([]byte, error) {
alpha := make([]byte, accessKeyMaxLen)
if _, err := rand.Read(alpha); err != nil {
return nil, err
}
for i := 0; i < accessKeyMaxLen; i++ {
alpha[i] = alphaNumericTable[alpha[i]%byte(len(alphaNumericTable))]
}
return alpha, nil
}
// genSecretAccessKey - generate random base64 numeric value from a random seed.
func genSecretAccessKey() ([]byte, error) {
rb := make([]byte, secretKeyMaxLen)
if _, err := rand.Read(rb); err != nil {
return nil, err
}
return []byte(base64.StdEncoding.EncodeToString(rb))[:secretKeyMaxLen], nil
}

578
cmd/admin-handlers.go Normal file
View File

@@ -0,0 +1,578 @@
/*
* Minio Cloud Storage, (C) 2016, 2017 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 cmd
import (
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"time"
)
const (
minioAdminOpHeader = "X-Minio-Operation"
)
// Type-safe query params.
type mgmtQueryKey string
// Only valid query params for list/clear locks management APIs.
const (
mgmtBucket mgmtQueryKey = "bucket"
mgmtObject mgmtQueryKey = "object"
mgmtPrefix mgmtQueryKey = "prefix"
mgmtOlderThan mgmtQueryKey = "older-than"
mgmtDelimiter mgmtQueryKey = "delimiter"
mgmtMarker mgmtQueryKey = "marker"
mgmtMaxKey mgmtQueryKey = "max-key"
mgmtDryRun mgmtQueryKey = "dry-run"
)
// ServerVersion - server version
type ServerVersion struct {
Version string `json:"version"`
CommitID string `json:"commitID"`
}
// ServerStatus - contains the response of service status API
type ServerStatus struct {
StorageInfo StorageInfo `json:"storageInfo"`
ServerVersion ServerVersion `json:"serverVersion"`
}
// ServiceStatusHandler - GET /?service
// HTTP header x-minio-operation: status
// ----------
// Fetches server status information like total disk space available
// to use, online disks, offline disks and quorum threshold.
func (adminAPI adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Request) {
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Fetch storage backend information
storageInfo := newObjectLayerFn().StorageInfo()
// Fetch server version
serverVersion := ServerVersion{Version: Version, CommitID: CommitID}
// Create API response
serverStatus := ServerStatus{
StorageInfo: storageInfo,
ServerVersion: serverVersion,
}
// Marshal API response
jsonBytes, err := json.Marshal(serverStatus)
if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
errorIf(err, "Failed to marshal storage info into json.")
return
}
// Reply with storage information (across nodes in a
// distributed setup) as json.
writeSuccessResponseJSON(w, jsonBytes)
}
// ServiceRestartHandler - POST /?service
// HTTP header x-minio-operation: restart
// ----------
// Restarts minio server gracefully. In a distributed setup, restarts
// all the servers in the cluster.
func (adminAPI adminAPIHandlers) ServiceRestartHandler(w http.ResponseWriter, r *http.Request) {
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Reply to the client before restarting minio server.
writeSuccessResponseHeadersOnly(w)
sendServiceCmd(globalAdminPeers, serviceRestart)
}
// setCredsReq request
type setCredsReq struct {
Username string `xml:"username"`
Password string `xml:"password"`
}
// ServiceCredsHandler - POST /?service
// HTTP header x-minio-operation: creds
// ----------
// Update credentials in a minio server. In a distributed setup, update all the servers
// in the cluster.
func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter, r *http.Request) {
// Authenticate request
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Avoid setting new credentials when they are already passed
// by the environnement
if globalEnvAccessKey != "" || globalEnvSecretKey != "" {
writeErrorResponse(w, ErrMethodNotAllowed, r.URL)
return
}
// Load request body
inputData, err := ioutil.ReadAll(r.Body)
if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
return
}
// Unmarshal request body
var req setCredsReq
err = xml.Unmarshal(inputData, &req)
if err != nil {
errorIf(err, "Cannot unmarshal credentials request")
writeErrorResponse(w, ErrMalformedXML, r.URL)
return
}
// Check passed credentials
cred, err := getCredential(req.Username, req.Password)
switch err {
case errInvalidAccessKeyLength:
writeErrorResponse(w, ErrAdminInvalidAccessKey, r.URL)
return
case errInvalidSecretKeyLength:
writeErrorResponse(w, ErrAdminInvalidSecretKey, r.URL)
return
}
// Notify all other Minio peers to update credentials
updateErrs := updateCredsOnPeers(cred)
for peer, err := range updateErrs {
errorIf(err, "Unable to update credentials on peer %s.", peer)
}
// Update local credentials
serverConfig.SetCredential(cred)
if err = serverConfig.Save(); err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
return
}
// At this stage, the operation is successful, return 200 OK
w.WriteHeader(http.StatusOK)
}
// validateLockQueryParams - Validates query params for list/clear locks management APIs.
func validateLockQueryParams(vars url.Values) (string, string, time.Duration, APIErrorCode) {
bucket := vars.Get(string(mgmtBucket))
prefix := vars.Get(string(mgmtPrefix))
relTimeStr := vars.Get(string(mgmtOlderThan))
// N B empty bucket name is invalid
if !IsValidBucketName(bucket) {
return "", "", time.Duration(0), ErrInvalidBucketName
}
// empty prefix is valid.
if !IsValidObjectPrefix(prefix) {
return "", "", time.Duration(0), ErrInvalidObjectName
}
// If older-than parameter was empty then set it to 0s to list
// all locks older than now.
if relTimeStr == "" {
relTimeStr = "0s"
}
relTime, err := time.ParseDuration(relTimeStr)
if err != nil {
errorIf(err, "Failed to parse duration passed as query value.")
return "", "", time.Duration(0), ErrInvalidDuration
}
return bucket, prefix, relTime, ErrNone
}
// ListLocksHandler - GET /?lock&bucket=mybucket&prefix=myprefix&older-than=rel_time
// - bucket is a mandatory query parameter
// - prefix and older-than are optional query parameters
// HTTP header x-minio-operation: list
// ---------
// Lists locks held on a given bucket, prefix and relative time.
func (adminAPI adminAPIHandlers) ListLocksHandler(w http.ResponseWriter, r *http.Request) {
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
vars := r.URL.Query()
bucket, prefix, relTime, adminAPIErr := validateLockQueryParams(vars)
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Fetch lock information of locks matching bucket/prefix that
// are available since relTime.
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix, relTime)
if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
errorIf(err, "Failed to fetch lock information from remote nodes.")
return
}
// Marshal list of locks as json.
jsonBytes, err := json.Marshal(volLocks)
if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
errorIf(err, "Failed to marshal lock information into json.")
return
}
// Reply with list of locks held on bucket, matching prefix
// older than relTime supplied, as json.
writeSuccessResponseJSON(w, jsonBytes)
}
// ClearLocksHandler - POST /?lock&bucket=mybucket&prefix=myprefix&older-than=relTime
// - bucket is a mandatory query parameter
// - prefix and older-than are optional query parameters
// HTTP header x-minio-operation: clear
// ---------
// Clear locks held on a given bucket, prefix and relative time.
func (adminAPI adminAPIHandlers) ClearLocksHandler(w http.ResponseWriter, r *http.Request) {
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
vars := r.URL.Query()
bucket, prefix, relTime, adminAPIErr := validateLockQueryParams(vars)
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Fetch lock information of locks matching bucket/prefix that
// are available since relTime.
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix, relTime)
if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
errorIf(err, "Failed to fetch lock information from remote nodes.")
return
}
// Marshal list of locks as json.
jsonBytes, err := json.Marshal(volLocks)
if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
errorIf(err, "Failed to marshal lock information into json.")
return
}
// Remove lock matching bucket/prefix older than relTime.
for _, volLock := range volLocks {
globalNSMutex.ForceUnlock(volLock.Bucket, volLock.Object)
}
// Reply with list of locks cleared, as json.
writeSuccessResponseJSON(w, jsonBytes)
}
// validateHealQueryParams - Validates query params for heal list management API.
func validateHealQueryParams(vars url.Values) (string, string, string, string, int, APIErrorCode) {
bucket := vars.Get(string(mgmtBucket))
prefix := vars.Get(string(mgmtPrefix))
marker := vars.Get(string(mgmtMarker))
delimiter := vars.Get(string(mgmtDelimiter))
maxKeyStr := vars.Get(string(mgmtMaxKey))
// N B empty bucket name is invalid
if !IsValidBucketName(bucket) {
return "", "", "", "", 0, ErrInvalidBucketName
}
// empty prefix is valid.
if !IsValidObjectPrefix(prefix) {
return "", "", "", "", 0, ErrInvalidObjectName
}
// check if maxKey is a valid integer.
maxKey, err := strconv.Atoi(maxKeyStr)
if err != nil {
return "", "", "", "", 0, ErrInvalidMaxKeys
}
// Validate prefix, marker, delimiter and maxKey.
apiErr := validateListObjectsArgs(prefix, marker, delimiter, maxKey)
if apiErr != ErrNone {
return "", "", "", "", 0, apiErr
}
return bucket, prefix, marker, delimiter, maxKey, ErrNone
}
// ListObjectsHealHandler - GET /?heal&bucket=mybucket&prefix=myprefix&marker=mymarker&delimiter=&mydelimiter&maxKey=1000
// - bucket is mandatory query parameter
// - rest are optional query parameters
// List upto maxKey objects that need healing in a given bucket matching the given prefix.
func (adminAPI adminAPIHandlers) ListObjectsHealHandler(w http.ResponseWriter, r *http.Request) {
// Get object layer instance.
objLayer := newObjectLayerFn()
if objLayer == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Validate query params.
vars := r.URL.Query()
bucket, prefix, marker, delimiter, maxKey, adminAPIErr := validateHealQueryParams(vars)
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Get the list objects to be healed.
objectInfos, err := objLayer.ListObjectsHeal(bucket, prefix, marker, delimiter, maxKey)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
listResponse := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKey, objectInfos)
// Write success response.
writeSuccessResponseXML(w, encodeResponse(listResponse))
}
// ListBucketsHealHandler - GET /?heal
func (adminAPI adminAPIHandlers) ListBucketsHealHandler(w http.ResponseWriter, r *http.Request) {
// Get object layer instance.
objLayer := newObjectLayerFn()
if objLayer == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Get the list buckets to be healed.
bucketsInfo, err := objLayer.ListBucketsHeal()
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
listResponse := generateListBucketsResponse(bucketsInfo)
// Write success response.
writeSuccessResponseXML(w, encodeResponse(listResponse))
}
// HealBucketHandler - POST /?heal&bucket=mybucket&dry-run
// - x-minio-operation = bucket
// - bucket is mandatory query parameter
// Heal a given bucket, if present.
func (adminAPI adminAPIHandlers) HealBucketHandler(w http.ResponseWriter, r *http.Request) {
// Get object layer instance.
objLayer := newObjectLayerFn()
if objLayer == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Validate bucket name and check if it exists.
vars := r.URL.Query()
bucket := vars.Get(string(mgmtBucket))
if err := checkBucketExist(bucket, objLayer); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// if dry-run is present in query-params, then only perform validations and return success.
if isDryRun(vars) {
writeSuccessResponseHeadersOnly(w)
return
}
// Heal the given bucket.
err := objLayer.HealBucket(bucket)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Return 200 on success.
writeSuccessResponseHeadersOnly(w)
}
// isDryRun - returns true if dry-run query param was set and false otherwise.
// otherwise.
func isDryRun(qval url.Values) bool {
if _, dryRun := qval[string(mgmtDryRun)]; dryRun {
return true
}
return false
}
// HealObjectHandler - POST /?heal&bucket=mybucket&object=myobject&dry-run
// - x-minio-operation = object
// - bucket and object are both mandatory query parameters
// Heal a given object, if present.
func (adminAPI adminAPIHandlers) HealObjectHandler(w http.ResponseWriter, r *http.Request) {
// Get object layer instance.
objLayer := newObjectLayerFn()
if objLayer == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
vars := r.URL.Query()
bucket := vars.Get(string(mgmtBucket))
object := vars.Get(string(mgmtObject))
// Validate bucket and object names.
if err := checkBucketAndObjectNames(bucket, object); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Check if object exists.
if _, err := objLayer.GetObjectInfo(bucket, object); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// if dry-run is set in query params then perform validations
// and return success.
if isDryRun(vars) {
writeSuccessResponseHeadersOnly(w)
return
}
err := objLayer.HealObject(bucket, object)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Return 200 on success.
writeSuccessResponseHeadersOnly(w)
}
// HealFormatHandler - POST /?heal&dry-run
// - x-minio-operation = format
// - bucket and object are both mandatory query parameters
// Heal a given object, if present.
func (adminAPI adminAPIHandlers) HealFormatHandler(w http.ResponseWriter, r *http.Request) {
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Check if this setup is an erasure code backend, since
// heal-format is only applicable to single node XL and
// distributed XL setup.
if !globalIsXL {
writeErrorResponse(w, ErrNotImplemented, r.URL)
return
}
// if dry-run is set in query-params, return success as
// validations are successful so far.
vars := r.URL.Query()
if isDryRun(vars) {
writeSuccessResponseHeadersOnly(w)
return
}
// Create a new set of storage instances to heal format.json.
bootstrapDisks, err := initStorageDisks(globalEndpoints)
if err != nil {
fmt.Println(traceError(err))
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Heal format.json on available storage.
err = healFormatXL(bootstrapDisks)
if err != nil {
fmt.Println(traceError(err))
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Instantiate new object layer with newly formatted storage.
newObjectAPI, err := newXLObjects(bootstrapDisks)
if err != nil {
fmt.Println(traceError(err))
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Set object layer with newly formatted storage to globalObjectAPI.
globalObjLayerMutex.Lock()
globalObjectAPI = newObjectAPI
globalObjLayerMutex.Unlock()
// Shutdown storage belonging to old object layer instance.
objectAPI.Shutdown()
// Inform peers to reinitialize storage with newly formatted storage.
reInitPeerDisks(globalAdminPeers)
// Return 200 on success.
writeSuccessResponseHeadersOnly(w)
}

988
cmd/admin-handlers_test.go Normal file
View File

@@ -0,0 +1,988 @@
/*
* Minio Cloud Storage, (C) 2016, 2017 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 cmd
import (
"bytes"
"encoding/json"
"encoding/xml"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
router "github.com/gorilla/mux"
)
// adminXLTestBed - encapsulates subsystems that need to be setup for
// admin-handler unit tests.
type adminXLTestBed struct {
configPath string
xlDirs []string
objLayer ObjectLayer
mux *router.Router
}
// prepareAdminXLTestBed - helper function that setups a single-node
// XL backend for admin-handler tests.
func prepareAdminXLTestBed() (*adminXLTestBed, error) {
// reset global variables to start afresh.
resetTestGlobals()
// Initialize minio server config.
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
return nil, err
}
// Initializing objectLayer for HealFormatHandler.
objLayer, xlDirs, xlErr := initTestXLObjLayer()
if xlErr != nil {
return nil, xlErr
}
// Set globalEndpoints for a single node XL setup.
for _, xlDir := range xlDirs {
globalEndpoints = append(globalEndpoints, &url.URL{
Path: xlDir,
})
}
// Set globalIsXL to indicate that the setup uses an erasure code backend.
globalIsXL = true
// initialize NSLock.
isDistXL := false
initNSLock(isDistXL)
// Setup admin mgmt REST API handlers.
adminRouter := router.NewRouter()
registerAdminRouter(adminRouter)
return &adminXLTestBed{
configPath: rootPath,
xlDirs: xlDirs,
objLayer: objLayer,
mux: adminRouter,
}, nil
}
// TearDown - method that resets the test bed for subsequent unit
// tests to start afresh.
func (atb *adminXLTestBed) TearDown() {
removeAll(atb.configPath)
removeRoots(atb.xlDirs)
resetTestGlobals()
}
// initTestObjLayer - Helper function to initialize an XL-based object
// layer and set globalObjectAPI.
func initTestXLObjLayer() (ObjectLayer, []string, error) {
objLayer, xlDirs, xlErr := prepareXL()
if xlErr != nil {
return nil, nil, xlErr
}
// Make objLayer available to all internal services via globalObjectAPI.
globalObjLayerMutex.Lock()
globalObjectAPI = objLayer
globalObjLayerMutex.Unlock()
return objLayer, xlDirs, nil
}
// cmdType - Represents different service subcomands like status, stop
// and restart.
type cmdType int
const (
statusCmd cmdType = iota
restartCmd
setCreds
)
// String - String representation for cmdType
func (c cmdType) String() string {
switch c {
case statusCmd:
return "status"
case restartCmd:
return "restart"
case setCreds:
return "set-credentials"
}
return ""
}
// apiMethod - Returns the HTTP method corresponding to the admin REST
// API for a given cmdType value.
func (c cmdType) apiMethod() string {
switch c {
case statusCmd:
return "GET"
case restartCmd:
return "POST"
case setCreds:
return "POST"
}
return "GET"
}
// toServiceSignal - Helper function that translates a given cmdType
// value to its corresponding serviceSignal value.
func (c cmdType) toServiceSignal() serviceSignal {
switch c {
case statusCmd:
return serviceStatus
case restartCmd:
return serviceRestart
}
return serviceStatus
}
// testServiceSignalReceiver - Helper function that simulates a
// go-routine waiting on service signal.
func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
expectedCmd := cmd.toServiceSignal()
serviceCmd := <-globalServiceSignalCh
if serviceCmd != expectedCmd {
t.Errorf("Expected service command %v but received %v", expectedCmd, serviceCmd)
}
}
// getServiceCmdRequest - Constructs a management REST API request for service
// subcommands for a given cmdType value.
func getServiceCmdRequest(cmd cmdType, cred credential, body []byte) (*http.Request, error) {
req, err := newTestRequest(cmd.apiMethod(), "/?service", 0, nil)
if err != nil {
return nil, err
}
// Set body
req.Body = ioutil.NopCloser(bytes.NewReader(body))
// minioAdminOpHeader is to identify the request as a
// management REST API request.
req.Header.Set(minioAdminOpHeader, cmd.String())
req.Header.Set("X-Amz-Content-Sha256", getSHA256Hash(body))
// management REST API uses signature V4 for authentication.
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
return nil, err
}
return req, nil
}
// testServicesCmdHandler - parametrizes service subcommand tests on
// cmdType value.
func testServicesCmdHandler(cmd cmdType, t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls. Note: In a
// single node setup, this degenerates to a simple function
// call under the hood.
eps, err := parseStorageEndpoints([]string{"http://localhost"})
if err != nil {
t.Fatalf("Failed to parse storage end point - %v", err)
}
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
globalMinioAddr = eps[0].Host
initGlobalAdminPeers(eps)
// Setting up a go routine to simulate ServerMux's
// handleServiceSignals for stop and restart commands.
if cmd == restartCmd {
go testServiceSignalReceiver(cmd, t)
}
credentials := serverConfig.GetCredential()
var body []byte
req, err := getServiceCmdRequest(cmd, credentials, body)
if err != nil {
t.Fatalf("Failed to build service status request %v", err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if cmd == statusCmd {
expectedInfo := ServerStatus{
StorageInfo: newObjectLayerFn().StorageInfo(),
ServerVersion: ServerVersion{Version: Version, CommitID: CommitID},
}
receivedInfo := ServerStatus{}
if jsonErr := json.Unmarshal(rec.Body.Bytes(), &receivedInfo); jsonErr != nil {
t.Errorf("Failed to unmarshal StorageInfo - %v", jsonErr)
}
if expectedInfo != receivedInfo {
t.Errorf("Expected storage info and received storage info differ, %v %v", expectedInfo, receivedInfo)
}
}
if rec.Code != http.StatusOK {
resp, _ := ioutil.ReadAll(rec.Body)
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
http.StatusOK, rec.Code, string(resp))
}
}
// Test for service status management REST API.
func TestServiceStatusHandler(t *testing.T) {
testServicesCmdHandler(statusCmd, t)
}
// Test for service restart management REST API.
func TestServiceRestartHandler(t *testing.T) {
testServicesCmdHandler(restartCmd, t)
}
// Test for service set creds management REST API.
func TestServiceSetCreds(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls. Note: In a
// single node setup, this degenerates to a simple function
// call under the hood.
eps, err := parseStorageEndpoints([]string{"http://localhost"})
if err != nil {
t.Fatalf("Failed to parse storage end point - %v", err)
}
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
globalMinioAddr = eps[0].Host
initGlobalAdminPeers(eps)
credentials := serverConfig.GetCredential()
var body []byte
testCases := []struct {
Username string
Password string
EnvKeysSet bool
ExpectedStatusCode int
}{
// Bad secret key
{"minio", "minio", false, http.StatusBadRequest},
// Bad secret key set from the env
{"minio", "minio", true, http.StatusMethodNotAllowed},
// Good keys set from the env
{"minio", "minio123", true, http.StatusMethodNotAllowed},
// Successful operation should be the last one to do not change server credentials during tests.
{"minio", "minio123", false, http.StatusOK},
}
for i, testCase := range testCases {
// Set or unset environement keys
if !testCase.EnvKeysSet {
globalEnvAccessKey = ""
globalEnvSecretKey = ""
} else {
globalEnvAccessKey = testCase.Username
globalEnvSecretKey = testCase.Password
}
// Construct setCreds request body
body, _ = xml.Marshal(setCredsReq{Username: testCase.Username, Password: testCase.Password})
// Construct setCreds request
req, err := getServiceCmdRequest(setCreds, credentials, body)
if err != nil {
t.Fatalf("Failed to build service status request %v", err)
}
rec := httptest.NewRecorder()
// Execute request
adminTestBed.mux.ServeHTTP(rec, req)
// Check if the http code response is expected
if rec.Code != testCase.ExpectedStatusCode {
t.Errorf("Test %d: Wrong status code, expected = %d, found = %d", i+1, testCase.ExpectedStatusCode, rec.Code)
resp, _ := ioutil.ReadAll(rec.Body)
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
http.StatusOK, rec.Code, string(resp))
}
// If we got 200 OK, check if new credentials are really set
if rec.Code == http.StatusOK {
cred := serverConfig.GetCredential()
if cred.AccessKey != testCase.Username {
t.Errorf("Test %d: Wrong access key, expected = %s, found = %s", i+1, testCase.Username, cred.AccessKey)
}
if cred.SecretKey != testCase.Password {
t.Errorf("Test %d: Wrong secret key, expected = %s, found = %s", i+1, testCase.Password, cred.SecretKey)
}
}
}
}
// mkLockQueryVal - helper function to build lock query param.
func mkLockQueryVal(bucket, prefix, relTimeStr string) url.Values {
qVal := url.Values{}
qVal.Set("lock", "")
qVal.Set(string(mgmtBucket), bucket)
qVal.Set(string(mgmtPrefix), prefix)
qVal.Set(string(mgmtOlderThan), relTimeStr)
return qVal
}
// Test for locks list management REST API.
func TestListLocksHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
eps, err := parseStorageEndpoints([]string{"http://localhost"})
if err != nil {
t.Fatalf("Failed to parse storage end point - %v", err)
}
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
globalMinioAddr = eps[0].Host
initGlobalAdminPeers(eps)
testCases := []struct {
bucket string
prefix string
relTime string
expectedStatus int
}{
// Test 1 - valid testcase
{
bucket: "mybucket",
prefix: "myobject",
relTime: "1s",
expectedStatus: http.StatusOK,
},
// Test 2 - invalid duration
{
bucket: "mybucket",
prefix: "myprefix",
relTime: "invalidDuration",
expectedStatus: http.StatusBadRequest,
},
// Test 3 - invalid bucket name
{
bucket: `invalid\\Bucket`,
prefix: "myprefix",
relTime: "1h",
expectedStatus: http.StatusBadRequest,
},
// Test 4 - invalid prefix
{
bucket: "mybucket",
prefix: `invalid\\Prefix`,
relTime: "1h",
expectedStatus: http.StatusBadRequest,
},
}
for i, test := range testCases {
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.relTime)
req, err := newTestRequest("GET", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct list locks request - %v", i+1, err)
}
req.Header.Set(minioAdminOpHeader, "list")
cred := serverConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatalf("Test %d - Failed to sign list locks request - %v", i+1, err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if test.expectedStatus != rec.Code {
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.expectedStatus, rec.Code)
}
}
}
// Test for locks clear management REST API.
func TestClearLocksHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
eps, err := parseStorageEndpoints([]string{"http://localhost"})
if err != nil {
t.Fatalf("Failed to parse storage end point - %v", err)
}
initGlobalAdminPeers(eps)
testCases := []struct {
bucket string
prefix string
relTime string
expectedStatus int
}{
// Test 1 - valid testcase
{
bucket: "mybucket",
prefix: "myobject",
relTime: "1s",
expectedStatus: http.StatusOK,
},
// Test 2 - invalid duration
{
bucket: "mybucket",
prefix: "myprefix",
relTime: "invalidDuration",
expectedStatus: http.StatusBadRequest,
},
// Test 3 - invalid bucket name
{
bucket: `invalid\\Bucket`,
prefix: "myprefix",
relTime: "1h",
expectedStatus: http.StatusBadRequest,
},
// Test 4 - invalid prefix
{
bucket: "mybucket",
prefix: `invalid\\Prefix`,
relTime: "1h",
expectedStatus: http.StatusBadRequest,
},
}
for i, test := range testCases {
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.relTime)
req, err := newTestRequest("POST", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct clear locks request - %v", i+1, err)
}
req.Header.Set(minioAdminOpHeader, "clear")
cred := serverConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatalf("Test %d - Failed to sign clear locks request - %v", i+1, err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if test.expectedStatus != rec.Code {
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.expectedStatus, rec.Code)
}
}
}
// Test for lock query param validation helper function.
func TestValidateLockQueryParams(t *testing.T) {
// reset globals.
// this is to make sure that the tests are not affected by modified globals.
resetTestGlobals()
// initialize NSLock.
initNSLock(false)
// Sample query values for test cases.
allValidVal := mkLockQueryVal("bucket", "prefix", "1s")
invalidBucketVal := mkLockQueryVal(`invalid\\Bucket`, "prefix", "1s")
invalidPrefixVal := mkLockQueryVal("bucket", `invalid\\Prefix`, "1s")
invalidOlderThanVal := mkLockQueryVal("bucket", "prefix", "invalidDuration")
testCases := []struct {
qVals url.Values
apiErr APIErrorCode
}{
{
qVals: invalidBucketVal,
apiErr: ErrInvalidBucketName,
},
{
qVals: invalidPrefixVal,
apiErr: ErrInvalidObjectName,
},
{
qVals: invalidOlderThanVal,
apiErr: ErrInvalidDuration,
},
{
qVals: allValidVal,
apiErr: ErrNone,
},
}
for i, test := range testCases {
_, _, _, apiErr := validateLockQueryParams(test.qVals)
if apiErr != test.apiErr {
t.Errorf("Test %d - Expected error %v but received %v", i+1, test.apiErr, apiErr)
}
}
}
// mkListObjectsQueryStr - helper to build ListObjectsHeal query string.
func mkListObjectsQueryVal(bucket, prefix, marker, delimiter, maxKeyStr string) url.Values {
qVal := url.Values{}
qVal.Set("heal", "")
qVal.Set(string(mgmtBucket), bucket)
qVal.Set(string(mgmtPrefix), prefix)
qVal.Set(string(mgmtMarker), marker)
qVal.Set(string(mgmtDelimiter), delimiter)
qVal.Set(string(mgmtMaxKey), maxKeyStr)
return qVal
}
// TestValidateHealQueryParams - Test for query param validation helper function for heal APIs.
func TestValidateHealQueryParams(t *testing.T) {
testCases := []struct {
bucket string
prefix string
marker string
delimiter string
maxKeys string
apiErr APIErrorCode
}{
// 1. Valid params.
{
bucket: "mybucket",
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "10",
apiErr: ErrNone,
},
// 2. Valid params with meta bucket.
{
bucket: minioMetaBucket,
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "10",
apiErr: ErrNone,
},
// 3. Valid params with empty prefix.
{
bucket: "mybucket",
prefix: "",
marker: "",
delimiter: "/",
maxKeys: "10",
apiErr: ErrNone,
},
// 4. Invalid params with invalid bucket.
{
bucket: `invalid\\Bucket`,
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "10",
apiErr: ErrInvalidBucketName,
},
// 5. Invalid params with invalid prefix.
{
bucket: "mybucket",
prefix: `invalid\\Prefix`,
marker: "prefix11",
delimiter: "/",
maxKeys: "10",
apiErr: ErrInvalidObjectName,
},
// 6. Invalid params with invalid maxKeys.
{
bucket: "mybucket",
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "-1",
apiErr: ErrInvalidMaxKeys,
},
// 7. Invalid params with unsupported prefix marker combination.
{
bucket: "mybucket",
prefix: "prefix",
marker: "notmatchingmarker",
delimiter: "/",
maxKeys: "10",
apiErr: ErrNotImplemented,
},
// 8. Invalid params with unsupported delimiter.
{
bucket: "mybucket",
prefix: "prefix",
marker: "notmatchingmarker",
delimiter: "unsupported",
maxKeys: "10",
apiErr: ErrNotImplemented,
},
// 9. Invalid params with invalid max Keys
{
bucket: "mybucket",
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "999999999999999999999999999",
apiErr: ErrInvalidMaxKeys,
},
}
for i, test := range testCases {
vars := mkListObjectsQueryVal(test.bucket, test.prefix, test.marker, test.delimiter, test.maxKeys)
_, _, _, _, _, actualErr := validateHealQueryParams(vars)
if actualErr != test.apiErr {
t.Errorf("Test %d - Expected %v but received %v",
i+1, getAPIError(test.apiErr), getAPIError(actualErr))
}
}
}
// TestListObjectsHeal - Test for ListObjectsHealHandler.
func TestListObjectsHealHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
err = adminTestBed.objLayer.MakeBucket("mybucket")
if err != nil {
t.Fatalf("Failed to make bucket - %v", err)
}
// Delete bucket after running all test cases.
defer adminTestBed.objLayer.DeleteBucket("mybucket")
testCases := []struct {
bucket string
prefix string
marker string
delimiter string
maxKeys string
statusCode int
}{
// 1. Valid params.
{
bucket: "mybucket",
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "10",
statusCode: http.StatusOK,
},
// 2. Valid params with meta bucket.
{
bucket: minioMetaBucket,
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "10",
statusCode: http.StatusOK,
},
// 3. Valid params with empty prefix.
{
bucket: "mybucket",
prefix: "",
marker: "",
delimiter: "/",
maxKeys: "10",
statusCode: http.StatusOK,
},
// 4. Invalid params with invalid bucket.
{
bucket: `invalid\\Bucket`,
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "10",
statusCode: getAPIError(ErrInvalidBucketName).HTTPStatusCode,
},
// 5. Invalid params with invalid prefix.
{
bucket: "mybucket",
prefix: `invalid\\Prefix`,
marker: "prefix11",
delimiter: "/",
maxKeys: "10",
statusCode: getAPIError(ErrInvalidObjectName).HTTPStatusCode,
},
// 6. Invalid params with invalid maxKeys.
{
bucket: "mybucket",
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "-1",
statusCode: getAPIError(ErrInvalidMaxKeys).HTTPStatusCode,
},
// 7. Invalid params with unsupported prefix marker combination.
{
bucket: "mybucket",
prefix: "prefix",
marker: "notmatchingmarker",
delimiter: "/",
maxKeys: "10",
statusCode: getAPIError(ErrNotImplemented).HTTPStatusCode,
},
// 8. Invalid params with unsupported delimiter.
{
bucket: "mybucket",
prefix: "prefix",
marker: "notmatchingmarker",
delimiter: "unsupported",
maxKeys: "10",
statusCode: getAPIError(ErrNotImplemented).HTTPStatusCode,
},
// 9. Invalid params with invalid max Keys
{
bucket: "mybucket",
prefix: "prefix",
marker: "prefix11",
delimiter: "/",
maxKeys: "999999999999999999999999999",
statusCode: getAPIError(ErrInvalidMaxKeys).HTTPStatusCode,
},
}
for i, test := range testCases {
if i != 0 {
continue
}
queryVal := mkListObjectsQueryVal(test.bucket, test.prefix, test.marker, test.delimiter, test.maxKeys)
req, err := newTestRequest("GET", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct list objects needing heal request - %v", i+1, err)
}
req.Header.Set(minioAdminOpHeader, "list-objects")
cred := serverConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatalf("Test %d - Failed to sign list objects needing heal request - %v", i+1, err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if test.statusCode != rec.Code {
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.statusCode, rec.Code)
}
}
}
// TestHealBucketHandler - Test for HealBucketHandler.
func TestHealBucketHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
err = adminTestBed.objLayer.MakeBucket("mybucket")
if err != nil {
t.Fatalf("Failed to make bucket - %v", err)
}
// Delete bucket after running all test cases.
defer adminTestBed.objLayer.DeleteBucket("mybucket")
testCases := []struct {
bucket string
statusCode int
dryrun string
}{
// 1. Valid test case.
{
bucket: "mybucket",
statusCode: http.StatusOK,
},
// 2. Invalid bucket name.
{
bucket: `invalid\\Bucket`,
statusCode: http.StatusBadRequest,
},
// 3. Bucket not found.
{
bucket: "bucketnotfound",
statusCode: http.StatusNotFound,
},
// 4. Valid test case with dry-run.
{
bucket: "mybucket",
statusCode: http.StatusOK,
dryrun: "yes",
},
}
for i, test := range testCases {
// Prepare query params.
queryVal := url.Values{}
queryVal.Set(string(mgmtBucket), test.bucket)
queryVal.Set("heal", "")
queryVal.Set(string(mgmtDryRun), test.dryrun)
req, err := newTestRequest("POST", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct heal bucket request - %v",
i+1, err)
}
req.Header.Set(minioAdminOpHeader, "bucket")
cred := serverConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatalf("Test %d - Failed to sign heal bucket request - %v",
i+1, err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if test.statusCode != rec.Code {
t.Errorf("Test %d - Expected HTTP status code %d but received %d",
i+1, test.statusCode, rec.Code)
}
}
}
// TestHealObjectHandler - Test for HealObjectHandler.
func TestHealObjectHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Create an object myobject under bucket mybucket.
bucketName := "mybucket"
objName := "myobject"
err = adminTestBed.objLayer.MakeBucket(bucketName)
if err != nil {
t.Fatalf("Failed to make bucket %s - %v", bucketName, err)
}
_, err = adminTestBed.objLayer.PutObject(bucketName, objName,
int64(len("hello")), bytes.NewReader([]byte("hello")), nil, "")
if err != nil {
t.Fatalf("Failed to create %s - %v", objName, err)
}
// Delete bucket and object after running all test cases.
defer func(objLayer ObjectLayer, bucketName, objName string) {
objLayer.DeleteObject(bucketName, objName)
objLayer.DeleteBucket(bucketName)
}(adminTestBed.objLayer, bucketName, objName)
testCases := []struct {
bucket string
object string
dryrun string
statusCode int
}{
// 1. Valid test case.
{
bucket: bucketName,
object: objName,
statusCode: http.StatusOK,
},
// 2. Invalid bucket name.
{
bucket: `invalid\\Bucket`,
object: "myobject",
statusCode: http.StatusBadRequest,
},
// 3. Bucket not found.
{
bucket: "bucketnotfound",
object: "myobject",
statusCode: http.StatusNotFound,
},
// 4. Invalid object name.
{
bucket: bucketName,
object: `invalid\\Object`,
statusCode: http.StatusBadRequest,
},
// 5. Object not found.
{
bucket: bucketName,
object: "objectnotfound",
statusCode: http.StatusNotFound,
},
// 6. Valid test case with dry-run.
{
bucket: bucketName,
object: objName,
dryrun: "yes",
statusCode: http.StatusOK,
},
}
for i, test := range testCases {
// Prepare query params.
queryVal := url.Values{}
queryVal.Set(string(mgmtBucket), test.bucket)
queryVal.Set(string(mgmtObject), test.object)
queryVal.Set("heal", "")
queryVal.Set(string(mgmtDryRun), test.dryrun)
req, err := newTestRequest("POST", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct heal object request - %v", i+1, err)
}
req.Header.Set(minioAdminOpHeader, "object")
cred := serverConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatalf("Test %d - Failed to sign heal object request - %v", i+1, err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if test.statusCode != rec.Code {
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.statusCode, rec.Code)
}
}
}
// TestHealFormatHandler - test for HealFormatHandler.
func TestHealFormatHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Prepare query params for heal-format mgmt REST API.
queryVal := url.Values{}
queryVal.Set("heal", "")
req, err := newTestRequest("POST", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Failed to construct heal object request - %v", err)
}
// Set x-minio-operation header to format.
req.Header.Set(minioAdminOpHeader, "format")
// Sign the request using signature v4.
cred := serverConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatalf("Failed to sign heal object request - %v", err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("Expected to succeed but failed with %d", rec.Code)
}
}

62
cmd/admin-router.go Normal file
View File

@@ -0,0 +1,62 @@
/*
* 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 cmd
import router "github.com/gorilla/mux"
// adminAPIHandlers provides HTTP handlers for Minio admin API.
type adminAPIHandlers struct {
}
// registerAdminRouter - Add handler functions for each service REST API routes.
func registerAdminRouter(mux *router.Router) {
adminAPI := adminAPIHandlers{}
// Admin router
adminRouter := mux.NewRoute().PathPrefix("/").Subrouter()
/// Service operations
// Service status
adminRouter.Methods("GET").Queries("service", "").Headers(minioAdminOpHeader, "status").HandlerFunc(adminAPI.ServiceStatusHandler)
// Service restart
adminRouter.Methods("POST").Queries("service", "").Headers(minioAdminOpHeader, "restart").HandlerFunc(adminAPI.ServiceRestartHandler)
// Service update credentials
adminRouter.Methods("POST").Queries("service", "").Headers(minioAdminOpHeader, "set-credentials").HandlerFunc(adminAPI.ServiceCredentialsHandler)
/// Lock operations
// List Locks
adminRouter.Methods("GET").Queries("lock", "").Headers(minioAdminOpHeader, "list").HandlerFunc(adminAPI.ListLocksHandler)
// Clear locks
adminRouter.Methods("POST").Queries("lock", "").Headers(minioAdminOpHeader, "clear").HandlerFunc(adminAPI.ClearLocksHandler)
/// Heal operations
// List Objects needing heal.
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-objects").HandlerFunc(adminAPI.ListObjectsHealHandler)
// List Buckets needing heal.
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-buckets").HandlerFunc(adminAPI.ListBucketsHealHandler)
// Heal Buckets.
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "bucket").HandlerFunc(adminAPI.HealBucketHandler)
// Heal Objects.
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "object").HandlerFunc(adminAPI.HealObjectHandler)
// Heal Format.
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "format").HandlerFunc(adminAPI.HealFormatHandler)
}

243
cmd/admin-rpc-client.go Normal file
View File

@@ -0,0 +1,243 @@
/*
* Minio Cloud Storage, (C) 2014-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 cmd
import (
"net/url"
"path"
"sync"
"time"
)
// localAdminClient - represents admin operation to be executed locally.
type localAdminClient struct {
}
// remoteAdminClient - represents admin operation to be executed
// remotely, via RPC.
type remoteAdminClient struct {
*AuthRPCClient
}
// adminCmdRunner - abstracts local and remote execution of admin
// commands like service stop and service restart.
type adminCmdRunner interface {
Restart() error
ListLocks(bucket, prefix string, relTime time.Duration) ([]VolumeLockInfo, error)
ReInitDisks() error
}
// Restart - Sends a message over channel to the go-routine
// responsible for restarting the process.
func (lc localAdminClient) Restart() error {
globalServiceSignalCh <- serviceRestart
return nil
}
// ListLocks - Fetches lock information from local lock instrumentation.
func (lc localAdminClient) ListLocks(bucket, prefix string, relTime time.Duration) ([]VolumeLockInfo, error) {
return listLocksInfo(bucket, prefix, relTime), nil
}
// Restart - Sends restart command to remote server via RPC.
func (rc remoteAdminClient) Restart() error {
args := AuthRPCArgs{}
reply := AuthRPCReply{}
return rc.Call("Admin.Restart", &args, &reply)
}
// ListLocks - Sends list locks command to remote server via RPC.
func (rc remoteAdminClient) ListLocks(bucket, prefix string, relTime time.Duration) ([]VolumeLockInfo, error) {
listArgs := ListLocksQuery{
bucket: bucket,
prefix: prefix,
relTime: relTime,
}
var reply ListLocksReply
if err := rc.Call("Admin.ListLocks", &listArgs, &reply); err != nil {
return nil, err
}
return reply.volLocks, nil
}
// ReInitDisks - There is nothing to do here, heal format REST API
// handler has already formatted and reinitialized the local disks.
func (lc localAdminClient) ReInitDisks() error {
return nil
}
// ReInitDisks - Signals peers via RPC to reinitialize their disks and
// object layer.
func (rc remoteAdminClient) ReInitDisks() error {
args := AuthRPCArgs{}
reply := AuthRPCReply{}
return rc.Call("Admin.ReInitDisks", &args, &reply)
}
// adminPeer - represents an entity that implements Restart methods.
type adminPeer struct {
addr string
cmdRunner adminCmdRunner
}
// type alias for a collection of adminPeer.
type adminPeers []adminPeer
// makeAdminPeers - helper function to construct a collection of adminPeer.
func makeAdminPeers(eps []*url.URL) adminPeers {
var servicePeers []adminPeer
// map to store peers that are already added to ret
seenAddr := make(map[string]bool)
// add local (self) as peer in the array
servicePeers = append(servicePeers, adminPeer{
globalMinioAddr,
localAdminClient{},
})
seenAddr[globalMinioAddr] = true
serverCred := serverConfig.GetCredential()
// iterate over endpoints to find new remote peers and add
// them to ret.
for _, ep := range eps {
if ep.Host == "" {
continue
}
// Check if the remote host has been added already
if !seenAddr[ep.Host] {
cfg := authConfig{
accessKey: serverCred.AccessKey,
secretKey: serverCred.SecretKey,
serverAddr: ep.Host,
secureConn: globalIsSSL,
serviceEndpoint: path.Join(reservedBucket, adminPath),
serviceName: "Admin",
}
servicePeers = append(servicePeers, adminPeer{
addr: ep.Host,
cmdRunner: &remoteAdminClient{newAuthRPCClient(cfg)},
})
seenAddr[ep.Host] = true
}
}
return servicePeers
}
// Initialize global adminPeer collection.
func initGlobalAdminPeers(eps []*url.URL) {
globalAdminPeers = makeAdminPeers(eps)
}
// invokeServiceCmd - Invoke Restart command.
func invokeServiceCmd(cp adminPeer, cmd serviceSignal) (err error) {
switch cmd {
case serviceRestart:
err = cp.cmdRunner.Restart()
}
return err
}
// sendServiceCmd - Invoke Restart command on remote peers
// adminPeer followed by on the local peer.
func sendServiceCmd(cps adminPeers, cmd serviceSignal) {
// Send service command like stop or restart to all remote nodes and finally run on local node.
errs := make([]error, len(cps))
var wg sync.WaitGroup
remotePeers := cps[1:]
for i := range remotePeers {
wg.Add(1)
go func(idx int) {
defer wg.Done()
// we use idx+1 because remotePeers slice is 1 position shifted w.r.t cps
errs[idx+1] = invokeServiceCmd(remotePeers[idx], cmd)
}(i)
}
wg.Wait()
errs[0] = invokeServiceCmd(cps[0], cmd)
}
// listPeerLocksInfo - fetch list of locks held on the given bucket,
// matching prefix older than relTime from all peer servers.
func listPeerLocksInfo(peers adminPeers, bucket, prefix string, relTime time.Duration) ([]VolumeLockInfo, error) {
// Used to aggregate volume lock information from all nodes.
allLocks := make([][]VolumeLockInfo, len(peers))
errs := make([]error, len(peers))
var wg sync.WaitGroup
localPeer := peers[0]
remotePeers := peers[1:]
for i, remotePeer := range remotePeers {
wg.Add(1)
go func(idx int, remotePeer adminPeer) {
defer wg.Done()
// `remotePeers` is right-shifted by one position relative to `peers`
allLocks[idx], errs[idx] = remotePeer.cmdRunner.ListLocks(bucket, prefix, relTime)
}(i+1, remotePeer)
}
wg.Wait()
allLocks[0], errs[0] = localPeer.cmdRunner.ListLocks(bucket, prefix, relTime)
// Summarizing errors received for ListLocks RPC across all
// nodes. N B the possible unavailability of quorum in errors
// applies only to distributed setup.
errCount, err := reduceErrs(errs, []error{})
if err != nil {
if errCount >= (len(peers)/2 + 1) {
return nil, err
}
return nil, InsufficientReadQuorum{}
}
// Group lock information across nodes by (bucket, object)
// pair. For readability only.
paramLockMap := make(map[nsParam][]VolumeLockInfo)
for _, nodeLocks := range allLocks {
for _, lockInfo := range nodeLocks {
param := nsParam{
volume: lockInfo.Bucket,
path: lockInfo.Object,
}
paramLockMap[param] = append(paramLockMap[param], lockInfo)
}
}
groupedLockInfos := []VolumeLockInfo{}
for _, volLocks := range paramLockMap {
groupedLockInfos = append(groupedLockInfos, volLocks...)
}
return groupedLockInfos, nil
}
// reInitPeerDisks - reinitialize disks and object layer on peer servers to use the new format.
func reInitPeerDisks(peers adminPeers) error {
errs := make([]error, len(peers))
// Send ReInitDisks RPC call to all nodes.
// for local adminPeer this is a no-op.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
errs[idx] = peer.cmdRunner.ReInitDisks()
}(i, peer)
}
wg.Wait()
return nil
}

120
cmd/admin-rpc-server.go Normal file
View File

@@ -0,0 +1,120 @@
/*
* 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 cmd
import (
"errors"
"net/rpc"
"time"
router "github.com/gorilla/mux"
)
const adminPath = "/admin"
var errUnsupportedBackend = errors.New("not supported for non erasure-code backend")
// adminCmd - exports RPC methods for service status, stop and
// restart commands.
type adminCmd struct {
AuthRPCServer
}
// ListLocksQuery - wraps ListLocks API's query values to send over RPC.
type ListLocksQuery struct {
AuthRPCArgs
bucket string
prefix string
relTime time.Duration
}
// ListLocksReply - wraps ListLocks response over RPC.
type ListLocksReply struct {
AuthRPCReply
volLocks []VolumeLockInfo
}
// Restart - Restart this instance of minio server.
func (s *adminCmd) Restart(args *AuthRPCArgs, reply *AuthRPCReply) error {
if err := args.IsAuthenticated(); err != nil {
return err
}
globalServiceSignalCh <- serviceRestart
return nil
}
// ListLocks - lists locks held by requests handled by this server instance.
func (s *adminCmd) ListLocks(query *ListLocksQuery, reply *ListLocksReply) error {
if err := query.IsAuthenticated(); err != nil {
return err
}
volLocks := listLocksInfo(query.bucket, query.prefix, query.relTime)
*reply = ListLocksReply{volLocks: volLocks}
return nil
}
// ReInitDisk - reinitialize storage disks and object layer to use the
// new format.
func (s *adminCmd) ReInitDisks(args *AuthRPCArgs, reply *AuthRPCReply) error {
if err := args.IsAuthenticated(); err != nil {
return err
}
if !globalIsXL {
return errUnsupportedBackend
}
// Get the current object layer instance.
objLayer := newObjectLayerFn()
// Initialize new disks to include the newly formatted disks.
bootstrapDisks, err := initStorageDisks(globalEndpoints)
if err != nil {
return err
}
// Initialize new object layer with newly formatted disks.
newObjectAPI, err := newXLObjects(bootstrapDisks)
if err != nil {
return err
}
// Replace object layer with newly formatted storage.
globalObjLayerMutex.Lock()
globalObjectAPI = newObjectAPI
globalObjLayerMutex.Unlock()
// Shutdown storage belonging to old object layer instance.
objLayer.Shutdown()
return nil
}
// registerAdminRPCRouter - registers RPC methods for service status,
// stop and restart commands.
func registerAdminRPCRouter(mux *router.Router) error {
adminRPCHandler := &adminCmd{}
adminRPCServer := rpc.NewServer()
err := adminRPCServer.RegisterName("Admin", adminRPCHandler)
if err != nil {
return traceError(err)
}
adminRouter := mux.NewRoute().PathPrefix(reservedBucket).Subrouter()
adminRouter.Path(adminPath).Handler(adminRPCServer)
return nil
}

View File

@@ -0,0 +1,146 @@
/*
* Minio Cloud Storage, (C) 2016, 2017 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 cmd
import (
"net/url"
"testing"
"time"
)
func testAdminCmd(cmd cmdType, t *testing.T) {
// reset globals.
// this is to make sure that the tests are not affected by modified globals.
resetTestGlobals()
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatalf("Failed to create test config - %v", err)
}
defer removeAll(rootPath)
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: time.Now().UTC(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
if err != nil {
t.Fatalf("Failed to login to admin server - %v", err)
}
go func() {
// A test signal receiver
<-globalServiceSignalCh
}()
ga := AuthRPCArgs{AuthToken: reply.AuthToken, RequestTime: time.Now().UTC()}
genReply := AuthRPCReply{}
switch cmd {
case restartCmd:
if err = adminServer.Restart(&ga, &genReply); err != nil {
t.Errorf("restartCmd: Expected: <nil>, got: %v", err)
}
}
}
// TestAdminRestart - test for Admin.Restart RPC service.
func TestAdminRestart(t *testing.T) {
testAdminCmd(restartCmd, t)
}
// TestReInitDisks - test for Admin.ReInitDisks RPC service.
func TestReInitDisks(t *testing.T) {
// Reset global variables to start afresh.
resetTestGlobals()
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("Unable to initialize server config. %s", err)
}
defer removeAll(rootPath)
// Initializing objectLayer for HealFormatHandler.
_, xlDirs, xlErr := initTestXLObjLayer()
if xlErr != nil {
t.Fatalf("failed to initialize XL based object layer - %v.", xlErr)
}
defer removeRoots(xlDirs)
// Set globalEndpoints for a single node XL setup.
for _, xlDir := range xlDirs {
globalEndpoints = append(globalEndpoints, &url.URL{Path: xlDir})
}
// Setup admin rpc server for an XL backend.
globalIsXL = true
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: time.Now().UTC(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
if err != nil {
t.Fatalf("Failed to login to admin server - %v", err)
}
authArgs := AuthRPCArgs{
AuthToken: reply.AuthToken,
RequestTime: time.Now().UTC(),
}
authReply := AuthRPCReply{}
err = adminServer.ReInitDisks(&authArgs, &authReply)
if err != nil {
t.Errorf("Expected to pass, but failed with %v", err)
}
// Negative test case with admin rpc server setup for FS.
globalIsXL = false
fsAdminServer := adminCmd{}
fsArgs := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: time.Now().UTC(),
}
fsReply := LoginRPCReply{}
err = fsAdminServer.Login(&fsArgs, &fsReply)
if err != nil {
t.Fatalf("Failed to login to fs admin server - %v", err)
}
authArgs = AuthRPCArgs{
AuthToken: fsReply.AuthToken,
RequestTime: time.Now().UTC(),
}
authReply = AuthRPCReply{}
// Attempt ReInitDisks service on a FS backend.
err = fsAdminServer.ReInitDisks(&authArgs, &authReply)
if err != errUnsupportedBackend {
t.Errorf("Expected to fail with %v, but received %v",
errUnsupportedBackend, err)
}
}

View File

@@ -20,6 +20,11 @@ import (
"encoding/xml"
)
const (
// Response request id.
responseRequestIDKey = "x-amz-request-id"
)
// ObjectIdentifier carries key name for the object to delete.
type ObjectIdentifier struct {
ObjectName string `xml:"Key"`

View File

@@ -62,6 +62,7 @@ const (
ErrInvalidPartNumberMarker
ErrInvalidRequestBody
ErrInvalidCopySource
ErrInvalidMetadataDirective
ErrInvalidCopyDest
ErrInvalidPolicyDocument
ErrInvalidObjectState
@@ -109,6 +110,7 @@ const (
ErrInvalidQuerySignatureAlgo
ErrInvalidQueryParams
ErrBucketAlreadyOwnedByYou
ErrInvalidDuration
// Add new error codes here.
// Bucket notification related errors.
@@ -138,6 +140,9 @@ const (
// Add new extended error codes here.
// Please open a https://github.com/minio/minio/issues before adding
// new error codes here.
ErrAdminInvalidAccessKey
ErrAdminInvalidSecretKey
)
// error code to APIError structure, these fields carry respective
@@ -145,7 +150,7 @@ const (
var errorCodeResponse = map[APIErrorCode]APIError{
ErrInvalidCopyDest: {
Code: "InvalidRequest",
Description: "This copy request is illegal because it is trying to copy an object to itself.",
Description: "This copy request is illegal because it is trying to copy an object to itself without changing the object's metadata, storage class, website redirect location or encryption attributes.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCopySource: {
@@ -153,6 +158,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "Copy Source must mention the source bucket and key: sourcebucket/sourcekey.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidMetadataDirective: {
Code: "InvalidArgument",
Description: "Unknown metadata directive.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidRequestBody: {
Code: "InvalidArgument",
Description: "Body shouldn't be set for this request.",
@@ -471,6 +481,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "Your previous request to create the named bucket succeeded and you already own it.",
HTTPStatusCode: http.StatusConflict,
},
ErrInvalidDuration: {
Code: "InvalidDuration",
Description: "Relative duration provided in the request is invalid.",
HTTPStatusCode: http.StatusBadRequest,
},
/// Bucket notification related errors.
ErrEventNotification: {
@@ -562,6 +577,17 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "Server not initialized, please try again.",
HTTPStatusCode: http.StatusServiceUnavailable,
},
ErrAdminInvalidAccessKey: {
Code: "XMinioAdminInvalidAccessKey",
Description: "The access key is invalid.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrAdminInvalidSecretKey: {
Code: "XMinioAdminInvalidSecretKey",
Description: "The secret key is invalid.",
HTTPStatusCode: http.StatusBadRequest,
},
// Add your error structure here.
}
@@ -649,15 +675,11 @@ func getAPIError(code APIErrorCode) APIError {
// getErrorResponse gets in standard error and resource value and
// provides a encodable populated response values
func getAPIErrorResponse(err APIError, resource string) APIErrorResponse {
var data = APIErrorResponse{}
data.Code = err.Code
data.Message = err.Description
if resource != "" {
data.Resource = resource
return APIErrorResponse{
Code: err.Code,
Message: err.Description,
Resource: resource,
RequestID: "3L137",
HostID: "3L137",
}
// TODO implement this in future
data.RequestID = "3L137"
data.HostID = "3L137"
return data
}

View File

@@ -18,31 +18,24 @@ package cmd
import (
"bytes"
"crypto/rand"
"encoding/xml"
"fmt"
"net/http"
"runtime"
"strconv"
"time"
)
// Static alphanumeric table used for generating unique request ids
var alphaNumericTable = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
// newRequestID generates and returns request ID string.
func newRequestID() string {
alpha := make([]byte, 16)
rand.Read(alpha)
for i := 0; i < 16; i++ {
alpha[i] = alphaNumericTable[alpha[i]%byte(len(alphaNumericTable))]
}
return string(alpha)
// Returns a hexadecimal representation of time at the
// time response is sent to the client.
func mustGetRequestID(t time.Time) string {
return fmt.Sprintf("%X", t.UnixNano())
}
// Write http common headers
func setCommonHeaders(w http.ResponseWriter) {
// Set unique request ID for each reply.
w.Header().Set("X-Amz-Request-Id", newRequestID())
w.Header().Set("Server", ("Minio/" + ReleaseTag + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"))
w.Header().Set(responseRequestIDKey, mustGetRequestID(time.Now().UTC()))
w.Header().Set("Server", globalServerUserAgent)
w.Header().Set("Accept-Ranges", "bytes")
}

View File

@@ -18,11 +18,12 @@ package cmd
import (
"testing"
"time"
)
func TestNewRequestID(t *testing.T) {
// Ensure that it returns an alphanumeric result of length 16.
var id = newRequestID()
var id = mustGetRequestID(time.Now().UTC())
if len(id) != 16 {
t.Fail()

View File

@@ -1,5 +1,5 @@
/*
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
* Minio Cloud Storage, (C) 2015, 2016, 2017 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,6 +19,7 @@ package cmd
import (
"encoding/xml"
"net/http"
"net/url"
"path"
"time"
)
@@ -180,8 +181,9 @@ type CommonPrefix struct {
// Bucket container for bucket metadata
type Bucket struct {
Name string
CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
Name string
CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
HealBucketInfo *HealBucketInfo `xml:"HealBucketInfo,omitempty"`
}
// Object container for object metadata
@@ -195,7 +197,8 @@ type Object struct {
Owner Owner
// The class of storage used to store the object.
StorageClass string
StorageClass string
HealObjectInfo *HealObjectInfo `xml:"HealObjectInfo,omitempty"`
}
// CopyObjectResponse container returns ETag and LastModified of the successfully copied object
@@ -251,6 +254,14 @@ type DeleteObjectsResponse struct {
Errors []DeleteError `xml:"Error,omitempty"`
}
// PostResponse container for POST object request when success_action_status is set to 201
type PostResponse struct {
Bucket string
Key string
ETag string
Location string
}
// getLocation get URL location.
func getLocation(r *http.Request) string {
return path.Clean(r.URL.Path) // Clean any trailing slashes.
@@ -268,13 +279,14 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
var data = ListBucketsResponse{}
var owner = Owner{}
owner.ID = "minio"
owner.DisplayName = "minio"
owner.ID = globalMinioDefaultOwnerID
owner.DisplayName = globalMinioDefaultOwnerID
for _, bucket := range buckets {
var listbucket = Bucket{}
listbucket.Name = bucket.Name
listbucket.CreationDate = bucket.Created.Format(timeFormatAMZLong)
listbucket.HealBucketInfo = bucket.HealBucketInfo
listbuckets = append(listbuckets, listbucket)
}
@@ -291,8 +303,8 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
var owner = Owner{}
var data = ListObjectsResponse{}
owner.ID = "minio"
owner.DisplayName = "minio"
owner.ID = globalMinioDefaultOwnerID
owner.DisplayName = globalMinioDefaultOwnerID
for _, object := range resp.Objects {
var content = Object{}
@@ -305,8 +317,10 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
content.ETag = "\"" + object.MD5Sum + "\""
}
content.Size = object.Size
content.StorageClass = "STANDARD"
content.StorageClass = globalMinioDefaultStorageClass
content.Owner = owner
// object.HealObjectInfo is non-empty only when resp is constructed in ListObjectsHeal.
content.HealObjectInfo = object.HealObjectInfo
contents = append(contents, content)
}
// TODO - support EncodingType in xml decoding
@@ -337,8 +351,8 @@ func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter
var data = ListObjectsV2Response{}
if fetchOwner {
owner.ID = "minio"
owner.DisplayName = "minio"
owner.ID = globalMinioDefaultOwnerID
owner.DisplayName = globalMinioDefaultOwnerID
}
for _, object := range resp.Objects {
@@ -352,7 +366,7 @@ func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter
content.ETag = "\"" + object.MD5Sum + "\""
}
content.Size = object.Size
content.StorageClass = "STANDARD"
content.StorageClass = globalMinioDefaultStorageClass
content.Owner = owner
contents = append(contents, content)
}
@@ -411,11 +425,11 @@ func generateListPartsResponse(partsInfo ListPartsInfo) ListPartsResponse {
listPartsResponse.Bucket = partsInfo.Bucket
listPartsResponse.Key = partsInfo.Object
listPartsResponse.UploadID = partsInfo.UploadID
listPartsResponse.StorageClass = "STANDARD"
listPartsResponse.Initiator.ID = "minio"
listPartsResponse.Initiator.DisplayName = "minio"
listPartsResponse.Owner.ID = "minio"
listPartsResponse.Owner.DisplayName = "minio"
listPartsResponse.StorageClass = globalMinioDefaultStorageClass
listPartsResponse.Initiator.ID = globalMinioDefaultOwnerID
listPartsResponse.Initiator.DisplayName = globalMinioDefaultOwnerID
listPartsResponse.Owner.ID = globalMinioDefaultOwnerID
listPartsResponse.Owner.DisplayName = globalMinioDefaultOwnerID
listPartsResponse.MaxParts = partsInfo.MaxParts
listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker
@@ -474,42 +488,67 @@ func generateMultiDeleteResponse(quiet bool, deletedObjects []ObjectIdentifier,
return deleteResp
}
// writeSuccessResponse write success headers and response if any.
func writeSuccessResponse(w http.ResponseWriter, response []byte) {
func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
setCommonHeaders(w)
if response == nil {
w.WriteHeader(http.StatusOK)
return
if mType != mimeNone {
w.Header().Set("Content-Type", string(mType))
}
w.Write(response)
w.(http.Flusher).Flush()
}
// writeSuccessNoContent write success headers with http status 204
func writeSuccessNoContent(w http.ResponseWriter) {
setCommonHeaders(w)
w.WriteHeader(http.StatusNoContent)
}
// writeErrorRespone write error headers
func writeErrorResponse(w http.ResponseWriter, req *http.Request, errorCode APIErrorCode, resource string) {
apiError := getAPIError(errorCode)
// set common headers
setCommonHeaders(w)
// write Header
w.WriteHeader(apiError.HTTPStatusCode)
writeErrorResponseNoHeader(w, req, errorCode, resource)
}
func writeErrorResponseNoHeader(w http.ResponseWriter, req *http.Request, errorCode APIErrorCode, resource string) {
apiError := getAPIError(errorCode)
// Generate error response.
errorResponse := getAPIErrorResponse(apiError, resource)
encodedErrorResponse := encodeResponse(errorResponse)
// HEAD should have no body, do not attempt to write to it
if req.Method != "HEAD" {
// write error body
w.Write(encodedErrorResponse)
w.WriteHeader(statusCode)
if response != nil {
w.Write(response)
w.(http.Flusher).Flush()
}
}
// mimeType represents various MIME type used API responses.
type mimeType string
const (
// Means no response type.
mimeNone mimeType = ""
// Means response type is JSON.
mimeJSON mimeType = "application/json"
// Means response type is XML.
mimeXML mimeType = "application/xml"
)
// writeSuccessResponseJSON writes success headers and response if any,
// with content-type set to `application/json`.
func writeSuccessResponseJSON(w http.ResponseWriter, response []byte) {
writeResponse(w, http.StatusOK, response, mimeJSON)
}
// writeSuccessResponseXML writes success headers and response if any,
// with content-type set to `application/xml`.
func writeSuccessResponseXML(w http.ResponseWriter, response []byte) {
writeResponse(w, http.StatusOK, response, mimeXML)
}
// writeSuccessNoContent writes success headers with http status 204
func writeSuccessNoContent(w http.ResponseWriter) {
writeResponse(w, http.StatusNoContent, nil, mimeNone)
}
// writeRedirectSeeOther writes Location header with http status 303
func writeRedirectSeeOther(w http.ResponseWriter, location string) {
w.Header().Set("Location", location)
writeResponse(w, http.StatusSeeOther, nil, mimeNone)
}
func writeSuccessResponseHeadersOnly(w http.ResponseWriter) {
writeResponse(w, http.StatusOK, nil, mimeNone)
}
// writeErrorRespone writes error headers
func writeErrorResponse(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) {
apiError := getAPIError(errorCode)
// Generate error response.
errorResponse := getAPIErrorResponse(apiError, reqURL.Path)
encodedErrorResponse := encodeResponse(errorResponse)
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeXML)
}
func writeErrorResponseHeadersOnly(w http.ResponseWriter, errorCode APIErrorCode) {
apiError := getAPIError(errorCode)
writeResponse(w, apiError.HTTPStatusCode, nil, mimeNone)
}

View File

@@ -58,12 +58,12 @@ func isRequestPresignedSignatureV2(r *http.Request) bool {
// Verify if request has AWS Post policy Signature Version '4'.
func isRequestPostPolicySignatureV4(r *http.Request) bool {
return strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") && r.Method == "POST"
return strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") && r.Method == httpPOST
}
// 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"
return r.Header.Get("x-amz-content-sha256") == streamingContentSHA256 && r.Method == httpPUT
}
// Authorization type.
@@ -224,12 +224,12 @@ func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
} else if aType == authTypeJWT {
// Validate Authorization header if its valid for JWT request.
if !isJWTReqAuthenticated(r) {
if !isHTTPRequestValid(r) {
w.WriteHeader(http.StatusUnauthorized)
return
}
a.handler.ServeHTTP(w, r)
return
}
writeErrorResponse(w, r, ErrSignatureVersionNotSupported, r.URL.Path)
writeErrorResponse(w, ErrSignatureVersionNotSupported, r.URL)
}

View File

@@ -1,5 +1,5 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,7 +37,7 @@ func TestGetRequestAuthType(t *testing.T) {
req: &http.Request{
URL: &url.URL{
Host: "localhost:9000",
Scheme: "http",
Scheme: httpScheme,
Path: "/",
},
Header: http.Header{
@@ -54,7 +54,7 @@ func TestGetRequestAuthType(t *testing.T) {
req: &http.Request{
URL: &url.URL{
Host: "localhost:9000",
Scheme: "http",
Scheme: httpScheme,
Path: "/",
},
Header: http.Header{
@@ -69,7 +69,7 @@ func TestGetRequestAuthType(t *testing.T) {
req: &http.Request{
URL: &url.URL{
Host: "localhost:9000",
Scheme: "http",
Scheme: httpScheme,
Path: "/",
},
Header: http.Header{
@@ -84,7 +84,7 @@ func TestGetRequestAuthType(t *testing.T) {
req: &http.Request{
URL: &url.URL{
Host: "localhost:9000",
Scheme: "http",
Scheme: httpScheme,
Path: "/",
RawQuery: "X-Amz-Credential=EXAMPLEINVALIDEXAMPL%2Fs3%2F20160314%2Fus-east-1",
},
@@ -97,7 +97,7 @@ func TestGetRequestAuthType(t *testing.T) {
req: &http.Request{
URL: &url.URL{
Host: "localhost:9000",
Scheme: "http",
Scheme: httpScheme,
Path: "/",
},
Header: http.Header{
@@ -301,7 +301,7 @@ func mustNewRequest(method string, urlStr string, contentLength int64, body io.R
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 := signRequestV4(req, cred.AccessKeyID, cred.SecretAccessKey); err != nil {
if err := signRequestV4(req, cred.AccessKey, cred.SecretKey); err != nil {
t.Fatalf("Unable to inititalized new signed http request %s", err)
}
return req
@@ -309,7 +309,7 @@ func mustNewSignedRequest(method string, urlStr string, contentLength int64, bod
// Tests is requested authenticated function, tests replies for s3 errors.
func TestIsReqAuthenticated(t *testing.T) {
path, err := newTestConfig("us-east-1")
path, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatalf("unable initialize config file, %s", err)
}

View File

@@ -17,190 +17,129 @@
package cmd
import (
"fmt"
"net/rpc"
"sync"
"time"
jwtgo "github.com/dgrijalva/jwt-go"
)
// GenericReply represents any generic RPC reply.
type GenericReply struct{}
// Attempt to retry only this many number of times before
// giving up on the remote RPC entirely.
const globalAuthRPCRetryThreshold = 1
// GenericArgs represents any generic RPC arguments.
type GenericArgs struct {
Token string // Used to authenticate every RPC call.
// Used to verify if the RPC call was issued between
// the same Login() and disconnect event pair.
Timestamp time.Time
// Indicates if args should be sent to remote peers as well.
Remote bool
}
// SetToken - sets the token to the supplied value.
func (ga *GenericArgs) SetToken(token string) {
ga.Token = token
}
// SetTimestamp - sets the timestamp to the supplied value.
func (ga *GenericArgs) SetTimestamp(tstamp time.Time) {
ga.Timestamp = tstamp
}
// RPCLoginArgs - login username and password for RPC.
type RPCLoginArgs struct {
Username string
Password string
}
// RPCLoginReply - login reply provides generated token to be used
// with subsequent requests.
type RPCLoginReply struct {
Token string
Timestamp time.Time
ServerVersion string
}
// Validates if incoming token is valid.
func isRPCTokenValid(tokenStr string) bool {
jwt, err := newJWT(defaultInterNodeJWTExpiry, serverConfig.GetCredential())
if err != nil {
errorIf(err, "Unable to initialize JWT")
return false
}
token, err := jwtgo.Parse(tokenStr, func(token *jwtgo.Token) (interface{}, error) {
if _, ok := token.Method.(*jwtgo.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte(jwt.SecretAccessKey), nil
})
if err != nil {
errorIf(err, "Unable to parse JWT token string")
return false
}
// Return if token is valid.
return token.Valid
}
// Auth config represents authentication credentials and Login method name to be used
// for fetching JWT tokens from the RPC server.
// authConfig requires to make new AuthRPCClient.
type authConfig struct {
accessKey string // Username for the server.
secretKey string // Password for the server.
secureConn bool // Ask for a secured connection
address string // Network address path of RPC server.
path string // Network path for HTTP dial.
loginMethod string // RPC service name for authenticating using JWT
accessKey string // Access key (like username) for authentication.
secretKey string // Secret key (like Password) for authentication.
serverAddr string // RPC server address.
serviceEndpoint string // Endpoint on the server to make any RPC call.
secureConn bool // Make TLS connection to RPC server or not.
serviceName string // Service name of auth server.
disableReconnect bool // Disable reconnect on failure or not.
}
// AuthRPCClient is a wrapper type for RPCClient which provides JWT based authentication across reconnects.
// AuthRPCClient is a authenticated RPC client which does authentication before doing Call().
type AuthRPCClient struct {
mu sync.Mutex
config *authConfig
rpc *RPCClient // reconnect'able rpc client built on top of net/rpc Client
isLoggedIn bool // Indicates if the auth client has been logged in and token is valid.
serverToken string // Disk rpc JWT based token.
serverVersion string // Server version exchanged by the RPC.
sync.Mutex // Mutex to lock this object.
rpcClient *RPCClient // Reconnectable RPC client to make any RPC call.
config authConfig // Authentication configuration information.
authToken string // Authentication token.
}
// newAuthClient - returns a jwt based authenticated (go) rpc client, which does automatic reconnect.
func newAuthClient(cfg *authConfig) *AuthRPCClient {
// newAuthRPCClient - returns a JWT based authenticated (go) rpc client, which does automatic reconnect.
func newAuthRPCClient(config authConfig) *AuthRPCClient {
return &AuthRPCClient{
// Save the config.
config: cfg,
// Initialize a new reconnectable rpc client.
rpc: newClient(cfg.address, cfg.path, cfg.secureConn),
// Allocated auth client not logged in yet.
isLoggedIn: false,
rpcClient: newRPCClient(config.serverAddr, config.serviceEndpoint, config.secureConn),
config: config,
}
}
// Close - closes underlying rpc connection.
func (authClient *AuthRPCClient) Close() error {
authClient.mu.Lock()
// reset token on closing a connection
authClient.isLoggedIn = false
authClient.mu.Unlock()
return authClient.rpc.Close()
}
// Login - a jwt based authentication is performed with rpc server.
func (authClient *AuthRPCClient) Login() (err error) {
authClient.mu.Lock()
// As soon as the function returns unlock,
defer authClient.mu.Unlock()
authClient.Lock()
defer authClient.Unlock()
// Return if already logged in.
if authClient.isLoggedIn {
if authClient.authToken != "" {
return nil
}
reply := RPCLoginReply{}
if err = authClient.rpc.Call(authClient.config.loginMethod, RPCLoginArgs{
Username: authClient.config.accessKey,
Password: authClient.config.secretKey,
}, &reply); err != nil {
// Call login.
args := LoginRPCArgs{
Username: authClient.config.accessKey,
Password: authClient.config.secretKey,
Version: Version,
RequestTime: time.Now().UTC(),
}
reply := LoginRPCReply{}
serviceMethod := authClient.config.serviceName + loginMethodName
if err = authClient.rpcClient.Call(serviceMethod, &args, &reply); err != nil {
return err
}
// Validate if version do indeed match.
if reply.ServerVersion != Version {
return errServerVersionMismatch
}
// Logged in successfully.
authClient.authToken = reply.AuthToken
// Validate if server timestamp is skewed.
curTime := time.Now().UTC()
if curTime.Sub(reply.Timestamp) > globalMaxSkewTime {
return errServerTimeMismatch
}
// Set token, time stamp as received from a successful login call.
authClient.serverToken = reply.Token
authClient.serverVersion = reply.ServerVersion
authClient.isLoggedIn = true
return nil
}
// Call - If rpc connection isn't established yet since previous disconnect,
// connection is established, a jwt authenticated login is performed and then
// the call is performed.
func (authClient *AuthRPCClient) Call(serviceMethod string, args interface {
SetToken(token string)
SetTimestamp(tstamp time.Time)
// call makes a RPC call after logs into the server.
func (authClient *AuthRPCClient) call(serviceMethod string, args interface {
SetAuthToken(authToken string)
SetRequestTime(requestTime time.Time)
}, reply interface{}) (err error) {
// On successful login, attempt the call.
// On successful login, execute RPC call.
if err = authClient.Login(); err == nil {
// Set token and timestamp before the rpc call.
args.SetToken(authClient.serverToken)
args.SetTimestamp(time.Now().UTC())
args.SetAuthToken(authClient.authToken)
args.SetRequestTime(time.Now().UTC())
// Call the underlying rpc.
err = authClient.rpc.Call(serviceMethod, args, reply)
// Invalidate token, and mark it for re-login on subsequent reconnect.
if err == rpc.ErrShutdown {
authClient.mu.Lock()
authClient.isLoggedIn = false
authClient.mu.Unlock()
}
// Do RPC call.
err = authClient.rpcClient.Call(serviceMethod, args, reply)
}
return err
}
// Node returns the node (network address) of the connection
func (authClient *AuthRPCClient) Node() (node string) {
if authClient.rpc != nil {
node = authClient.rpc.node
// Call executes RPC call till success or globalAuthRPCRetryThreshold on ErrShutdown.
func (authClient *AuthRPCClient) Call(serviceMethod string, args interface {
SetAuthToken(authToken string)
SetRequestTime(requestTime time.Time)
}, reply interface{}) (err error) {
doneCh := make(chan struct{})
defer close(doneCh)
for i := range newRetryTimer(time.Second, 30*time.Second, MaxJitter, doneCh) {
if err = authClient.call(serviceMethod, args, reply); err == rpc.ErrShutdown {
// As connection at server side is closed, close the rpc client.
authClient.Close()
// Retry if reconnect is not disabled.
if !authClient.config.disableReconnect {
// Retry until threshold reaches.
if i < globalAuthRPCRetryThreshold {
continue
}
}
}
break
}
return node
return err
}
// RPCPath returns the RPC path of the connection
func (authClient *AuthRPCClient) RPCPath() (rpcPath string) {
if authClient.rpc != nil {
rpcPath = authClient.rpc.rpcPath
}
return rpcPath
// Close closes underlying RPC Client.
func (authClient *AuthRPCClient) Close() error {
authClient.Lock()
defer authClient.Unlock()
authClient.authToken = ""
return authClient.rpcClient.Close()
}
// ServerAddr returns the serverAddr (network address) of the connection.
func (authClient *AuthRPCClient) ServerAddr() string {
return authClient.config.serverAddr
}
// ServiceEndpoint returns the RPC service endpoint of the connection.
func (authClient *AuthRPCClient) ServiceEndpoint() string {
return authClient.config.serviceEndpoint
}

View File

@@ -20,32 +20,36 @@ import "testing"
// Tests authorized RPC client.
func TestAuthRPCClient(t *testing.T) {
authCfg := &authConfig{
// reset globals.
// this is to make sure that the tests are not affected by modified globals.
resetTestGlobals()
authCfg := authConfig{
accessKey: "123",
secretKey: "123",
serverAddr: "localhost:9000",
serviceEndpoint: "/rpc/disk",
secureConn: false,
serviceName: "MyPackage",
}
authRPC := newAuthRPCClient(authCfg)
if authRPC.ServerAddr() != authCfg.serverAddr {
t.Fatalf("Unexpected node value %s, but expected %s", authRPC.ServerAddr(), authCfg.serverAddr)
}
if authRPC.ServiceEndpoint() != authCfg.serviceEndpoint {
t.Fatalf("Unexpected node value %s, but expected %s", authRPC.ServiceEndpoint(), authCfg.serviceEndpoint)
}
authCfg = authConfig{
accessKey: "123",
secretKey: "123",
secureConn: false,
address: "localhost:9000",
path: "/rpc/disk",
loginMethod: "MyPackage.LoginHandler",
serviceName: "MyPackage",
}
authRPC := newAuthClient(authCfg)
if authRPC.Node() != authCfg.address {
t.Fatalf("Unexpected node value %s, but expected %s", authRPC.Node(), authCfg.address)
authRPC = newAuthRPCClient(authCfg)
if authRPC.ServerAddr() != authCfg.serverAddr {
t.Fatalf("Unexpected node value %s, but expected %s", authRPC.ServerAddr(), authCfg.serverAddr)
}
if authRPC.RPCPath() != authCfg.path {
t.Fatalf("Unexpected node value %s, but expected %s", authRPC.RPCPath(), authCfg.path)
}
authCfg = &authConfig{
accessKey: "123",
secretKey: "123",
secureConn: false,
loginMethod: "MyPackage.LoginHandler",
}
authRPC = newAuthClient(authCfg)
if authRPC.Node() != authCfg.address {
t.Fatalf("Unexpected node value %s, but expected %s", authRPC.Node(), authCfg.address)
}
if authRPC.RPCPath() != authCfg.path {
t.Fatalf("Unexpected node value %s, but expected %s", authRPC.RPCPath(), authCfg.path)
if authRPC.ServiceEndpoint() != authCfg.serviceEndpoint {
t.Fatalf("Unexpected node value %s, but expected %s", authRPC.ServiceEndpoint(), authCfg.serviceEndpoint)
}
}

View File

@@ -16,26 +16,28 @@
package cmd
import "time"
// Base login method name. It should be used along with service name.
const loginMethodName = ".Login"
type loginServer struct {
// AuthRPCServer RPC server authenticates using JWT.
type AuthRPCServer struct {
}
// LoginHandler - Handles JWT based RPC logic.
func (b loginServer) LoginHandler(args *RPCLoginArgs, reply *RPCLoginReply) error {
jwt, err := newJWT(defaultInterNodeJWTExpiry, serverConfig.GetCredential())
// Login - Handles JWT based RPC login.
func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
// Validate LoginRPCArgs
if err := args.IsValid(); err != nil {
return err
}
// Authenticate using JWT.
token, err := authenticateNode(args.Username, args.Password)
if err != nil {
return err
}
if err = jwt.Authenticate(args.Username, args.Password); err != nil {
return err
}
token, err := jwt.GenerateToken(args.Username)
if err != nil {
return err
}
reply.Token = token
reply.Timestamp = time.Now().UTC()
reply.ServerVersion = Version
// Return the token.
reply.AuthToken = token
return nil
}

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