Allow bucket creation
This commit is contained in:
parent
7c0ca14525
commit
7855e07a96
8 changed files with 281 additions and 61 deletions
|
@ -1,2 +1,3 @@
|
||||||
vendor
|
vendor
|
||||||
|
s3manager
|
||||||
README.md
|
README.md
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
vendor
|
vendor
|
||||||
|
s3manager
|
||||||
glide.lock
|
glide.lock
|
||||||
|
|
76
api-handlers.go
Normal file
76
api-handlers.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/minio-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createBucketHandler creates a new bucket
|
||||||
|
func createBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var bucket minio.BucketInfo
|
||||||
|
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err = r.Body.Close(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &bucket)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = minioClient.MakeBucket(bucket.Name, "")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
err = json.NewEncoder(w).Encode(bucket)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getObjectHandler downloads an object to the client
|
||||||
|
func getObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
objectName := vars["objectName"]
|
||||||
|
|
||||||
|
object, err := minioClient.GetObject(vars["bucketName"], objectName)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", objectName))
|
||||||
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
|
||||||
|
_, err = io.Copy(w, object)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteObjectHandler deletes an object
|
||||||
|
func deleteObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
err := minioClient.RemoveObject(vars["bucketName"], vars["objectName"])
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
|
@ -6,11 +6,22 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"strings"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
minio "github.com/minio/minio-go"
|
minio "github.com/minio/minio-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ObjectWithIcon is a minio object with an added icon
|
||||||
|
type ObjectWithIcon struct {
|
||||||
|
minio.ObjectInfo
|
||||||
|
Icon string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BucketPage defines the details page of a bucket
|
||||||
|
type BucketPage struct {
|
||||||
|
BucketName string
|
||||||
|
Objects []ObjectWithIcon
|
||||||
|
}
|
||||||
|
|
||||||
// indexPageHandler forwards to "/buckets"
|
// indexPageHandler forwards to "/buckets"
|
||||||
func indexPageHandler(w http.ResponseWriter, r *http.Request) {
|
func indexPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/buckets", http.StatusPermanentRedirect)
|
http.Redirect(w, r, "/buckets", http.StatusPermanentRedirect)
|
||||||
|
@ -37,10 +48,10 @@ func bucketsPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bucketHandler handles the main page
|
// bucketHandler shows the details page of a bucket
|
||||||
func bucketPageHandler(w http.ResponseWriter, r *http.Request) {
|
func bucketPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
bucket := strings.Split(r.URL.Path, "/")[2]
|
bucketName := mux.Vars(r)["bucketName"]
|
||||||
var objects []minio.ObjectInfo
|
var objects []ObjectWithIcon
|
||||||
|
|
||||||
lp := path.Join("templates", "layout.html")
|
lp := path.Join("templates", "layout.html")
|
||||||
bp := path.Join("templates", "bucket.html")
|
bp := path.Join("templates", "bucket.html")
|
||||||
|
@ -50,20 +61,40 @@ func bucketPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a done channel to control 'ListObjectsV2' go routine.
|
|
||||||
doneCh := make(chan struct{})
|
doneCh := make(chan struct{})
|
||||||
|
|
||||||
objectCh := minioClient.ListObjectsV2(bucket, "", false, doneCh)
|
objectCh := minioClient.ListObjectsV2(bucketName, "", false, doneCh)
|
||||||
for object := range objectCh {
|
for object := range objectCh {
|
||||||
if object.Err != nil {
|
if object.Err != nil {
|
||||||
fmt.Println(object.Err)
|
fmt.Println(object.Err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
objects = append(objects, object)
|
objectWithIcon := ObjectWithIcon{object, getIcon(object.Key)}
|
||||||
|
objects = append(objects, objectWithIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.ExecuteTemplate(w, "layout", objects)
|
bucketPage := BucketPage{
|
||||||
|
BucketName: bucketName,
|
||||||
|
Objects: objects,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.ExecuteTemplate(w, "layout", bucketPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getIcon(fileName string) string {
|
||||||
|
e := path.Ext(fileName)
|
||||||
|
|
||||||
|
switch e {
|
||||||
|
case ".tgz":
|
||||||
|
return "archive"
|
||||||
|
case ".png", ".jpg", ".gif", ".svg":
|
||||||
|
return "photo"
|
||||||
|
case ".mp3":
|
||||||
|
return "music_note"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "insert_drive_file"
|
||||||
|
}
|
20
routes.go
20
routes.go
|
@ -29,7 +29,25 @@ var routes = Routes{
|
||||||
Route{
|
Route{
|
||||||
"Load Bucket Page",
|
"Load Bucket Page",
|
||||||
"GET",
|
"GET",
|
||||||
"/buckets/{bucketID}",
|
"/buckets/{bucketName}",
|
||||||
bucketPageHandler,
|
bucketPageHandler,
|
||||||
},
|
},
|
||||||
|
Route{
|
||||||
|
"Create Bucket",
|
||||||
|
"POST",
|
||||||
|
"/api/buckets",
|
||||||
|
createBucketHandler,
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
"Download Object",
|
||||||
|
"GET",
|
||||||
|
"/api/buckets/{bucketName}/objects/{objectName}",
|
||||||
|
getObjectHandler,
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
"Delete Object",
|
||||||
|
"DELETE",
|
||||||
|
"/api/buckets/{bucketName}/objects/{objectName}",
|
||||||
|
deleteObjectHandler,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,94 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<table>
|
<nav class="purple" role="navigation">
|
||||||
|
<div class="nav-wrapper container">
|
||||||
|
<span href="#" class="brand-logo"><i class="material-icons">folder_open</i>{{ .BucketName }}</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<thead>
|
<div class="container">
|
||||||
<tr>
|
<div class="section">
|
||||||
<th>Key</th>
|
|
||||||
<th>Size</th>
|
|
||||||
<th>Owner</th>
|
|
||||||
<th>Last Modified</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
<table class="highlight bordered">
|
||||||
{{ range $object := . }}
|
|
||||||
<tr>
|
<thead>
|
||||||
<td>{{ $object.Key }}</td>
|
<tr>
|
||||||
<td>{{ $object.Size }} bytes</td>
|
<th></th>
|
||||||
<td>{{ $object.Owner }}</td>
|
<th>Key</th>
|
||||||
<td>{{ $object.LastModified }}</td>
|
<th>Size</th>
|
||||||
</tr>
|
<th>Owner</th>
|
||||||
|
<th>Last Modified</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{{ range $object := .Objects }}
|
||||||
|
<tr>
|
||||||
|
<td><i class="material-icons">{{ $object.Icon }}</i></td>
|
||||||
|
<td>{{ $object.Key }}</td>
|
||||||
|
<td>{{ $object.Size }} bytes</td>
|
||||||
|
<td>{{ $object.Owner }}</td>
|
||||||
|
<td>{{ $object.LastModified }}</td>
|
||||||
|
<td>
|
||||||
|
<!-- Dropdown Trigger -->
|
||||||
|
<a class='dropdown-button' href='#' data-activates='actions-dropdown-{{ $object.Key }}'>
|
||||||
|
Actions <i class="material-icons right">arrow_drop_down</i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Dropdown Structure -->
|
||||||
|
<ul id='actions-dropdown-{{ $object.Key }}' class='dropdown-content'>
|
||||||
|
<li><a href="/api/buckets/{{ $.BucketName }}/objects/{{ $object.Key }}">Download</a></li>
|
||||||
|
<li><a href="#" onclick="deleteObject({{ $object.Key }})">Delete</a></li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{ if not .Objects }}
|
||||||
|
<p style="text-align: center;margin-top: 2em;">Oh noes... No objects in <strong>{{ .BucketName }}</strong> yet...</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</tbody>
|
<p></p>
|
||||||
|
|
||||||
</table>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fixed-action-btn">
|
||||||
|
<a class="btn-floating btn-large red" href="#modal-create-object">
|
||||||
|
<i class="large material-icons">add</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-create-object" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Create Object</h4>
|
||||||
|
<br>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col s6">
|
||||||
|
<form action="/api/buckets" method="POST" id="create-bucket-form">
|
||||||
|
<div class="input-field">
|
||||||
|
<input placeholder="My Bucket" id="name" type="text" name="name">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" form="create-bucket-form" class="modal-action modal-close waves-effect waves-green btn-flat">Create</button>
|
||||||
|
<button class="modal-action modal-close waves-effect waves-green btn-flat">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function deleteObject(objectName) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'DELETE',
|
||||||
|
url: '/api/buckets/{{ .BucketName }}/objects/' + objectName,
|
||||||
|
success: function () { location.reload(); }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,27 +1,37 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="row">
|
<nav class="purple" role="navigation">
|
||||||
<div class="col m12 l6">
|
<div class="nav-wrapper container">
|
||||||
|
<a href="/" class="brand-logo">S3 Manager</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
{{ range $bucket := . }}
|
<div class="container">
|
||||||
<a href="/buckets/{{ $bucket.Name }}" style="color: black;">
|
<div class="section">
|
||||||
<div class="card">
|
<div class="row">
|
||||||
<div class="card-content">
|
|
||||||
<div class="row" style="margin-bottom: 0;">
|
{{ range $bucket := . }}
|
||||||
<div class="col">
|
<div class="col m12 l6">
|
||||||
<i class="material-icons large">cloud_circle</i>
|
<a href="/buckets/{{ $bucket.Name }}" style="color: black;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="row" style="margin-bottom: 0;">
|
||||||
|
<div class="col">
|
||||||
|
<i class="material-icons large">folder_open</i>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<span class="card-title">
|
||||||
|
{{ $bucket.Name }}
|
||||||
|
</span>
|
||||||
|
<p style="color: gray;">Created on {{ $bucket.CreationDate }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
|
||||||
<span class="card-title">
|
|
||||||
{{ $bucket.Name }}
|
|
||||||
</span>
|
|
||||||
<p style="color: gray;">Created on {{ $bucket.CreationDate }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
{{ end }}
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -37,9 +47,9 @@
|
||||||
<br>
|
<br>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s6">
|
<div class="col s6">
|
||||||
<form action="/api/buckets" method="POST">
|
<form id="create-bucket-form" onsubmit="createBucket()">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<input placeholder="My Bucket" id="name" type="text">
|
<input placeholder="My Bucket" id="name" type="text" name="name">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -47,8 +57,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<a href="#!" class=" modal-action modal-close waves-effect waves-green btn-flat">Create</a>
|
<button type="submit" form="create-bucket-form" class="modal-action modal-close waves-effect waves-green btn-flat">Create</button>
|
||||||
<a href="#!" class=" modal-action modal-close waves-effect waves-green btn-flat">Cancel</a>
|
<button class="modal-action modal-close waves-effect waves-green btn-flat">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function createBucket() {
|
||||||
|
var formData = {};
|
||||||
|
$.each($('#create-bucket-form')
|
||||||
|
.serializeArray(), function(i, field) {
|
||||||
|
formData[field.name] = field.value;
|
||||||
|
});
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/api/buckets',
|
||||||
|
data: JSON.stringify(formData),
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function() { location.reload(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -14,20 +14,16 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<nav class="purple" role="navigation">
|
|
||||||
<div class="nav-wrapper container">
|
|
||||||
<a href="/" class="brand-logo">S3 Manager</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container">
|
{{ template "content" . }}
|
||||||
<div class="section">
|
|
||||||
{{ template "content" . }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/js/materialize.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/js/materialize.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
$('.modal').modal();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue