Compare commits
2 Commits
e8861940cb
...
8a313c2b28
Author | SHA1 | Date |
---|---|---|
Ambrose Chua | 8a313c2b28 | |
Ambrose Chua | 7b6b92e32e |
119
README.md
119
README.md
|
@ -3,48 +3,106 @@
|
|||
|
||||
A photo bucket management suite.
|
||||
|
||||
There are two modes of operation:
|
||||
- Domain
|
||||
- Buckets are exactly equal to their domain names
|
||||
- `unset MINIO_DOMAIN`
|
||||
- Subdomain
|
||||
- Buckets are named after subdomains
|
||||
- `export MINIO_DOMAIN=your.domain`
|
||||
## Bucket Format
|
||||
|
||||
- `photo/[filename]`
|
||||
- Original photo. Name conflict resolution (excluding extension) must be handled client-side.
|
||||
- `preview/[filename]_h[height]q[quality].[format]`
|
||||
- Preview photos at lower resolutions and quality.
|
||||
- Generator: preview
|
||||
- `photometadata/[filename]/size`
|
||||
- Original photo size, JSON
|
||||
- Generator: preview
|
||||
- `photometadata/[filename]/date`
|
||||
- Original photo date taken, unix timestamp
|
||||
- Generator: preview
|
||||
- `photometadata/[filename]/title`
|
||||
- Photo title
|
||||
- `photometadata/[filename]/tags`
|
||||
- Photo tags
|
||||
- `metadata/title`
|
||||
- Photo album title
|
||||
- `metadata/description`
|
||||
- Photo album description, markdown
|
||||
- `metadata/ordering`
|
||||
- Manual sort ordering, newline separated
|
||||
- `internal/control`
|
||||
- Access control settings, JSON
|
||||
|
||||
## `admin`
|
||||
|
||||
Create new buckets. Standalone tool.
|
||||
|
||||
## `control`
|
||||
|
||||
Implement access controls by signing or proxying requests.
|
||||
- Standalone tool to manage an S3-like account
|
||||
- In-browser handling of:
|
||||
- ListBuckets
|
||||
- MakeBucket
|
||||
- RemoveBucket
|
||||
- Setup photo bucket
|
||||
- Uploads initial metadata
|
||||
- SetBucketPolicy that matches
|
||||
- Adds credentials to database.
|
||||
- Disconnect photo bucket
|
||||
- Removes credentials from database
|
||||
|
||||
### Operations
|
||||
|
||||
#### `GET /list?bucket=BUCKET&auth=TOKEN`
|
||||
#### `PUT /credential?bucket=BUCKET`
|
||||
|
||||
1. Consult the bucket for metadata.json
|
||||
2. Get list access method for the bucket
|
||||
3. Validate the token against the access method
|
||||
4. Return ListObjectsV2 for prefix `photo/`
|
||||
- Can also 307 redirect to the bucket read URL, if is public readable
|
||||
1. Ensure credentials work on BUCKET
|
||||
2. Write credentials to KV store
|
||||
|
||||
#### `GET /read?bucket=BUCKET&auth=TOKEN&object=OBJECTNAME`
|
||||
#### `DELETE /credential?bucket=BUCKET`
|
||||
|
||||
1. Consult the bucket for metadata.json
|
||||
1. Ensure credentials work on BUCKET
|
||||
2. Write credentials to KV store
|
||||
|
||||
### Data
|
||||
|
||||
Credentials must be passed as:
|
||||
```
|
||||
{
|
||||
"access_key": string,
|
||||
"secret_key": string,
|
||||
"region": string
|
||||
}
|
||||
```
|
||||
|
||||
## `control`
|
||||
|
||||
Implement access controls by signing requests, using credentials in database.
|
||||
|
||||
### Operations
|
||||
|
||||
#### `GET /sign?bucket=BUCKET&token=TOKEN&request=REQUEST`
|
||||
|
||||
1. Consult the bucket for `metadata/control` with credentials
|
||||
2. Get read access method for the bucket
|
||||
3. Validate the token against the access method
|
||||
4. Validate that OBJECTNAME starts with `photo/`
|
||||
4. Validate REQUEST is an acceptable request
|
||||
- ListObjectsV2, GetObject
|
||||
- Not `internal/`
|
||||
5. If necessary, presign an object URL for 4 days
|
||||
- Cache presigned URLs for 2 days in memory/Redis
|
||||
- Cache presigned URLs for 2 days in memory/Redis
|
||||
6. 307 redirect to presigned URL
|
||||
|
||||
#### `PUT /write?bucket=BUCKET&auth=TOKEN&object=OBJECTNAME`
|
||||
#### `PUT /sign?bucket=BUCKET&token=TOKEN&request=REQUEST`
|
||||
|
||||
1. Consult the bucket for metadata.json
|
||||
1. Consult the bucket for `metadata/control` with credentials
|
||||
2. Get write access method for the bucket
|
||||
3. Validate the token against the access method
|
||||
4. Validate that OBJECTNAME starts with `photo/`
|
||||
4. Validate REQUEST is an acceptable request
|
||||
- PutObject
|
||||
- Not `internal/`
|
||||
5. If necessary, presign an object URL for 30 minutes
|
||||
6. 307 redirect to presigned URL
|
||||
|
||||
#### `DELETE /sign?bucket=BUCKET&token=TOKEN&request=REQUEST`
|
||||
|
||||
1. Consult the bucket for `metadata/control` with credentials
|
||||
2. Get write access method for the bucket
|
||||
3. Validate the token against the access method
|
||||
4. Validate REQUEST is an acceptable request
|
||||
- RemoveObject
|
||||
- Not `internal/`
|
||||
5. If necessary, presign an object URL for 30 minutes
|
||||
6. 307 redirect to presigned URL
|
||||
|
||||
|
@ -58,27 +116,24 @@ The read/write token is checked against a simple string defined in the bucket.
|
|||
|
||||
Recommended IDP: [dex](https://github.com/dexidp/dex)
|
||||
|
||||
The read/write operation is gated by a signed key corresponding to allowed
|
||||
users defined in the bucket.
|
||||
The read/write operation is gated by a signed key corresponding to allowed users defined in the bucket.
|
||||
|
||||
## `web`
|
||||
|
||||
Generates the web interface for a photo bucket. Also updates the shared asset bucket on start.
|
||||
Generates the web interface for a photo bucket. Also serves up the shared assets.
|
||||
|
||||
### Operations
|
||||
|
||||
#### `POST /webhook`
|
||||
#### `POST /update?bucket=BUCKET`
|
||||
|
||||
Regenerate and upload `index.html` and `manage/index.html` to bucket.
|
||||
|
||||
## `preview`
|
||||
|
||||
Generate previews from photo buckets. Registers webhooks.
|
||||
Generate previews from photo buckets.
|
||||
|
||||
### Operations
|
||||
|
||||
#### `POST /webhook`
|
||||
#### `POST /update?bucket=BUCKET&photo=OBJECT`
|
||||
|
||||
1. Perform preview generation using libvips (maybe limit?)
|
||||
|
@ -86,7 +141,7 @@ Generate previews from photo buckets. Registers webhooks.
|
|||
|
||||
## `proxy`
|
||||
|
||||
Reverse proxies buckets to the minio endpoint, as a substitute for the AWS S3 website hosting features. Serves up `index.html` when URLs end in a slash.
|
||||
Reverse proxies buckets to the MinIO endpoint, as a substitute for AWS S3 website hosting features. Serves up `index.html` when URLs end in a slash.
|
||||
|
||||
In production, [replace this with Nginx](https://docs.min.io/docs/setup-nginx-proxy-with-minio).
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:alpine AS build
|
||||
FROM golang:buster AS build
|
||||
|
||||
RUN mkdir /src /dist
|
||||
|
||||
|
@ -10,7 +10,7 @@ ENV CGO_ENABLED=0
|
|||
RUN go build -o /dist/web
|
||||
|
||||
|
||||
FROM scratch
|
||||
FROM debian:buster
|
||||
|
||||
COPY --from=build /dist/web /web
|
||||
|
||||
|
|
|
@ -3,114 +3,90 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.makerforce.io/photos/photos/internal/httphelpers"
|
||||
lib "git.makerforce.io/photos/photos/pkg/bucket"
|
||||
"git.makerforce.io/photos/photos/pkg/credentials"
|
||||
"git.makerforce.io/photos/photos/pkg/signer"
|
||||
)
|
||||
|
||||
var signer *lib.Signer
|
||||
var creds *credentials.Client
|
||||
var sig *signer.Signer
|
||||
|
||||
func main() {
|
||||
// Setup bucket signer
|
||||
var err error
|
||||
signer, err = lib.NewSignerFromEnv()
|
||||
// Setup bucket credential source
|
||||
creds, err = credentials.NewClientFromEnv()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Setup bucket signer
|
||||
sig, err = signer.NewSignerFromEnv()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":8000",
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
http.HandleFunc("/list", list)
|
||||
http.HandleFunc("/read", read)
|
||||
http.HandleFunc("/write", write)
|
||||
http.HandleFunc("/sign", sign)
|
||||
err = server.ListenAndServe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func list(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
func sign(w http.ResponseWriter, req *http.Request) {
|
||||
var err error
|
||||
|
||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||
token := req.FormValue("token")
|
||||
request := SafePathable(req.FormValue("request"))
|
||||
|
||||
err = bucket.Validate()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
err = request.Validate()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var cred credentials.Credential
|
||||
if token == "todo" {
|
||||
cred, err = creds.Get(bucket)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: error getting credentials: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var signedReq *http.Request
|
||||
unsignedReq, err := http.NewRequest(req.Method, bucket.URL(request).String(), nil)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: error creating request: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == http.MethodGet {
|
||||
signedReq = sig.PreSignRead(unsignedReq, cred)
|
||||
} else if req.Method == http.MethodPost || req.Method == http.MethodPut || req.Method == http.MethodDelete {
|
||||
signedReq = sig.PreSignRead(unsignedReq, cred)
|
||||
} else {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||
startAfter := req.FormValue("start-after")
|
||||
maxKeysString := req.FormValue("max-keys")
|
||||
if maxKeysString == "" {
|
||||
maxKeysString = "4000"
|
||||
}
|
||||
maxKeys, err := strconv.Atoi(maxKeysString) // Let minio handle error
|
||||
|
||||
url, err := signer.GetBucketPhotos(bucket, startAfter, maxKeys)
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Location", url)
|
||||
w.Header().Add("Location", signedReq.URL.String())
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func read(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||
photo := lib.Photo(req.FormValue("object"))
|
||||
preview := lib.Preview(req.FormValue("object"))
|
||||
|
||||
if photo.Validate() == nil {
|
||||
url, err := signer.GetPhoto(bucket, photo)
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Location", url)
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
return
|
||||
} else if preview.Validate() == nil {
|
||||
url, err := signer.GetPreview(bucket, preview)
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Location", url)
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
httphelpers.ErrorResponse(w, httphelpers.ErrorBadRequest)
|
||||
}
|
||||
|
||||
func write(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPut {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||
photo := lib.Photo(req.FormValue("object"))
|
||||
|
||||
if photo.Validate() == nil {
|
||||
url, err := signer.PutPhoto(bucket, photo)
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Location", url)
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
httphelpers.ErrorResponse(w, httphelpers.ErrorBadRequest)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SafePathable string
|
||||
|
||||
var ErrorInvalidRequest = errors.New("invalid request")
|
||||
|
||||
func (s SafePathable) Validate() error {
|
||||
if strings.HasPrefix(string(s), "/") {
|
||||
return ErrorInvalidRequest
|
||||
}
|
||||
if strings.HasPrefix(string(s), "internal/") {
|
||||
return ErrorInvalidRequest
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SafePathable) Path() *url.URL {
|
||||
return &url.URL{
|
||||
Path: string(s),
|
||||
}
|
||||
}
|
|
@ -4,15 +4,17 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.makerforce.io/photos/photos/internal/httphelpers"
|
||||
lib "git.makerforce.io/photos/photos/pkg/bucket"
|
||||
"github.com/davidbyttow/govips/pkg/vips"
|
||||
"github.com/minio/minio-go"
|
||||
)
|
||||
|
||||
var client *lib.Client
|
||||
var creds *credentials.Cred
|
||||
var sig *signer.Signer
|
||||
|
||||
type resizeOperation struct {
|
||||
input []byte
|
||||
|
@ -22,12 +24,15 @@ type resizeOperation struct {
|
|||
took time.Duration
|
||||
}
|
||||
|
||||
const maxAspectRatio = 6
|
||||
|
||||
func main() {
|
||||
// Setup bucket client
|
||||
var err error
|
||||
client, err = lib.NewClientFromEnv()
|
||||
// Setup bucket credential source
|
||||
creds, err = credentials.NewClientFromEnv()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Setup bucket signer
|
||||
sig, err = signer.NewSignerFromEnv()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -36,6 +41,17 @@ func main() {
|
|||
vips.Startup(nil)
|
||||
defer vips.Shutdown()
|
||||
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 4,
|
||||
MaxIdleConnsPerHost: 4,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
DisableCompression: true,
|
||||
}
|
||||
http.DefaultClient = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":8003",
|
||||
ReadTimeout: 5 * time.Second,
|
||||
|
@ -57,32 +73,59 @@ func update(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||
photo := lib.Photo(req.FormValue("photo"))
|
||||
|
||||
err = bucket.Validate()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
err = photo.Validate()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Obtain bucket credentials
|
||||
cred, err := creds.Get(bucket)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: error getting credentials: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Obtain preview options
|
||||
/*
|
||||
bucketMetadata, err := client.GetBucketMetadata(bucket)
|
||||
previewOptions := bucketMetadata.PreviewOptions
|
||||
*/
|
||||
previewOptions := lib.DefaultPreviewOptions()
|
||||
|
||||
original, err := client.GetPhoto(bucket, photo)
|
||||
// Get photo
|
||||
original, err := getPhoto(bucket, cred, photo)
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// First, extract photo size
|
||||
width, height, err := readsize(original)
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
metadata, _ := client.GetPhotoMetadata(bucket, photo)
|
||||
metadata.Width = width
|
||||
metadata.Height = height
|
||||
err = client.PutPhotoMetadata(bucket, photo, metadata)
|
||||
s := bucket.PhotoMetadataSize{
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
err = putPhotoMetadataSize(bucket, cred, photo, s)
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Do other resizes
|
||||
resizes := make([]resizeOperation, len(previewOptions))
|
||||
for i := range resizes {
|
||||
resizes[i] = resizeOperation{
|
||||
|
@ -151,40 +194,3 @@ func update(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
w.Header().Add("X-Debug-Resize-Timings", fmt.Sprintf("%v", timings))
|
||||
}
|
||||
|
||||
func readsize(buf []byte) (int, int, error) {
|
||||
i, err := vips.NewImageFromBuffer(buf)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return i.Width(), i.Height(), nil
|
||||
}
|
||||
|
||||
func resize(buf []byte, height int, format vips.ImageType, quality int) ([]byte, error) {
|
||||
i, err := vips.NewImageFromBuffer(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer i.Close()
|
||||
|
||||
scale := float64(height) / float64(i.Height())
|
||||
err = i.Resize(scale)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outBuf, _, err := i.Export(vips.ExportParams{
|
||||
Format: format,
|
||||
Quality: quality,
|
||||
})
|
||||
return outBuf, err
|
||||
}
|
||||
|
||||
func toVipsImageType(format lib.PhotoFormat) vips.ImageType {
|
||||
if format == lib.PhotoFormatWEBP {
|
||||
return vips.ImageTypeWEBP
|
||||
}
|
||||
if format == lib.PhotoFormatJPEG {
|
||||
return vips.ImageTypeJPEG
|
||||
}
|
||||
return vips.ImageTypeUnknown
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"git.makerforce.io/photos/photos/internal/httphelpers"
|
||||
lib "git.makerforce.io/photos/photos/pkg/bucket"
|
||||
"git.makerforce.io/photos/photos/pkg/credentials"
|
||||
)
|
||||
|
||||
// TODO: handle errors nicer
|
||||
|
||||
func getPhoto(bucket lib.Bucket, cred credentials.Credential, photo lib.Photo) ([]byte, error) {
|
||||
unsignedReq, err := http.NewRequest(http.MethodGET, bucket.URL(photo).String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signedReq := sig.Sign(unsignedReq, cred)
|
||||
|
||||
resp, err := http.DefaultClient.Do(signedReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, httphelpers.ErrorFromStatus(resp.StatusCode)
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func putPhotoMetadataSize(bucket lib.Bucket, cred credentials.Credential, photo lib.Photo, s lib.PhotoMetadataSize) error {
|
||||
buf, err := lib.PhotoMetadataSizeMarshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unsignedReq, err := http.NewRequest(http.MethodPUT, bucket.URL(photo.MetadataSize()).String(), bytes.NewBuffer(buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signedReq := sig.Sign(unsignedReq, cred)
|
||||
|
||||
resp, err := http.DefaultClient.Do(signedReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return httphelpers.ErrorFromStatus(resp.StatusCode)
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
lib "git.makerforce.io/photos/photos/pkg/bucket"
|
||||
"github.com/davidbyttow/govips/pkg/vips"
|
||||
)
|
||||
|
||||
func readsize(buf []byte) (int, int, error) {
|
||||
i, err := vips.NewImageFromBuffer(buf)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return i.Width(), i.Height(), nil
|
||||
}
|
||||
|
||||
func resize(buf []byte, height int, format vips.ImageType, quality int) ([]byte, error) {
|
||||
i, err := vips.NewImageFromBuffer(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer i.Close()
|
||||
|
||||
scale := float64(height) / float64(i.Height())
|
||||
err = i.Resize(scale)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outBuf, _, err := i.Export(vips.ExportParams{
|
||||
Format: format,
|
||||
Quality: quality,
|
||||
})
|
||||
return outBuf, err
|
||||
}
|
||||
|
||||
func toVipsImageType(format lib.PhotoFormat) vips.ImageType {
|
||||
if format == lib.PhotoFormatWEBP {
|
||||
return vips.ImageTypeWEBP
|
||||
}
|
||||
if format == lib.PhotoFormatJPEG {
|
||||
return vips.ImageTypeJPEG
|
||||
}
|
||||
return vips.ImageTypeUnknown
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// This is honestly terrible. It only exists to replace the AWS S3 website hosting feature missing in minio
|
||||
// This is honestly terrible. It only exists to replace the AWS S3 website hosting feature missing in MinIO
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -10,7 +10,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
internal_s3utils "git.makerforce.io/photos/photos/internal/s3utils"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
|
@ -26,6 +25,17 @@ func main() {
|
|||
domain = os.Getenv("MINIO_DOMAIN")
|
||||
behindProxy = os.Getenv("BEHIND_PROXY") == "true"
|
||||
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 4,
|
||||
MaxIdleConnsPerHost: 4,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
DisableCompression: true,
|
||||
}
|
||||
http.DefaultClient = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":80",
|
||||
ReadTimeout: 5 * time.Second,
|
||||
|
@ -56,14 +66,7 @@ func director(req *http.Request) {
|
|||
req.URL.Scheme = "https"
|
||||
}
|
||||
req.URL.Host = endpoint
|
||||
if len(domain) < 1 {
|
||||
// If we are using domains, rewrite the host into path
|
||||
req.URL.Path = internal_s3utils.PathFromHost(mapPath(*req), host)
|
||||
} else {
|
||||
// If we are using subdomains, set the host header
|
||||
req.URL.Path = mapPath(*req)
|
||||
req.Header.Set("Host", host)
|
||||
}
|
||||
req.URL.Path = pathFromHost(mapPath(*req), host)
|
||||
|
||||
// Prevent MINIO from issuing redirects
|
||||
userAgent := req.Header.Get("User-Agent")
|
||||
|
@ -94,3 +97,10 @@ func mapPath(req http.Request) string {
|
|||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func pathFromHost(path, host string) string {
|
||||
if len(host) > 0 {
|
||||
return "/" + host + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
|
|
@ -42,6 +42,118 @@ spec:
|
|||
- protocol: TCP
|
||||
port: 80
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: photos-control
|
||||
labels:
|
||||
app: photos-control
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: photos-control
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: photos-control
|
||||
spec:
|
||||
containers:
|
||||
- name: control
|
||||
image: registry.makerforce.io/photos/control:dev
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: MINIO_ENDPOINT
|
||||
value: api.ambrose.photos:9000
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: accesskey
|
||||
name: minio
|
||||
- name: MINIO_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: secretkey
|
||||
name: minio
|
||||
- name: MINIO_REGION_NAME
|
||||
value: "sgp1"
|
||||
- name: MINIO_ENDPOINT_SECURE
|
||||
value: "false"
|
||||
- name: MINIO_CREDENTIALS_BUCKET
|
||||
value: "credentials"
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: photos-control
|
||||
spec:
|
||||
selector:
|
||||
app: photos-control
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8000
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: photos-preview
|
||||
labels:
|
||||
app: photos-preview
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: photos-preview
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: photos-preview
|
||||
spec:
|
||||
containers:
|
||||
- name: preview
|
||||
image: registry.makerforce.io/photos/preview:dev
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: MINIO_ENDPOINT
|
||||
value: api.ambrose.photos:9000
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: accesskey
|
||||
name: minio
|
||||
- name: MINIO_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: secretkey
|
||||
name: minio
|
||||
- name: MINIO_REGION_NAME
|
||||
value: "sgp1"
|
||||
- name: MINIO_ENDPOINT_SECURE
|
||||
value: "false"
|
||||
- name: MINIO_CREDENTIALS_BUCKET
|
||||
value: "credentials"
|
||||
ports:
|
||||
- containerPort: 8003
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: photos-preview
|
||||
spec:
|
||||
selector:
|
||||
app: photos-preview
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8003
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
|
@ -66,10 +178,6 @@ spec:
|
|||
env:
|
||||
- name: MINIO_ENDPOINT
|
||||
value: api.ambrose.photos:9000
|
||||
- name: MINIO_DOMAIN
|
||||
value: ""
|
||||
- name: MINIO_ENDPOINT_SECURE
|
||||
value: "false"
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
@ -80,6 +188,12 @@ spec:
|
|||
secretKeyRef:
|
||||
key: secretkey
|
||||
name: minio
|
||||
- name: MINIO_REGION_NAME
|
||||
value: "sgp1"
|
||||
- name: MINIO_ENDPOINT_SECURE
|
||||
value: "false"
|
||||
- name: MINIO_CREDENTIALS_BUCKET
|
||||
value: "credentials"
|
||||
- name: WEB_ENDPOINT
|
||||
value: "http://web.ambrose.photos"
|
||||
ports:
|
||||
|
|
2
go.mod
2
go.mod
|
@ -7,7 +7,9 @@ require (
|
|||
github.com/elazarl/go-bindata-assetfs v1.0.0
|
||||
github.com/go-bindata/go-bindata v1.0.0 // indirect
|
||||
github.com/go-bindata/go-bindata/v3 v3.1.3 // indirect
|
||||
github.com/go-ini/ini v1.57.0 // indirect
|
||||
github.com/miekg/dns v1.1.29
|
||||
github.com/minio/minio-go v6.0.14+incompatible
|
||||
github.com/minio/minio-go/v6 v6.0.55
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
|
||||
golang.org/x/tools v0.0.0-20200530233709-52effbd89c51 // indirect
|
||||
|
|
5
go.sum
5
go.sum
|
@ -10,6 +10,8 @@ github.com/go-bindata/go-bindata v1.0.0 h1:DZ34txDXWn1DyWa+vQf7V9ANc2ILTtrEjtlsd
|
|||
github.com/go-bindata/go-bindata v1.0.0/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
|
||||
github.com/go-bindata/go-bindata/v3 v3.1.3 h1:F0nVttLC3ws0ojc7p60veTurcOm//D4QBODNM7EGrCI=
|
||||
github.com/go-bindata/go-bindata/v3 v3.1.3/go.mod h1:1/zrpXsLD8YDIbhZRqXzm1Ghc7NhEvIN9+Z6R5/xH4I=
|
||||
github.com/go-ini/ini v1.57.0 h1:Qwzj3wZQW+Plax5Ntj+GYe07DfGj1OH+aL1nMTMaNow=
|
||||
github.com/go-ini/ini v1.57.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
|
@ -22,6 +24,9 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL
|
|||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/minio/minio-go v1.0.0 h1:ooSujki+Z1PRGZsYffJw5jnF5eMBvzMVV86TLAlM0UM=
|
||||
github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJDswNVuni6km9o=
|
||||
github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
|
||||
github.com/minio/minio-go/v6 v6.0.55 h1:Hqm41952DdRNKXM+6hCnPXCsHCYSgLf03iuYoxJG2Wk=
|
||||
github.com/minio/minio-go/v6 v6.0.55/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
|
||||
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
|
||||
|
|
|
@ -2,6 +2,7 @@ package httphelpers
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio-go/v6"
|
||||
|
@ -9,6 +10,7 @@ import (
|
|||
|
||||
var ErrorBadRequest = errors.New("bad request")
|
||||
var ErrorMethodNotAllowed = errors.New("method not allowed")
|
||||
var ErrorNotFound = errors.New("not found")
|
||||
|
||||
func ErrorResponse(w http.ResponseWriter, err error) {
|
||||
if err == nil {
|
||||
|
@ -23,6 +25,9 @@ func ErrorResponse(w http.ResponseWriter, err error) {
|
|||
if errors.Is(err, ErrorMethodNotAllowed) {
|
||||
errorStatus = http.StatusMethodNotAllowed
|
||||
}
|
||||
if errors.Is(err, ErrorNotFound) {
|
||||
errorStatus = http.StatusNotFound
|
||||
}
|
||||
|
||||
var minioErrorResponse minio.ErrorResponse
|
||||
if errors.As(err, &minioErrorResponse) {
|
||||
|
@ -32,10 +37,17 @@ func ErrorResponse(w http.ResponseWriter, err error) {
|
|||
if minioErrorResponse.Code == "NoSuchBucket" {
|
||||
errorStatus = http.StatusNotFound
|
||||
}
|
||||
// Do not handle minio AccessDenied: that's a server error. Our credentials should always be correct
|
||||
// Do not handle MinIO AccessDenied: that's a server error. Our credentials should always be correct
|
||||
}
|
||||
|
||||
// Let Header.Write() handle escaping
|
||||
w.Header().Add("X-Debug-Error", errorMessage)
|
||||
w.WriteHeader(errorStatus)
|
||||
}
|
||||
|
||||
func ErrorFromStatus(code int) error {
|
||||
if code == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s", http.StatusText(code))
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
package s3utils
|
||||
|
||||
func PathFromHost(path, host string) string {
|
||||
if len(host) > 0 {
|
||||
return "/" + host + path
|
||||
}
|
||||
return path
|
||||
}
|
|
@ -3,16 +3,26 @@ package bucket
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Bucket string
|
||||
|
||||
const bucketMetadataObject = "metadata.json"
|
||||
const bucketMetadataPrefix = "metadata/"
|
||||
|
||||
var ErrorInvalidBucket = errors.New("invalid bucket")
|
||||
|
||||
func (b Bucket) Validate() error {
|
||||
if len(b) < 1 || b == "meta" {
|
||||
// Ensure is URL
|
||||
u, err := url.ParseRequestURI(string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w: %v", ErrorInvalidBucket, err, b)
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return fmt.Errorf("%w: %v", ErrorInvalidBucket, b)
|
||||
}
|
||||
if u.Host == "" || !strings.HasSuffix(u.Path, "/") {
|
||||
return fmt.Errorf("%w: %v", ErrorInvalidBucket, b)
|
||||
}
|
||||
return nil
|
||||
|
@ -22,7 +32,55 @@ func (b Bucket) String() string {
|
|||
return string(b)
|
||||
}
|
||||
|
||||
type BucketMetadata struct {
|
||||
Title string `json:"title"`
|
||||
PreviewOptions []PreviewOption `json:"preview_options"`
|
||||
func (b Bucket) URL(p Pathable) *url.URL {
|
||||
u, _ := url.ParseRequestURI(string(b))
|
||||
path := p.Path()
|
||||
u.Path = pathJoin(u.Path, path.Path)
|
||||
u.RawQuery = path.RawQuery
|
||||
return u
|
||||
}
|
||||
|
||||
func pathJoin(paths ...string) string {
|
||||
cleanedPaths := make([]string, 0, len(paths))
|
||||
for i, path := range paths {
|
||||
cleanedPath := strings.TrimPrefix(path, "/")
|
||||
if i != len(paths)-1 {
|
||||
cleanedPath = strings.TrimSuffix(cleanedPath, "/")
|
||||
}
|
||||
if i == 0 && len(cleanedPath) == 0 {
|
||||
continue
|
||||
}
|
||||
cleanedPaths = append(cleanedPaths, cleanedPath)
|
||||
}
|
||||
return "/" + strings.Join(cleanedPaths, "/")
|
||||
}
|
||||
|
||||
type Pathable interface {
|
||||
Path() *url.URL
|
||||
}
|
||||
|
||||
type BucketMetadata string
|
||||
|
||||
func (b Bucket) Metadata(name string) BucketMetadata {
|
||||
return BucketMetadata(bucketMetadataPrefix + name)
|
||||
}
|
||||
|
||||
func (m BucketMetadata) String() string {
|
||||
return string(m)
|
||||
}
|
||||
|
||||
func (m BucketMetadata) Path() *url.URL {
|
||||
return &url.URL{
|
||||
Path: string(m),
|
||||
}
|
||||
}
|
||||
|
||||
func (b Bucket) MetadataTitle() BucketMetadata {
|
||||
return b.Metadata("title")
|
||||
}
|
||||
func (b Bucket) MetadataDescription() BucketMetadata {
|
||||
return b.Metadata("description")
|
||||
}
|
||||
func (b Bucket) MetadataOrdering() BucketMetadata {
|
||||
return b.Metadata("ordering")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package bucket
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBucket_Validate(t *testing.T) {
|
||||
var got error
|
||||
|
||||
got = Bucket("http://api.url").Validate()
|
||||
if got == nil {
|
||||
t.Errorf("Bucket.Validate() = %v; want non-nil", got)
|
||||
}
|
||||
|
||||
got = Bucket("http://api.url/").Validate()
|
||||
if got != nil {
|
||||
t.Errorf("Bucket.Validate() = %v; want nil", got)
|
||||
}
|
||||
|
||||
got = Bucket("http://api.url/bucket").Validate()
|
||||
if got == nil {
|
||||
t.Errorf("Bucket.Validate() = %v; want non-nil", got)
|
||||
}
|
||||
|
||||
got = Bucket("http://api.url/bucket/").Validate()
|
||||
if got != nil {
|
||||
t.Errorf("Bucket.Validate() = %v; want nil", got)
|
||||
}
|
||||
|
||||
got = Bucket("http://api.url/bucket/path/").Validate()
|
||||
if got != nil {
|
||||
t.Errorf("Bucket.Validate() = %v; want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBucket_URL(t *testing.T) {
|
||||
var got, want string
|
||||
var b Bucket
|
||||
|
||||
b = Bucket("http://api.url/bucket/")
|
||||
|
||||
got = b.URL(Photo("/photo/hello")).String()
|
||||
want = "http://api.url/bucket/photo/hello"
|
||||
if got != want {
|
||||
t.Errorf("Bucket.URL(Photo) = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
got = b.URL(Preview("preview/hello")).String()
|
||||
want = "http://api.url/bucket/preview/hello"
|
||||
if got != want {
|
||||
t.Errorf("Bucket.URL(Photo) = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
got = b.URL(NewList("photo/", 1000, "name")).String()
|
||||
want = "http://api.url/bucket/?delimiter=&encoding-type=url&list-type=2&max-keys=1000&metadata=true&prefix=photo%2F&start-after=name"
|
||||
if got != want {
|
||||
t.Errorf("Bucket.URL(Photo) = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
b = Bucket("http://bucket.api.url")
|
||||
|
||||
got = b.URL(Photo("/photo/hello")).String()
|
||||
want = "http://bucket.api.url/photo/hello"
|
||||
if got != want {
|
||||
t.Errorf("Bucket.URL(Photo) = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
got = b.URL(Preview("preview/hello")).String()
|
||||
want = "http://bucket.api.url/preview/hello"
|
||||
if got != want {
|
||||
t.Errorf("Bucket.URL(Photo) = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
got = b.URL(NewList("photo/", 1000, "name")).String()
|
||||
want = "http://bucket.api.url" + NewList("photo/", 1000, "name").Path().String()
|
||||
if got != want {
|
||||
t.Errorf("Bucket.URL(Photo) = %v; want %v", got, want)
|
||||
}
|
||||
}
|
|
@ -1,205 +0,0 @@
|
|||
package bucket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/minio/minio-go/v6"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
*minio.Client
|
||||
}
|
||||
|
||||
func NewClient(endpoint, accessKey, secretKey, regionName string, endpointSecure bool) (*Client, error) {
|
||||
m, err := minio.NewWithRegion(endpoint, accessKey, secretKey, endpointSecure, regionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
Client: m,
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func NewClientFromEnv() (*Client, error) {
|
||||
accessKey := os.Getenv("MINIO_ACCESS_KEY")
|
||||
secretKey := os.Getenv("MINIO_SECRET_KEY")
|
||||
regionName := os.Getenv("MINIO_REGION_NAME")
|
||||
endpoint := os.Getenv("MINIO_ENDPOINT")
|
||||
endpointSecure := os.Getenv("MINIO_ENDPOINT_SECURE") == "true"
|
||||
return NewClient(endpoint, accessKey, secretKey, regionName, endpointSecure)
|
||||
}
|
||||
|
||||
func (c *Client) GetBucketMetadata(b Bucket) (BucketMetadata, error) {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return BucketMetadata{}, err
|
||||
}
|
||||
|
||||
o, err := c.GetObject(b.String(), bucketMetadataObject, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return BucketMetadata{}, err
|
||||
}
|
||||
defer o.Close()
|
||||
buf, err := ioutil.ReadAll(o)
|
||||
if err != nil {
|
||||
return BucketMetadata{}, err
|
||||
}
|
||||
|
||||
m := BucketMetadata{}
|
||||
err = json.Unmarshal(buf, &m)
|
||||
if err != nil {
|
||||
return BucketMetadata{}, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (c *Client) PutBucketMetadata(b Bucket, m BucketMetadata) error {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := json.MarshalIndent(m, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.PutObject(b.String(), bucketMetadataObject, bytes.NewBuffer(buf), int64(len(buf)), minio.PutObjectOptions{
|
||||
ContentType: "application/json",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PutBucketWeb(b Bucket, buf []byte) error {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.PutObject(b.String(), "index.html", bytes.NewBuffer(buf), int64(len(buf)), minio.PutObjectOptions{
|
||||
ContentType: "text/html",
|
||||
StorageClass: "REDUCED_REDUNDANCY",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetPhotos(b Bucket) ([]Photo, error) {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys := make([]Photo, 0)
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
objCh := c.ListObjectsV2(b.String(), photoPrefix, true, doneCh)
|
||||
for obj := range objCh {
|
||||
if obj.Err != nil {
|
||||
return nil, obj.Err
|
||||
}
|
||||
keys = append(keys, Photo(obj.Key))
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetPhoto(b Bucket, p Photo) ([]byte, error) {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = p.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o, err := c.GetObject(b.String(), p.String(), minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer o.Close()
|
||||
return ioutil.ReadAll(o)
|
||||
}
|
||||
|
||||
func (c *Client) GetPhotoMetadata(b Bucket, p Photo) (PhotoMetadata, error) {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return PhotoMetadata{}, err
|
||||
}
|
||||
err = p.Validate()
|
||||
if err != nil {
|
||||
return PhotoMetadata{}, err
|
||||
}
|
||||
|
||||
o, err := c.GetObject(b.String(), p.MetadataString(), minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return PhotoMetadata{}, err
|
||||
}
|
||||
defer o.Close()
|
||||
buf, err := ioutil.ReadAll(o)
|
||||
if err != nil {
|
||||
return PhotoMetadata{}, err
|
||||
}
|
||||
|
||||
m := PhotoMetadata{}
|
||||
err = json.Unmarshal(buf, &m)
|
||||
if err != nil {
|
||||
return PhotoMetadata{}, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (c *Client) PutPhotoMetadata(b Bucket, p Photo, m PhotoMetadata) error {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := json.MarshalIndent(m, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.PutObject(b.String(), p.MetadataString(), bytes.NewBuffer(buf), int64(len(buf)), minio.PutObjectOptions{
|
||||
ContentType: "application/json",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PutPreview(b Bucket, p Preview, buf []byte) error {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.PutObject(b.String(), p.String(), bytes.NewBuffer(buf), int64(len(buf)), minio.PutObjectOptions{
|
||||
ContentType: p.Format().ContentType(),
|
||||
StorageClass: "REDUCED_REDUNDANCY",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package bucket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
)
|
||||
|
||||
type List struct {
|
||||
prefix string
|
||||
maxKeys int
|
||||
startAfter string
|
||||
}
|
||||
|
||||
func NewList(prefix string, maxKeys int, startAfter string) List {
|
||||
return List{
|
||||
prefix: prefix,
|
||||
maxKeys: maxKeys,
|
||||
startAfter: startAfter,
|
||||
}
|
||||
}
|
||||
|
||||
func (l List) Path() *url.URL {
|
||||
params := make(url.Values)
|
||||
params.Set("list-type", "2")
|
||||
params.Set("metadata", "true")
|
||||
params.Set("encoding-type", "url")
|
||||
params.Set("prefix", l.prefix)
|
||||
params.Set("delimiter", "")
|
||||
params.Set("max-keys", fmt.Sprintf("%d", l.maxKeys))
|
||||
params.Set("start-after", l.startAfter)
|
||||
return &url.URL{
|
||||
Path: "/",
|
||||
RawQuery: s3utils.QueryEncode(params),
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package bucket
|
|
@ -10,6 +10,7 @@ import (
|
|||
type Photo string
|
||||
|
||||
const photoPrefix = "photo/"
|
||||
const photoMetadataPrefix = "photometadata/"
|
||||
|
||||
var ErrorInvalidPhoto = errors.New("invalid photo")
|
||||
|
||||
|
@ -17,6 +18,9 @@ func (p Photo) Validate() error {
|
|||
if !strings.HasPrefix(string(p), photoPrefix) {
|
||||
return fmt.Errorf("%w: %v", ErrorInvalidPhoto, p)
|
||||
}
|
||||
if strings.HasSuffix(string(p), "/") {
|
||||
return fmt.Errorf("%w: %v", ErrorInvalidPhoto, p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -24,23 +28,38 @@ func (p Photo) String() string {
|
|||
return string(p)
|
||||
}
|
||||
|
||||
func (p Photo) Path() string {
|
||||
u := url.URL{
|
||||
func (p Photo) Path() *url.URL {
|
||||
return &url.URL{
|
||||
Path: string(p),
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
const photoMetadataPrefix = "photometa/"
|
||||
type PhotoMetadata string
|
||||
|
||||
func (p Photo) MetadataString() string {
|
||||
func (p Photo) Metadata(name string) PhotoMetadata {
|
||||
objectBase := strings.Replace(string(p), photoPrefix, photoMetadataPrefix, 1)
|
||||
objectBase += ".metadata.json"
|
||||
return objectBase
|
||||
return PhotoMetadata(objectBase + "/" + name)
|
||||
}
|
||||
|
||||
type PhotoMetadata struct {
|
||||
Title string `json:"title"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
func (m PhotoMetadata) String() string {
|
||||
return string(m)
|
||||
}
|
||||
|
||||
func (m PhotoMetadata) Path() *url.URL {
|
||||
return &url.URL{
|
||||
Path: string(m),
|
||||
}
|
||||
}
|
||||
|
||||
func (p Photo) MetadataSize() PhotoMetadata {
|
||||
return p.Metadata("size")
|
||||
}
|
||||
func (p Photo) MetadataDate() PhotoMetadata {
|
||||
return p.Metadata("date")
|
||||
}
|
||||
func (p Photo) MetadataTitle() PhotoMetadata {
|
||||
return p.Metadata("title")
|
||||
}
|
||||
func (p Photo) MetadataTags() PhotoMetadata {
|
||||
return p.Metadata("tags")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package bucket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type PhotoMetadataSize struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
func PhotoMetadataSizeUnmarshal(buf []byte) (PhotoMetadataSize, error) {
|
||||
s := PhotoMetadataSize{}
|
||||
err := json.Unmarshal(buf, &s)
|
||||
return s, err
|
||||
}
|
||||
|
||||
func PhotoMetadataSizeMarshal(s PhotoMetadataSize) ([]byte, error) {
|
||||
return json.MarshalIndent(s, "", "\t")
|
||||
}
|
|
@ -19,6 +19,9 @@ func (p Preview) Validate() error {
|
|||
if !strings.HasPrefix(string(p), previewPrefix) {
|
||||
return fmt.Errorf("%w: %v", ErrorInvalidPreview, p)
|
||||
}
|
||||
if strings.HasSuffix(string(p), "/") {
|
||||
return fmt.Errorf("%w: %v", ErrorInvalidPreview, p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -26,11 +29,10 @@ func (p Preview) String() string {
|
|||
return string(p)
|
||||
}
|
||||
|
||||
func (p Preview) Path() string {
|
||||
u := url.URL{
|
||||
func (p Preview) Path() *url.URL {
|
||||
return &url.URL{
|
||||
Path: string(p),
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (p Preview) Format() PhotoFormat {
|
||||
|
@ -93,7 +95,7 @@ func (p Photo) GetPreview(option PreviewOption) Preview {
|
|||
extIndex := strings.LastIndex(objectBase, ".")
|
||||
base := objectBase[:extIndex]
|
||||
res := fmt.Sprintf("_h%dq%d", option.Height, option.Quality)
|
||||
exts, err := mime.ExtensionsByType(option.Format.ContentType())
|
||||
exts, err := mime.ExtensionsByType(option.Format.ContentType()) // TODO: replace with hardcoded list
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
package bucket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
internal_s3utils "git.makerforce.io/photos/photos/internal/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/signer"
|
||||
)
|
||||
|
||||
type Signer struct {
|
||||
accessKey string
|
||||
secretKey string
|
||||
regionName string
|
||||
bucketSecure bool
|
||||
expirations Expirations
|
||||
}
|
||||
|
||||
type Expirations struct {
|
||||
// Expiration time for list and read in time.Duration
|
||||
Read time.Duration
|
||||
// Expiration time for write in time.Duration
|
||||
Write time.Duration
|
||||
}
|
||||
|
||||
var ErrorExpirationTooLow = errors.New("expiration time too low")
|
||||
|
||||
func NewSigner(accessKey, secretKey, regionName string, bucketSecure bool, expirations Expirations) (*Signer, error) {
|
||||
if expirations.Read == 0 {
|
||||
expirations.Read = 30 * time.Minute
|
||||
}
|
||||
if expirations.Write == 0 {
|
||||
expirations.Write = 5 * time.Minute
|
||||
}
|
||||
if expirations.Read < time.Second || expirations.Write < time.Second {
|
||||
return nil, ErrorExpirationTooLow
|
||||
}
|
||||
signer := &Signer{
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
regionName: regionName,
|
||||
bucketSecure: bucketSecure,
|
||||
expirations: expirations,
|
||||
}
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
func NewSignerFromEnv() (*Signer, error) {
|
||||
accessKey := os.Getenv("MINIO_ACCESS_KEY")
|
||||
secretKey := os.Getenv("MINIO_SECRET_KEY")
|
||||
regionName := os.Getenv("MINIO_REGION_NAME")
|
||||
bucketSecure := os.Getenv("BUCKET_SECURE") == "true"
|
||||
expirationRead, _ := strconv.Atoi(os.Getenv("EXPIRATION_READ"))
|
||||
expirationWrite, _ := strconv.Atoi(os.Getenv("EXPIRATION_WRITE"))
|
||||
expirations := Expirations{Read: time.Duration(expirationRead), Write: time.Duration(expirationWrite)}
|
||||
return NewSigner(accessKey, secretKey, regionName, bucketSecure, expirations)
|
||||
}
|
||||
|
||||
func (s *Signer) baseBucket(b Bucket) url.URL {
|
||||
url := url.URL{
|
||||
Scheme: "http",
|
||||
Host: b.String(),
|
||||
Path: "/",
|
||||
}
|
||||
if s.bucketSecure {
|
||||
url.Scheme = "https"
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func (s *Signer) preSignV4(req http.Request) http.Request {
|
||||
domain := os.Getenv("MINIO_DOMAIN")
|
||||
|
||||
// Validate host
|
||||
host, _, err := net.SplitHostPort(req.Host)
|
||||
if err != nil {
|
||||
host = req.Host
|
||||
}
|
||||
originalPath := req.URL.Path
|
||||
|
||||
if len(domain) < 1 {
|
||||
// If we are using domains, rewrite the host into path
|
||||
req.URL.Path = internal_s3utils.PathFromHost(req.URL.Path, host)
|
||||
}
|
||||
// If we are using subdomains, sign the direct URL
|
||||
signedReq := signer.PreSignV4(
|
||||
req,
|
||||
s.accessKey, s.secretKey, "",
|
||||
"sgp1",
|
||||
int64(s.expirations.Read/time.Second),
|
||||
)
|
||||
signedReq.URL.Path = originalPath
|
||||
return *signedReq
|
||||
}
|
||||
|
||||
func listBucketParams(prefix, startAfter string, maxKeys int) url.Values {
|
||||
params := make(url.Values)
|
||||
params.Set("list-type", "2")
|
||||
params.Set("metadata", "true")
|
||||
params.Set("encoding-type", "url")
|
||||
params.Set("prefix", prefix)
|
||||
params.Set("delimiter", "")
|
||||
params.Set("max-keys", strconv.Itoa(maxKeys))
|
||||
params.Set("start-after", startAfter)
|
||||
return params
|
||||
}
|
||||
|
||||
func (s *Signer) GetBucketPhotos(b Bucket, startAfter string, maxKeys int) (string, error) {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url := s.baseBucket(b)
|
||||
url.Path += "/"
|
||||
params := listBucketParams(photoPrefix, startAfter, maxKeys)
|
||||
url.RawQuery = s3utils.QueryEncode(params)
|
||||
|
||||
req, err := http.NewRequest("GET", url.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedReq := s.preSignV4(*req)
|
||||
return signedReq.URL.String(), nil
|
||||
}
|
||||
|
||||
func (s *Signer) GetPhoto(b Bucket, p Photo) (string, error) {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = p.Validate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url := s.baseBucket(b)
|
||||
url.Path += p.Path()
|
||||
|
||||
req, err := http.NewRequest("GET", url.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedReq := signer.PreSignV4(*req, s.accessKey, s.secretKey, "", "sgp1", int64(s.expirations.Read/time.Second))
|
||||
return signedReq.URL.String(), nil
|
||||
}
|
||||
|
||||
func (s *Signer) PutPhoto(b Bucket, p Photo) (string, error) {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = p.Validate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url := s.baseBucket(b)
|
||||
url.Path += p.Path()
|
||||
|
||||
req, err := http.NewRequest("PUT", url.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedReq := signer.PreSignV4(*req, s.accessKey, s.secretKey, "", "sgp1", int64(s.expirations.Write/time.Second))
|
||||
return signedReq.URL.String(), nil
|
||||
}
|
||||
|
||||
func (s *Signer) GetPreview(b Bucket, p Preview) (string, error) {
|
||||
err := b.Validate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = p.Validate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url := s.baseBucket(b)
|
||||
url.Path += p.Path()
|
||||
|
||||
req, err := http.NewRequest("GET", url.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signedReq := signer.PreSignV4(*req, s.accessKey, s.secretKey, "", "sgp1", int64(s.expirations.Read/time.Second))
|
||||
return signedReq.URL.String(), nil
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package credentials
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.makerforce.io/photos/photos/pkg/bucket"
|
||||
"github.com/minio/minio-go/v6"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
*minio.Client
|
||||
Bucket string
|
||||
}
|
||||
|
||||
func NewClient(endpoint, accessKey, secretKey, regionName string, endpointSecure bool, bucket string) (*Client, error) {
|
||||
m, err := minio.NewWithRegion(endpoint, accessKey, secretKey, endpointSecure, regionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
Client: m,
|
||||
Bucket: bucket,
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func NewClientFromEnv() (*Client, error) {
|
||||
accessKey := os.Getenv("MINIO_ACCESS_KEY")
|
||||
secretKey := os.Getenv("MINIO_SECRET_KEY")
|
||||
regionName := os.Getenv("MINIO_REGION_NAME")
|
||||
endpoint := os.Getenv("MINIO_ENDPOINT")
|
||||
endpointSecure := os.Getenv("MINIO_ENDPOINT_SECURE") == "true"
|
||||
bucket := os.Getenv("MINIO_CREDENTIALS_BUCKET")
|
||||
return NewClient(endpoint, accessKey, secretKey, regionName, endpointSecure, bucket)
|
||||
}
|
||||
|
||||
func (c *Client) Get(bucket bucket.Bucket) (Credential, error) {
|
||||
normalizedBucket := normalize(bucket.String())
|
||||
log.Printf("Getting credentials for %s using %s %s", normalizedBucket, c.EndpointURL(), c.Bucket)
|
||||
|
||||
o, err := c.GetObject(c.Bucket, normalizedBucket, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return Credential{}, err
|
||||
}
|
||||
defer o.Close()
|
||||
buf, err := ioutil.ReadAll(o)
|
||||
if err != nil {
|
||||
return Credential{}, err
|
||||
}
|
||||
|
||||
cred := Credential{}
|
||||
err = json.Unmarshal(buf, &cred)
|
||||
if err != nil {
|
||||
return Credential{}, err
|
||||
}
|
||||
return cred, nil
|
||||
}
|
||||
|
||||
func (c *Client) Set(bucket bucket.Bucket, cred Credential) error {
|
||||
normalizedBucket := normalize(bucket.String())
|
||||
log.Printf("Setting credentials for %s using %s %s", normalizedBucket, c.EndpointURL(), c.Bucket)
|
||||
|
||||
buf, err := json.MarshalIndent(cred, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.PutObject(c.Bucket, normalizedBucket, bytes.NewBuffer(buf), int64(len(buf)), minio.PutObjectOptions{
|
||||
ContentType: "application/json",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalize(bucket string) string {
|
||||
nodouble := strings.Replace(bucket, "//", "", 1)
|
||||
nolastslash := strings.TrimSuffix(nodouble, "/")
|
||||
return nolastslash
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package credentials
|
||||
|
||||
type Credential struct {
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Region string `json:"region"`
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package signer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.makerforce.io/photos/photos/pkg/credentials"
|
||||
"github.com/minio/minio-go/v6/pkg/signer"
|
||||
)
|
||||
|
||||
type Signer struct {
|
||||
expirations Expirations
|
||||
}
|
||||
|
||||
type Expirations struct {
|
||||
// Expiration time for list and read in time.Duration
|
||||
Read time.Duration
|
||||
// Expiration time for write in time.Duration
|
||||
Write time.Duration
|
||||
}
|
||||
|
||||
var ErrorExpirationTooLow = errors.New("expiration time too low")
|
||||
|
||||
func NewSigner(expirations Expirations) (*Signer, error) {
|
||||
if expirations.Read == 0 {
|
||||
expirations.Read = 30 * time.Minute
|
||||
}
|
||||
if expirations.Write == 0 {
|
||||
expirations.Write = 5 * time.Minute
|
||||
}
|
||||
if expirations.Read < time.Second || expirations.Write < time.Second {
|
||||
return nil, ErrorExpirationTooLow
|
||||
}
|
||||
return &Signer{
|
||||
expirations: expirations,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewSignerFromEnv() (*Signer, error) {
|
||||
expirationRead, _ := strconv.Atoi(os.Getenv("EXPIRATION_READ"))
|
||||
expirationWrite, _ := strconv.Atoi(os.Getenv("EXPIRATION_WRITE"))
|
||||
expirations := Expirations{Read: time.Duration(expirationRead), Write: time.Duration(expirationWrite)}
|
||||
return NewSigner(expirations)
|
||||
}
|
||||
|
||||
func (s *Signer) PreSignRead(req *http.Request, cred credentials.Credential) *http.Request {
|
||||
signedReq := signer.PreSignV4(
|
||||
*req,
|
||||
cred.AccessKey, cred.SecretKey, "",
|
||||
cred.Region,
|
||||
int64(s.expirations.Read/time.Second),
|
||||
)
|
||||
return signedReq
|
||||
}
|
||||
|
||||
func (s *Signer) PreSignWrite(req *http.Request, cred credentials.Credential) *http.Request {
|
||||
signedReq := signer.PreSignV4(
|
||||
*req,
|
||||
cred.AccessKey, cred.SecretKey, "",
|
||||
cred.Region,
|
||||
int64(s.expirations.Write/time.Second),
|
||||
)
|
||||
return signedReq
|
||||
}
|
||||
|
||||
func (s *Signer) Sign(req *http.Request, cred credentials.Credential) *http.Request {
|
||||
signedReq := signer.SignV4(
|
||||
*req,
|
||||
cred.AccessKey, cred.SecretKey, "",
|
||||
cred.Region,
|
||||
)
|
||||
return signedReq
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"access_key": "azCdtns6MMayxqEWu0UrdDZp",
|
||||
"secret_key": "W3sJ65RPgr3VzrOeH03nFGvUI5T8ZpESY5OUnvJz6c8Uv3SaIZOA22MboAf4NbEz",
|
||||
"region": "sgp1"
|
||||
}
|
|
@ -1,7 +1,12 @@
|
|||
<script>
|
||||
export let photo;
|
||||
|
||||
async function
|
||||
async function ar() {
|
||||
|
||||
}
|
||||
async function preview() {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="gallery-item preloaded-thumbnail" data-ar="{ar(photo)}">
|
||||
|
|
Loading…
Reference in New Issue