Generate link feature
This commit is contained in:
parent
d6a2b056c2
commit
4858c7493b
5 changed files with 241 additions and 20 deletions
43
internal/app/s3manager/generate_presigned_url.go
Normal file
43
internal/app/s3manager/generate_presigned_url.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package s3manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func HandleGenerateUrl(s3 S3) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bucketName := mux.Vars(r)["bucketName"]
|
||||
objectName := mux.Vars(r)["objectName"]
|
||||
expiry := r.URL.Query().Get("expiry")
|
||||
|
||||
parsedExpiry, err := strconv.ParseInt(expiry, 10, 0)
|
||||
if err != nil {
|
||||
handleHTTPError(w, fmt.Errorf("error when converting expiry: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if parsedExpiry > 7*24*60*60 || parsedExpiry < 1 {
|
||||
handleHTTPError(w, fmt.Errorf("invalid expiry value: %v", parsedExpiry))
|
||||
return
|
||||
}
|
||||
|
||||
expirySecond := time.Duration(parsedExpiry * 1e9)
|
||||
reqParams := make(url.Values)
|
||||
url, err := s3.PresignedGetObject(r.Context(), bucketName, objectName, expirySecond, reqParams)
|
||||
if err != nil {
|
||||
handleHTTPError(w, fmt.Errorf("error when generate url: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.Encode(map[string]string{"url": url.String()})
|
||||
}
|
||||
}
|
|
@ -8,7 +8,9 @@ import (
|
|||
"github.com/cloudlena/s3manager/internal/app/s3manager"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"io"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Ensure, that S3Mock does implement s3manager.S3.
|
||||
|
@ -33,6 +35,9 @@ var _ s3manager.S3 = &S3Mock{}
|
|||
// MakeBucketFunc: func(ctx context.Context, bucketName string, opts minio.MakeBucketOptions) error {
|
||||
// panic("mock out the MakeBucket method")
|
||||
// },
|
||||
// PresignedGetObjectFunc: func(ctx context.Context, bucketName string, objectName string, expiry time.Duration, reqParams url.Values) (*url.URL, error) {
|
||||
// panic("mock out the PresignedGetObject method")
|
||||
// },
|
||||
// PutObjectFunc: func(ctx context.Context, bucketName string, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (minio.UploadInfo, error) {
|
||||
// panic("mock out the PutObject method")
|
||||
// },
|
||||
|
@ -61,6 +66,9 @@ type S3Mock struct {
|
|||
// MakeBucketFunc mocks the MakeBucket method.
|
||||
MakeBucketFunc func(ctx context.Context, bucketName string, opts minio.MakeBucketOptions) error
|
||||
|
||||
// PresignedGetObjectFunc mocks the PresignedGetObject method.
|
||||
PresignedGetObjectFunc func(ctx context.Context, bucketName string, objectName string, expiry time.Duration, reqParams url.Values) (*url.URL, error)
|
||||
|
||||
// PutObjectFunc mocks the PutObject method.
|
||||
PutObjectFunc func(ctx context.Context, bucketName string, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (minio.UploadInfo, error)
|
||||
|
||||
|
@ -106,6 +114,19 @@ type S3Mock struct {
|
|||
// Opts is the opts argument value.
|
||||
Opts minio.MakeBucketOptions
|
||||
}
|
||||
// PresignedGetObject holds details about calls to the PresignedGetObject method.
|
||||
PresignedGetObject []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// BucketName is the bucketName argument value.
|
||||
BucketName string
|
||||
// ObjectName is the objectName argument value.
|
||||
ObjectName string
|
||||
// Expiry is the expiry argument value.
|
||||
Expiry time.Duration
|
||||
// ReqParams is the reqParams argument value.
|
||||
ReqParams url.Values
|
||||
}
|
||||
// PutObject holds details about calls to the PutObject method.
|
||||
PutObject []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
|
@ -140,13 +161,14 @@ type S3Mock struct {
|
|||
Opts minio.RemoveObjectOptions
|
||||
}
|
||||
}
|
||||
lockGetObject sync.RWMutex
|
||||
lockListBuckets sync.RWMutex
|
||||
lockListObjects sync.RWMutex
|
||||
lockMakeBucket sync.RWMutex
|
||||
lockPutObject sync.RWMutex
|
||||
lockRemoveBucket sync.RWMutex
|
||||
lockRemoveObject sync.RWMutex
|
||||
lockGetObject sync.RWMutex
|
||||
lockListBuckets sync.RWMutex
|
||||
lockListObjects sync.RWMutex
|
||||
lockMakeBucket sync.RWMutex
|
||||
lockPresignedGetObject sync.RWMutex
|
||||
lockPutObject sync.RWMutex
|
||||
lockRemoveBucket sync.RWMutex
|
||||
lockRemoveObject sync.RWMutex
|
||||
}
|
||||
|
||||
// GetObject calls GetObjectFunc.
|
||||
|
@ -305,6 +327,54 @@ func (mock *S3Mock) MakeBucketCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// PresignedGetObject calls PresignedGetObjectFunc.
|
||||
func (mock *S3Mock) PresignedGetObject(ctx context.Context, bucketName string, objectName string, expiry time.Duration, reqParams url.Values) (*url.URL, error) {
|
||||
if mock.PresignedGetObjectFunc == nil {
|
||||
panic("S3Mock.PresignedGetObjectFunc: method is nil but S3.PresignedGetObject was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
BucketName string
|
||||
ObjectName string
|
||||
Expiry time.Duration
|
||||
ReqParams url.Values
|
||||
}{
|
||||
Ctx: ctx,
|
||||
BucketName: bucketName,
|
||||
ObjectName: objectName,
|
||||
Expiry: expiry,
|
||||
ReqParams: reqParams,
|
||||
}
|
||||
mock.lockPresignedGetObject.Lock()
|
||||
mock.calls.PresignedGetObject = append(mock.calls.PresignedGetObject, callInfo)
|
||||
mock.lockPresignedGetObject.Unlock()
|
||||
return mock.PresignedGetObjectFunc(ctx, bucketName, objectName, expiry, reqParams)
|
||||
}
|
||||
|
||||
// PresignedGetObjectCalls gets all the calls that were made to PresignedGetObject.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedS3.PresignedGetObjectCalls())
|
||||
func (mock *S3Mock) PresignedGetObjectCalls() []struct {
|
||||
Ctx context.Context
|
||||
BucketName string
|
||||
ObjectName string
|
||||
Expiry time.Duration
|
||||
ReqParams url.Values
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
BucketName string
|
||||
ObjectName string
|
||||
Expiry time.Duration
|
||||
ReqParams url.Values
|
||||
}
|
||||
mock.lockPresignedGetObject.RLock()
|
||||
calls = mock.calls.PresignedGetObject
|
||||
mock.lockPresignedGetObject.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// PutObject calls PutObjectFunc.
|
||||
func (mock *S3Mock) PutObject(ctx context.Context, bucketName string, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (minio.UploadInfo, error) {
|
||||
if mock.PutObjectFunc == nil {
|
||||
|
|
|
@ -3,6 +3,8 @@ package s3manager
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
)
|
||||
|
@ -15,6 +17,7 @@ type S3 interface {
|
|||
ListBuckets(ctx context.Context) ([]minio.BucketInfo, error)
|
||||
ListObjects(ctx context.Context, bucketName string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
|
||||
MakeBucket(ctx context.Context, bucketName string, opts minio.MakeBucketOptions) error
|
||||
PresignedGetObject(ctx context.Context, bucketName, objectName string, expiry time.Duration, reqParams url.Values) (*url.URL, error)
|
||||
PutObject(ctx context.Context, bucketName, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (minio.UploadInfo, error)
|
||||
RemoveBucket(ctx context.Context, bucketName string) error
|
||||
RemoveObject(ctx context.Context, bucketName, objectName string, opts minio.RemoveObjectOptions) error
|
||||
|
|
1
main.go
1
main.go
|
@ -161,6 +161,7 @@ func main() {
|
|||
r.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucket(s3)).Methods(http.MethodDelete)
|
||||
}
|
||||
r.Handle("/api/buckets/{bucketName}/objects", s3manager.HandleCreateObject(s3, sseType)).Methods(http.MethodPost)
|
||||
r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}/url", s3manager.HandleGenerateUrl(s3)).Methods(http.MethodGet)
|
||||
r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleGetObject(s3, configuration.ForceDownload)).Methods(http.MethodGet)
|
||||
if configuration.AllowDelete {
|
||||
r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleDeleteObject(s3)).Methods(http.MethodDelete)
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
<!-- Dropdown Structure -->
|
||||
<ul id="actions-dropdown-{{ $index }}" class="dropdown-content">
|
||||
<li><a target="_blank" href="/api/buckets/{{ $.BucketName }}/objects/{{ $object.Key }}">Download</a></li>
|
||||
<li><a onclick="handleOpenDownloadLinkModal({{ $object.Key }})">Download link</a></li>
|
||||
{{- if $.AllowDelete }}
|
||||
<li><a href="#" onclick="deleteObject({{ $.BucketName }}, {{ $object.Key }})">Delete</a></li>
|
||||
{{- end }}
|
||||
|
@ -110,7 +111,7 @@
|
|||
<i class="large material-icons">create_new_folder</i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn-floating btn-large red modal-trigger tooltipped" data-target="modal-create-folder" data-position="top" data-tooltip="Change path">
|
||||
<button type="button" class="btn-floating btn-large red modal-trigger tooltipped" data-target="modal-change-path" data-position="top" data-tooltip="Change path">
|
||||
<i class="large material-icons">create</i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -118,9 +119,8 @@
|
|||
<input type="file" id="upload-folder-input" webkitdirectory multiple style="display: none;">
|
||||
<input type="file" id="upload-file-input" name="file" multiple style="display: none;">
|
||||
|
||||
<div id="modal-create-folder" class="modal">
|
||||
<form id="create-folder-form" enctype="multipart/form-data">
|
||||
|
||||
<div id="modal-change-path" class="modal">
|
||||
<form id="change-path-form" enctype="multipart/form-data">
|
||||
<div class="modal-content">
|
||||
<h4>Change directory path</h4>
|
||||
<br>
|
||||
|
@ -133,12 +133,58 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="modal-close waves-effect waves-green btn-flat">Cancel</button>
|
||||
<button type="submit" class="modal-close waves-effect waves-green btn">Create</button>
|
||||
<button type="submit" class="modal-close waves-effect waves-green btn">Change path</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="modal-create-download-link" class="modal">
|
||||
<form id="download-link-form">
|
||||
<div class="modal-content">
|
||||
<div class="row">
|
||||
<h4>Create download link for </h4>
|
||||
<input name="objectName" id="objectName" type="text" readonly>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s4">
|
||||
<div class="input-field">
|
||||
<input name="day" id="gen-link-day" value="0" type="text" pattern="^[\d]+$" class="validate">
|
||||
<label for="gen-link-day">Day</label>
|
||||
<span class="helper-text" data-error="Invalid day format"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col s4">
|
||||
<div class="input-field">
|
||||
<input name="hour" id="gen-link-hour" value="1" type="text" pattern="^([0-9]|1\d|2[0-3])$" class="validate">
|
||||
<label for="gen-link-hour">Hour</label>
|
||||
<span class="helper-text" data-error="Invalid hour format"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col s4">
|
||||
<div class="input-field">
|
||||
<input name="minute" id="gen-link-minute" value="0" type="text" pattern="^([0-9]|[1-5]\d)$" class="validate">
|
||||
<label for="gen-link-minute">Minute</label>
|
||||
<span class="helper-text" data-error="Invalid minute format"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s3">
|
||||
<button id="create-link-btn" class="waves-effect waves-green btn">Create link</button>
|
||||
</div>
|
||||
<div class="col s9 red-text text-darken-2" id="gen-url-error"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s11">
|
||||
<div class="input-field">
|
||||
<i class="material-icons prefix" onclick="handleCopyLink()" style="cursor:pointer;">content_copy</i>
|
||||
<input name="generated-link" id="generated-link" type="text" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
@ -150,6 +196,7 @@ function deleteObject(bucketName, objectName) {
|
|||
success: function () { location.reload(); }
|
||||
})
|
||||
}
|
||||
|
||||
function deleteBucket(bucketName) {
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
|
@ -164,19 +211,24 @@ function handleUploadFiles(event) {
|
|||
uploadFiles(files, url);
|
||||
}
|
||||
|
||||
function handleCreateFolder(event) {
|
||||
function handleChangePath(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData(form);
|
||||
const newPath = formData.get("new-path")
|
||||
|
||||
let newHref = window.location.href + newPath
|
||||
if(!newHref.endsWith("/")) {
|
||||
newHref = newHref + "/"
|
||||
let appendPath = formData.get("new-path")
|
||||
if(!appendPath.endsWith("/")) {
|
||||
appendPath = appendPath + "/";
|
||||
}
|
||||
|
||||
window.location.href = newHref
|
||||
let currentPath = window.location.href
|
||||
if(!currentPath.endsWith("/")) {
|
||||
currentPath = currentPath + "/";
|
||||
}
|
||||
|
||||
form.reset();
|
||||
window.location.href = currentPath + appendPath;
|
||||
}
|
||||
|
||||
function uploadFiles(files, url) {
|
||||
|
@ -219,8 +271,60 @@ function createNotification(fileName) {
|
|||
return notification;
|
||||
}
|
||||
|
||||
function handleOpenDownloadLinkModal(objectName) {
|
||||
const downloadLinkForm = document.forms['download-link-form']
|
||||
downloadLinkForm.elements['objectName'].value = objectName;
|
||||
|
||||
const createLinkModalElement = document.getElementById('modal-create-download-link')
|
||||
document.getElementById('generated-link').setAttribute('value', "");
|
||||
document.getElementById('gen-url-error').setHTML("");
|
||||
const modalInstance = M.Modal.init(createLinkModalElement);
|
||||
modalInstance.open()
|
||||
}
|
||||
|
||||
function handleGenerateDownloadLink(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData(form);
|
||||
const objectName = formData.get('objectName');
|
||||
const genUrlMessage = document.getElementById('gen-url-error');
|
||||
|
||||
const expiryTime = formData.get('day') * 24 * 60 * 60 + formData.get('hour') * 60 * 60 + formData.get('minute') * 60;
|
||||
|
||||
if(expiryTime > 7 * 24 * 60 * 60) {
|
||||
genUrlMessage.setHTML("Expiry time must be less than 7 days");
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '/api/buckets/' + {{ $.BucketName }}+ "/objects/" + objectName + "/url?expiry=" + expiryTime,
|
||||
success: function (result) {
|
||||
genUrlMessage.setHTML("")
|
||||
document.getElementById("generated-link").setAttribute('value', JSON.parse(result).url);
|
||||
},
|
||||
error: function(request, status, error) {
|
||||
genUrlMessage.setHTML("Error when generating url")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleCopyLink() {
|
||||
const url = document.getElementById("generated-link").value;
|
||||
|
||||
if(!!url) {
|
||||
navigator.clipboard.writeText(url).then(function() {
|
||||
M.toast({html: 'Copied to clipboard!'});
|
||||
}, function(err) {
|
||||
console.error('Could not copy:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = (event) => {
|
||||
$('#create-folder-form').submit(handleCreateFolder)
|
||||
$('#change-path-form').submit(handleChangePath)
|
||||
$('#download-link-form').submit(handleGenerateDownloadLink)
|
||||
|
||||
uploadFolderInput = $('#upload-folder-input');
|
||||
$('#upload-folder-btn').click(event => uploadFolderInput.click());
|
||||
|
|
Loading…
Reference in a new issue