Compare commits
4 Commits
4677480e8b
...
32d5b985b4
Author | SHA1 | Date |
---|---|---|
Ambrose Chua | 32d5b985b4 | |
Ambrose Chua | aeddf1c9d6 | |
Ambrose Chua | ff4646598f | |
Ambrose Chua | 0e54187b15 |
|
@ -0,0 +1,11 @@
|
|||
cmd/admin/admin
|
||||
cmd/control/control
|
||||
cmd/web/web
|
||||
cmd/preview/preview
|
||||
cmd/proxy/proxy
|
||||
# dist is required for Dockerfile.local
|
||||
env
|
||||
tilt_modules
|
||||
|
||||
build
|
||||
deployments
|
|
@ -3,5 +3,6 @@ cmd/control/control
|
|||
cmd/web/web
|
||||
cmd/preview/preview
|
||||
cmd/proxy/proxy
|
||||
|
||||
dist
|
||||
env
|
||||
tilt_modules
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
load('ext://restart_process', 'docker_build_with_restart')
|
||||
|
||||
local_resource(
|
||||
'control-go-compile',
|
||||
'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/control ./cmd/control',
|
||||
deps=['cmd/control', 'pkg', 'internal'],
|
||||
allow_parallel=True,
|
||||
)
|
||||
docker_build_with_restart(
|
||||
'registry.makerforce.io/photos/control',
|
||||
'.',
|
||||
entrypoint=['/control'],
|
||||
dockerfile='build/control/Dockerfile.local',
|
||||
only=['dist/control'],
|
||||
live_update=[
|
||||
sync('dist/control', '/control'),
|
||||
],
|
||||
)
|
||||
|
||||
docker_build_with_restart(
|
||||
'registry.makerforce.io/photos/preview',
|
||||
'.',
|
||||
entrypoint=['/preview'],
|
||||
dockerfile='build/preview/Dockerfile',
|
||||
target='build',
|
||||
only=['cmd/preview', 'pkg', 'internal', 'go.mod', 'go.sum'],
|
||||
live_update=[
|
||||
sync('cmd/preview', '/src/cmd/preview'),
|
||||
sync('pkg', '/src/pkg'),
|
||||
sync('internal', '/src/internal'),
|
||||
run('go build -o /preview'),
|
||||
],
|
||||
)
|
||||
|
||||
local_resource(
|
||||
'proxy-go-compile',
|
||||
'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/proxy ./cmd/proxy',
|
||||
deps=['cmd/proxy'],
|
||||
allow_parallel=True,
|
||||
)
|
||||
docker_build_with_restart(
|
||||
'registry.makerforce.io/photos/proxy',
|
||||
'.',
|
||||
entrypoint=['/proxy'],
|
||||
dockerfile='build/proxy/Dockerfile.local',
|
||||
only=['dist/proxy'],
|
||||
live_update=[
|
||||
sync('dist/proxy', '/proxy'),
|
||||
],
|
||||
)
|
||||
|
||||
local_resource(
|
||||
'web-go-compile',
|
||||
'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/web ./cmd/web',
|
||||
deps=['cmd/web', 'pkg', 'internal', 'web'],
|
||||
allow_parallel=True,
|
||||
)
|
||||
docker_build_with_restart(
|
||||
'registry.makerforce.io/photos/web',
|
||||
'.',
|
||||
entrypoint=['/web'],
|
||||
dockerfile='build/web/Dockerfile.local',
|
||||
only=['dist/web'],
|
||||
live_update=[
|
||||
sync('dist/web', '/web'),
|
||||
],
|
||||
)
|
||||
|
||||
k8s_yaml('deployments/secrets.yml')
|
||||
k8s_yaml('deployments/minio-basic.yml')
|
||||
k8s_resource(
|
||||
'minio',
|
||||
port_forwards=[
|
||||
'9000:9000',
|
||||
'9001:9001',
|
||||
],
|
||||
objects=['minio:secret', 'minio:service'],
|
||||
)
|
||||
|
||||
k8s_yaml('deployments/services.yml')
|
||||
k8s_resource(
|
||||
'proxy',
|
||||
port_forwards=[
|
||||
'8101',
|
||||
],
|
||||
)
|
||||
k8s_resource(
|
||||
'control',
|
||||
port_forwards=[
|
||||
'8100',
|
||||
],
|
||||
)
|
||||
k8s_resource(
|
||||
'preview',
|
||||
port_forwards=[
|
||||
'8103',
|
||||
],
|
||||
)
|
||||
k8s_resource(
|
||||
'web',
|
||||
port_forwards=[
|
||||
'8104',
|
||||
],
|
||||
)
|
|
@ -1,17 +1,15 @@
|
|||
FROM golang:alpine AS build
|
||||
|
||||
RUN mkdir /src /dist
|
||||
FROM golang:1.16-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
COPY . ./
|
||||
|
||||
WORKDIR /src/cmd/control/
|
||||
ENV CGO_ENABLED=0
|
||||
RUN go build -o /dist/control
|
||||
RUN go build -o /control
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /dist/control /control
|
||||
COPY --from=build /control /control
|
||||
|
||||
ENTRYPOINT ["/control"]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
FROM alpine:latest
|
||||
COPY ./dist/control /control
|
||||
ENTRYPOINT ["/control"]
|
|
@ -1,23 +1,21 @@
|
|||
FROM golang:buster AS build
|
||||
FROM golang:1.16-bullseye AS build
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y libvips-dev
|
||||
|
||||
RUN mkdir /src /dist
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libvips-dev
|
||||
|
||||
WORKDIR /src
|
||||
COPY . ./
|
||||
|
||||
WORKDIR /src/cmd/preview/
|
||||
RUN go build -o /dist/preview
|
||||
RUN go build -o /preview
|
||||
|
||||
|
||||
FROM debian:buster
|
||||
FROM debian:bullseye
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libvips42 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /dist/preview /preview
|
||||
COPY --from=build /preview /preview
|
||||
|
||||
ENTRYPOINT ["/preview"]
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
FROM golang:alpine AS build
|
||||
|
||||
RUN mkdir /src /dist
|
||||
FROM golang:1.16-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
COPY . ./
|
||||
|
||||
WORKDIR /src/cmd/proxy/
|
||||
ENV CGO_ENABLED=0
|
||||
RUN go build -o /dist/proxy
|
||||
RUN go build -o /proxy
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /dist/proxy /proxy
|
||||
COPY --from=build /proxy /proxy
|
||||
|
||||
ENTRYPOINT ["/proxy"]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
FROM alpine:latest
|
||||
COPY ./dist/proxy /proxy
|
||||
ENTRYPOINT ["/proxy"]
|
|
@ -1,17 +1,15 @@
|
|||
FROM golang:buster AS build
|
||||
|
||||
RUN mkdir /src /dist
|
||||
FROM golang:1.16-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
COPY . ./
|
||||
|
||||
WORKDIR /src/cmd/web/
|
||||
ENV CGO_ENABLED=0
|
||||
RUN go build -o /dist/web
|
||||
RUN go build -o /web
|
||||
|
||||
|
||||
FROM debian:buster
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /dist/web /web
|
||||
COPY --from=build /web /web
|
||||
|
||||
ENTRYPOINT ["/web"]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
FROM alpine:latest
|
||||
COPY ./dist/web /web
|
||||
ENTRYPOINT ["/web"]
|
|
@ -27,6 +27,17 @@ func main() {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 4,
|
||||
MaxIdleConnsPerHost: 4,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
DisableCompression: true,
|
||||
}
|
||||
http.DefaultClient = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":8000",
|
||||
ReadTimeout: 5 * time.Second,
|
||||
|
@ -44,7 +55,7 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||
token := req.FormValue("token")
|
||||
request := SafePathable(req.FormValue("request"))
|
||||
resource := SafePathable(req.FormValue("resource"))
|
||||
|
||||
err = bucket.Validate()
|
||||
if err != nil {
|
||||
|
@ -52,7 +63,7 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
|||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
err = request.Validate()
|
||||
err = resource.Validate()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
|
@ -70,7 +81,7 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
var signedReq *http.Request
|
||||
unsignedReq, err := http.NewRequest(req.Method, bucket.URL(request).String(), nil)
|
||||
unsignedReq, err := http.NewRequest(req.Method, bucket.URL(resource).String(), nil)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: error creating request: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
|
@ -80,13 +91,14 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
|||
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)
|
||||
signedReq = sig.PreSignWrite(unsignedReq, cred)
|
||||
} else {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Add("Location", signedReq.URL.String())
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ func (s SafePathable) Validate() error {
|
|||
if strings.HasPrefix(string(s), "internal/") {
|
||||
return ErrorInvalidRequest
|
||||
}
|
||||
return nil
|
||||
_, err := url.Parse(string(s))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s SafePathable) Path() *url.URL {
|
||||
return &url.URL{
|
||||
Path: string(s),
|
||||
}
|
||||
u, _ := url.Parse(string(s))
|
||||
return u
|
||||
}
|
||||
|
|
|
@ -38,10 +38,6 @@ func main() {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
// Setup vips
|
||||
vips.Startup(nil)
|
||||
defer vips.Shutdown()
|
||||
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 4,
|
||||
MaxIdleConnsPerHost: 4,
|
||||
|
@ -53,6 +49,10 @@ func main() {
|
|||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// Setup vips
|
||||
vips.Startup(nil)
|
||||
defer vips.Shutdown()
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":8003",
|
||||
ReadTimeout: 5 * time.Second,
|
||||
|
|
|
@ -45,6 +45,7 @@ func putPhotoMetadataSize(bucket lib.Bucket, cred credentials.Credential, photo
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unsignedReq.Header.Set("Content-Type", "application/json")
|
||||
signedReq := sig.Sign(unsignedReq, cred)
|
||||
|
||||
resp, err := http.DefaultClient.Do(signedReq)
|
||||
|
@ -61,6 +62,7 @@ func putPreview(bucket lib.Bucket, cred credentials.Credential, preview lib.Prev
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unsignedReq.Header.Set("Content-Type", string(preview.Format()))
|
||||
signedReq := sig.Sign(unsignedReq, cred)
|
||||
|
||||
resp, err := http.DefaultClient.Do(signedReq)
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
# Creating embedded data
|
||||
|
||||
```bash
|
||||
go get -u github.com/go-bindata/go-bindata/v3/...
|
||||
go generate
|
||||
```
|
||||
|
||||
<!-- vim: set conceallevel=2 et ts=2 sw=2: -->
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,147 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"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 getPhotos(bucket lib.Bucket, cred credentials.Credential) ([]lib.Photo, error) {
|
||||
unsignedReq, err := http.NewRequest(http.MethodGet, bucket.ListURL(10000, "").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 lib.BucketListUnmarshal(buf)
|
||||
}
|
||||
|
||||
func getPhotoMetadataSize(bucket lib.Bucket, cred credentials.Credential, photo lib.Photo) (lib.PhotoMetadataSize, error) {
|
||||
buf, err := getPhotoMetadata(bucket, cred, photo.MetadataSize())
|
||||
if err != nil {
|
||||
return lib.PhotoMetadataSize{}, err
|
||||
}
|
||||
return lib.PhotoMetadataSizeUnmarshal(buf)
|
||||
}
|
||||
|
||||
func getPhotoMetadataTitle(bucket lib.Bucket, cred credentials.Credential, photo lib.Photo) (lib.PhotoMetadataTitle, error) {
|
||||
buf, err := getPhotoMetadata(bucket, cred, photo.MetadataTitle())
|
||||
if errors.Is(err, httphelpers.ErrorNotFound) {
|
||||
return lib.PhotoMetadataTitle(""), nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return lib.PhotoMetadataTitle(buf), nil
|
||||
}
|
||||
|
||||
func getPhotoMetadataTags(bucket lib.Bucket, cred credentials.Credential, photo lib.Photo) (lib.PhotoMetadataTags, error) {
|
||||
buf, err := getPhotoMetadata(bucket, cred, photo.MetadataTags())
|
||||
if errors.Is(err, httphelpers.ErrorNotFound) {
|
||||
return lib.PhotoMetadataTags([]string{}), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lib.PhotoMetadataTagsUnmarshal(buf)
|
||||
}
|
||||
|
||||
func getPhotoMetadata(bucket lib.Bucket, cred credentials.Credential, photoMetadata lib.PhotoMetadata) ([]byte, error) {
|
||||
unsignedReq, err := http.NewRequest(http.MethodGet, bucket.URL(photoMetadata).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)
|
||||
}
|
||||
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func getBucketMetadataTitle(bucket lib.Bucket, cred credentials.Credential) (lib.BucketMetadataTitle, error) {
|
||||
buf, err := getBucketMetadata(bucket, cred, bucket.MetadataTitle())
|
||||
return lib.BucketMetadataTitle(buf), err
|
||||
}
|
||||
|
||||
func getBucketMetadata(bucket lib.Bucket, cred credentials.Credential, bucketMetadata lib.BucketMetadata) ([]byte, error) {
|
||||
unsignedReq, err := http.NewRequest(http.MethodGet, bucket.URL(bucketMetadata).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)
|
||||
}
|
||||
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func putBucketView(bucket lib.Bucket, cred credentials.Credential, buf []byte) error {
|
||||
unsignedReq, err := http.NewRequest(http.MethodPut, bucket.ViewURL().String(), bytes.NewBuffer(buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unsignedReq.Header.Set("Content-Type", "text/html")
|
||||
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)
|
||||
}
|
||||
|
||||
func putBucketManage(bucket lib.Bucket, cred credentials.Credential, buf []byte) error {
|
||||
unsignedReq, err := http.NewRequest(http.MethodPut, bucket.ManageURL().String(), bytes.NewBuffer(buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unsignedReq.Header.Set("Content-Type", "text/html")
|
||||
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)
|
||||
}
|
|
@ -5,31 +5,41 @@ import (
|
|||
"strings"
|
||||
|
||||
lib "git.makerforce.io/photos/photos/pkg/bucket"
|
||||
"git.makerforce.io/photos/photos/web"
|
||||
)
|
||||
|
||||
var funcs = template.FuncMap{
|
||||
"ar": func(p Photo) float64 { return float64(p.Width) / float64(p.Height) },
|
||||
"mul": func(a, b float64) float64 { return a * b },
|
||||
"photo": photo,
|
||||
"preview": preview,
|
||||
"ar": func(p Photo) float64 { return float64(p.Size.Width) / float64(p.Size.Height) },
|
||||
"mul": func(a, b float64) float64 { return a * b },
|
||||
"bucketmetadatatitle": func(t lib.BucketMetadataTitle) string { return strings.TrimSpace(string(t)) },
|
||||
"photometadatatitle": func(t lib.PhotoMetadataTitle) string { return strings.TrimSpace(string(t)) },
|
||||
"photo": photo,
|
||||
"preview": preview,
|
||||
}
|
||||
|
||||
type IndexTemplateData struct {
|
||||
Assets string
|
||||
Bucket lib.Bucket
|
||||
Metadata lib.BucketMetadata
|
||||
Photos []Photo
|
||||
Assets string
|
||||
Control string
|
||||
|
||||
Bucket lib.Bucket
|
||||
Title lib.BucketMetadataTitle
|
||||
Photos []Photo
|
||||
|
||||
Development bool
|
||||
}
|
||||
|
||||
type Photo struct {
|
||||
lib.Photo
|
||||
lib.PhotoMetadata
|
||||
Size lib.PhotoMetadataSize
|
||||
Title lib.PhotoMetadataTitle
|
||||
Tags lib.PhotoMetadataTags
|
||||
}
|
||||
|
||||
var indexTemplate = mustTemplateAsset("index")
|
||||
var viewTemplate = mustTemplateAsset("view/index.tmpl")
|
||||
var manageTemplate = mustTemplateAsset("manage/index.tmpl")
|
||||
|
||||
func mustTemplateAsset(name string) *template.Template {
|
||||
buf, err := Asset("view/" + name + ".tmpl")
|
||||
buf, err := web.FS.ReadFile(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -39,7 +49,7 @@ func mustTemplateAsset(name string) *template.Template {
|
|||
}
|
||||
|
||||
func photo(p Photo) template.URL {
|
||||
return template.URL(p.Path())
|
||||
return template.URL(p.Path().String())
|
||||
}
|
||||
|
||||
func preview(p Photo, height int, format lib.PhotoFormat, quality int) template.Srcset {
|
||||
|
@ -48,6 +58,6 @@ func preview(p Photo, height int, format lib.PhotoFormat, quality int) template.
|
|||
Format: format,
|
||||
Quality: quality,
|
||||
})
|
||||
path := strings.ReplaceAll(preview.Path(), ",", "%2C")
|
||||
path := strings.ReplaceAll(preview.Path().String(), ",", "%2C")
|
||||
return template.Srcset(path)
|
||||
}
|
||||
|
|
125
cmd/web/web.go
125
cmd/web/web.go
|
@ -1,7 +1,5 @@
|
|||
package main
|
||||
|
||||
//go:generate go-bindata -fs -prefix "../../web/" "../../web/..."
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
@ -11,25 +9,48 @@ import (
|
|||
|
||||
"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"
|
||||
"git.makerforce.io/photos/photos/web"
|
||||
)
|
||||
|
||||
var client *lib.Client
|
||||
var creds *credentials.Client
|
||||
var sig *signer.Signer
|
||||
|
||||
var sharedFileServer http.Handler
|
||||
|
||||
var endpoint string
|
||||
var controlEndpoint string
|
||||
|
||||
func main() {
|
||||
// Read configuration
|
||||
endpoint = os.Getenv("WEB_ENDPOINT")
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
sharedFileServer = http.FileServer(AssetFile())
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 4,
|
||||
MaxIdleConnsPerHost: 4,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
DisableCompression: true,
|
||||
}
|
||||
http.DefaultClient = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// Read configuration
|
||||
endpoint = os.Getenv("WEB_ENDPOINT")
|
||||
controlEndpoint = os.Getenv("CONTROL_ENDPOINT")
|
||||
|
||||
sharedFileServer = http.FileServer(http.FS(web.FS))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":8004",
|
||||
|
@ -38,6 +59,7 @@ func main() {
|
|||
}
|
||||
http.HandleFunc("/shared/", shared)
|
||||
http.HandleFunc("/update", update)
|
||||
|
||||
err = server.ListenAndServe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -49,10 +71,11 @@ func shared(w http.ResponseWriter, req *http.Request) {
|
|||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
|
||||
sharedFileServer.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
func update(w http.ResponseWriter, req *http.Request) {
|
||||
var err error
|
||||
|
||||
if req.Method != http.MethodPost {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
|
@ -60,43 +83,91 @@ func update(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||
/*
|
||||
bucketMetadata, err := client.GetBucketMetadata(bucket)
|
||||
previewOptions := bucketMetadata.PreviewOptions
|
||||
*/
|
||||
|
||||
metadata, err := client.GetBucketMetadata(bucket)
|
||||
photos, err := client.GetPhotos(bucket)
|
||||
err = bucket.Validate()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var cred credentials.Credential
|
||||
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
|
||||
}
|
||||
|
||||
title, err := getBucketMetadataTitle(bucket, cred)
|
||||
if err != nil {
|
||||
/*
|
||||
err := fmt.Errorf("bucket title: %w", err)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
*/
|
||||
title = ""
|
||||
}
|
||||
|
||||
photos, err := getPhotos(bucket, cred)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("cannot list bucket: %w", err)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
detailedPhotos := make([]Photo, 0)
|
||||
for _, photo := range photos {
|
||||
photoMetadata, err := client.GetPhotoMetadata(bucket, photo)
|
||||
size, err := getPhotoMetadataSize(bucket, cred, photo)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: %v", err, photo)
|
||||
err := fmt.Errorf("photo size: %w: %v", err, photo)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
detailedPhotos = append(detailedPhotos, Photo{photo, photoMetadata})
|
||||
title, err := getPhotoMetadataTitle(bucket, cred, photo)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("photo title: %w: %v", err, photo)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
tags, err := getPhotoMetadataTags(bucket, cred, photo)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("photo tags: %w: %v", err, photo)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
detailedPhotos = append(detailedPhotos, Photo{photo, size, title, tags})
|
||||
}
|
||||
|
||||
data := IndexTemplateData{
|
||||
Assets: endpoint + "/shared/",
|
||||
Bucket: bucket,
|
||||
Metadata: metadata,
|
||||
Photos: detailedPhotos,
|
||||
Assets: endpoint + "/shared/",
|
||||
Control: controlEndpoint,
|
||||
Bucket: bucket,
|
||||
Title: title,
|
||||
Photos: detailedPhotos,
|
||||
}
|
||||
indexBuf := new(bytes.Buffer)
|
||||
err = indexTemplate.Execute(indexBuf, data)
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
err = viewTemplate.Execute(buf, data)
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = client.PutBucketWeb(bucket, indexBuf.Bytes())
|
||||
err = putBucketView(bucket, cred, buf.Bytes())
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
|
||||
err = manageTemplate.Execute(buf, data)
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = putBucketManage(bucket, cred, buf.Bytes())
|
||||
if err != nil {
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: minio
|
||||
labels:
|
||||
app: minio
|
||||
spec:
|
||||
containers:
|
||||
- name: minio
|
||||
image: minio/minio:RELEASE.2021-11-09T03-21-45Z
|
||||
command: ["minio", "server", "/data", "--console-address", ":9001"]
|
||||
env:
|
||||
- name: MINIO_ROOT_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: accesskey
|
||||
name: minio
|
||||
- name: MINIO_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: secretkey
|
||||
name: minio
|
||||
ports:
|
||||
- containerPort: 9000
|
||||
- containerPort: 9001
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: minio
|
||||
spec:
|
||||
selector:
|
||||
app: minio
|
||||
ports:
|
||||
- name: api
|
||||
protocol: TCP
|
||||
port: 9000
|
||||
- name: console
|
||||
protocol: TCP
|
||||
port: 9001
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: minio
|
||||
labels:
|
||||
app: minio
|
||||
data:
|
||||
accesskey: bWluaW9hZG1pbg==
|
||||
secretkey: bWluaW9hZG1pbg==
|
|
@ -1,18 +1,18 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: photos-proxy
|
||||
name: proxy
|
||||
labels:
|
||||
app: photos-proxy
|
||||
app: proxy
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: photos-proxy
|
||||
app: proxy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: photos-proxy
|
||||
app: proxy
|
||||
spec:
|
||||
containers:
|
||||
- name: proxy
|
||||
|
@ -20,7 +20,7 @@ spec:
|
|||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: MINIO_ENDPOINT
|
||||
value: api.ambrose.photos:9000
|
||||
value: minio:9000
|
||||
- name: MINIO_DOMAIN
|
||||
value: ""
|
||||
- name: MINIO_ENDPOINT_SECURE
|
||||
|
@ -29,35 +29,33 @@ spec:
|
|||
value: "false"
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: photos-proxy
|
||||
name: proxy
|
||||
spec:
|
||||
selector:
|
||||
app: photos-proxy
|
||||
app: proxy
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: photos-control
|
||||
name: control
|
||||
labels:
|
||||
app: photos-control
|
||||
app: control
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: photos-control
|
||||
app: control
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: photos-control
|
||||
app: control
|
||||
spec:
|
||||
containers:
|
||||
- name: control
|
||||
|
@ -65,7 +63,7 @@ spec:
|
|||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: MINIO_ENDPOINT
|
||||
value: api.ambrose.photos:9000
|
||||
value: minio:9000
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
@ -82,38 +80,40 @@ spec:
|
|||
value: "false"
|
||||
- name: MINIO_CREDENTIALS_BUCKET
|
||||
value: "credentials"
|
||||
- name: EXPIRATION_READ
|
||||
value: "5m"
|
||||
- name: EXPIRATION_WRITE
|
||||
value: "1m"
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: photos-control
|
||||
name: control
|
||||
spec:
|
||||
selector:
|
||||
app: photos-control
|
||||
app: control
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8000
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: photos-preview
|
||||
name: preview
|
||||
labels:
|
||||
app: photos-preview
|
||||
app: preview
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: photos-preview
|
||||
app: preview
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: photos-preview
|
||||
app: preview
|
||||
spec:
|
||||
containers:
|
||||
- name: preview
|
||||
|
@ -121,7 +121,7 @@ spec:
|
|||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: MINIO_ENDPOINT
|
||||
value: api.ambrose.photos:9000
|
||||
value: minio:9000
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
@ -138,38 +138,40 @@ spec:
|
|||
value: "false"
|
||||
- name: MINIO_CREDENTIALS_BUCKET
|
||||
value: "credentials"
|
||||
- name: EXPIRATION_READ
|
||||
value: "5m"
|
||||
- name: EXPIRATION_WRITE
|
||||
value: "1m"
|
||||
ports:
|
||||
- containerPort: 8003
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: photos-preview
|
||||
name: preview
|
||||
spec:
|
||||
selector:
|
||||
app: photos-preview
|
||||
app: preview
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8003
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: photos-web
|
||||
name: web
|
||||
labels:
|
||||
app: photos-web
|
||||
app: web
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: photos-web
|
||||
app: web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: photos-web
|
||||
app: web
|
||||
spec:
|
||||
containers:
|
||||
- name: web
|
||||
|
@ -177,7 +179,7 @@ spec:
|
|||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: MINIO_ENDPOINT
|
||||
value: api.ambrose.photos:9000
|
||||
value: minio:9000
|
||||
- name: MINIO_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
@ -195,18 +197,21 @@ spec:
|
|||
- name: MINIO_CREDENTIALS_BUCKET
|
||||
value: "credentials"
|
||||
- name: WEB_ENDPOINT
|
||||
value: "http://web.ambrose.photos"
|
||||
value: "http://localhost:8104"
|
||||
- name: EXPIRATION_READ
|
||||
value: "5m"
|
||||
- name: EXPIRATION_WRITE
|
||||
value: "1m"
|
||||
ports:
|
||||
- containerPort: 8004
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: photos-web
|
||||
name: web
|
||||
spec:
|
||||
selector:
|
||||
app: photos-web
|
||||
app: web
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
8
go.mod
8
go.mod
|
@ -1,16 +1,18 @@
|
|||
module git.makerforce.io/photos/photos
|
||||
|
||||
go 1.14
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/davidbyttow/govips v0.0.0-20200412130214-cbefdd8c639a
|
||||
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.1.2+incompatible // indirect
|
||||
github.com/go-bindata/go-bindata/v3 v3.1.3 // indirect
|
||||
github.com/go-ini/ini v1.57.0 // indirect
|
||||
github.com/kisielk/errcheck v1.4.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
|
||||
golang.org/x/tools v0.0.0-20200821200730-1e23e48ab93b // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
)
|
||||
|
|
19
go.sum
19
go.sum
|
@ -8,6 +8,8 @@ github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3C
|
|||
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/go-bindata/go-bindata v1.0.0 h1:DZ34txDXWn1DyWa+vQf7V9ANc2ILTtrEjtlsdJRF26M=
|
||||
github.com/go-bindata/go-bindata v1.0.0/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
|
||||
github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE=
|
||||
github.com/go-bindata/go-bindata v3.1.2+incompatible/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=
|
||||
|
@ -21,6 +23,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
|
|||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/errcheck v1.2.0 h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.4.0 h1:ueN6QYA+c7eDQo7ebpNdYR8mUJZThiGz9PEoJEMGPzA=
|
||||
github.com/kisielk/errcheck v1.4.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
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=
|
||||
|
@ -49,17 +53,22 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
|
@ -69,15 +78,20 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7
|
|||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -89,9 +103,14 @@ golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapK
|
|||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200530233709-52effbd89c51 h1:Wec8/IO8hAraBf0it7/dPQYOslIrgM938wZYNkLnOYc=
|
||||
golang.org/x/tools v0.0.0-20200530233709-52effbd89c51/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200821200730-1e23e48ab93b h1:ob2Rprc4uPVPGKaYKm9lrGewYQJRu7KtuzGTICCM1X4=
|
||||
golang.org/x/tools v0.0.0-20200821200730-1e23e48ab93b/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
var ErrorBadRequest = errors.New("bad request")
|
||||
var ErrorMethodNotAllowed = errors.New("method not allowed")
|
||||
var ErrorNotFound = errors.New("not found")
|
||||
var ErrorUnauthorized = errors.New("unauthorized")
|
||||
|
||||
func ErrorResponse(w http.ResponseWriter, err error) {
|
||||
if err == nil {
|
||||
|
@ -49,5 +50,14 @@ func ErrorFromStatus(code int) error {
|
|||
if code == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
if code == http.StatusNotFound {
|
||||
return ErrorNotFound
|
||||
}
|
||||
if code == http.StatusBadRequest {
|
||||
return ErrorBadRequest
|
||||
}
|
||||
if code == http.StatusUnauthorized {
|
||||
return ErrorUnauthorized
|
||||
}
|
||||
return fmt.Errorf("%s", http.StatusText(code))
|
||||
}
|
||||
|
|
|
@ -40,6 +40,11 @@ func (b Bucket) URL(p Pathable) *url.URL {
|
|||
return u
|
||||
}
|
||||
|
||||
func (b Bucket) ListURL(maxKeys int, startAfter string) *url.URL {
|
||||
list := newList(photoPrefix, maxKeys, startAfter)
|
||||
return b.URL(list)
|
||||
}
|
||||
|
||||
func pathJoin(paths ...string) string {
|
||||
cleanedPaths := make([]string, 0, len(paths))
|
||||
for i, path := range paths {
|
||||
|
@ -84,3 +89,18 @@ func (b Bucket) MetadataDescription() BucketMetadata {
|
|||
func (b Bucket) MetadataOrdering() BucketMetadata {
|
||||
return b.Metadata("ordering")
|
||||
}
|
||||
|
||||
type index string
|
||||
|
||||
func (i index) Path() *url.URL {
|
||||
return &url.URL{
|
||||
Path: "/" + string(i) + "index.html",
|
||||
}
|
||||
}
|
||||
|
||||
func (b Bucket) ViewURL() *url.URL {
|
||||
return b.URL(index(""))
|
||||
}
|
||||
func (b Bucket) ManageURL() *url.URL {
|
||||
return b.URL(index("manage/"))
|
||||
}
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
package bucket
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/minio/minio-go/v6"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
)
|
||||
|
||||
type List struct {
|
||||
type list struct {
|
||||
prefix string
|
||||
maxKeys int
|
||||
startAfter string
|
||||
}
|
||||
|
||||
func NewList(prefix string, maxKeys int, startAfter string) List {
|
||||
return List{
|
||||
func newList(prefix string, maxKeys int, startAfter string) list {
|
||||
return list{
|
||||
prefix: prefix,
|
||||
maxKeys: maxKeys,
|
||||
startAfter: startAfter,
|
||||
}
|
||||
}
|
||||
|
||||
func (l List) Path() *url.URL {
|
||||
func (l list) Path() *url.URL {
|
||||
params := make(url.Values)
|
||||
params.Set("list-type", "2")
|
||||
params.Set("metadata", "true")
|
||||
|
@ -35,3 +37,16 @@ func (l List) Path() *url.URL {
|
|||
RawQuery: s3utils.QueryEncode(params),
|
||||
}
|
||||
}
|
||||
|
||||
func BucketListUnmarshal(buf []byte) ([]Photo, error) {
|
||||
list := minio.ListBucketV2Result{}
|
||||
err := xml.Unmarshal(buf, &list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
photos := make([]Photo, 0, len(list.Contents))
|
||||
for _, o := range list.Contents {
|
||||
photos = append(photos, Photo(o.Key))
|
||||
}
|
||||
return photos, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package bucket
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListUnmarshal(t *testing.T) {
|
||||
xml := []byte(`
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Name>screenshot</Name><Prefix></Prefix><Marker></Marker><MaxKeys>10000</MaxKeys><Delimiter></Delimiter><IsTruncated>false</IsTruncated><Contents><Key>Screenshot_2020-08-02 [Filebeat Cisco] ASA Firewall - Elastic.png</Key><LastModified>2020-08-22T13:44:33.034Z</LastModified><ETag>"94aef50ebfceadd0cefee92b14b7e73a"</ETag><Size>190116</Size><Owner><ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID><DisplayName></DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><Key>grim.png</Key><LastModified>2020-08-20T06:37:30.595Z</LastModified><ETag>"7078589d69baa9434ab28729449ed7a9"</ETag><Size>5166601</Size><Owner><ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID><DisplayName></DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents></ListBucketResult>
|
||||
`)
|
||||
l, err := ListUnmarshal(xml)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if string(l[0]) != "Screenshot_2020-08-02 [Filebeat Cisco] ASA Firewall - Elastic.png" {
|
||||
t.Errorf("Wrong filename")
|
||||
}
|
||||
if string(l[1]) != "grim.png" {
|
||||
t.Errorf("Wrong filename")
|
||||
}
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
package bucket
|
||||
|
||||
type BucketMetadataTitle string
|
||||
|
|
|
@ -2,6 +2,7 @@ package bucket
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PhotoMetadataSize struct {
|
||||
|
@ -14,7 +15,15 @@ func PhotoMetadataSizeUnmarshal(buf []byte) (PhotoMetadataSize, error) {
|
|||
err := json.Unmarshal(buf, &s)
|
||||
return s, err
|
||||
}
|
||||
|
||||
func PhotoMetadataSizeMarshal(s PhotoMetadataSize) ([]byte, error) {
|
||||
return json.MarshalIndent(s, "", "\t")
|
||||
}
|
||||
|
||||
type PhotoMetadataTitle string
|
||||
|
||||
type PhotoMetadataTags []string
|
||||
|
||||
func PhotoMetadataTagsUnmarshal(buf []byte) (PhotoMetadataTags, error) {
|
||||
tags := string(buf)
|
||||
return PhotoMetadataTags(strings.Split(tags, ",")), nil
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.makerforce.io/photos/photos/pkg/credentials"
|
||||
|
@ -40,9 +39,15 @@ func NewSigner(expirations Expirations) (*Signer, error) {
|
|||
}
|
||||
|
||||
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)}
|
||||
expirationRead, err := time.ParseDuration(os.Getenv("EXPIRATION_READ"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
expirationWrite, err := time.ParseDuration(os.Getenv("EXPIRATION_WRITE"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
expirations := Expirations{Read: expirationRead, Write: expirationWrite}
|
||||
return NewSigner(expirations)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package web
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed manage shared view
|
||||
var FS embed.FS
|
|
@ -42,7 +42,8 @@ container: .containertest .container
|
|||
touch $@
|
||||
|
||||
|
||||
DEV_BUCKET ?= http://api.ambrose.photos:9000/test.ambrose.photos
|
||||
DEV_BUCKET ?= https://bad-boy-bucket.makerforce.io/test1/
|
||||
CONTROL_ENDPOINT ?= http://localhost:8000
|
||||
.PHONY: dev
|
||||
# Start a development server
|
||||
dev: $(SOURCE)/* $(OUTPUT)/index.html $(shared_copy_files) | container
|
||||
|
@ -52,7 +53,7 @@ dev: $(SOURCE)/* $(OUTPUT)/index.html $(shared_copy_files) | container
|
|||
|
||||
$(OUTPUT)/index.html: index.tmpl indextmpl.go
|
||||
mkdir -p $(@D)
|
||||
$(GO) run indextmpl.go -t $< -o $@ -b $(DEV_BUCKET) -a $(subst $(OUTPUT)/,,$(SHARED_COPY))/ -mt "[dev] Manage"
|
||||
$(GO) run indextmpl.go -t $< -o $@ -a $(subst $(OUTPUT)/,,$(SHARED_COPY))/ -c $(CONTROL_ENDPOINT) -b $(DEV_BUCKET) -title "[dev] Manage"
|
||||
|
||||
.SECONDEXPANSION:
|
||||
$(shared_copy_files): $$(subst $$(SHARED_COPY)/,$$(SHARED)/,$$@)
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ .Metadata.Title }}</title>
|
||||
<title>{{ .Title }}</title>
|
||||
<link rel="stylesheet" href="{{ .Assets }}css/base.css">
|
||||
<link rel="stylesheet" href="{{if .Development}}{{else}}{{ .Assets }}css/{{end}}manage.css">
|
||||
<!-- Svelte! -->
|
||||
<script src="{{if .Development}}{{else}}{{ .Assets }}js/{{end}}manage.js" defer async></script>
|
||||
</head>
|
||||
<body data-bucket="{{ .Bucket }}">
|
||||
<body data-bucket="{{ .Bucket }}" data-control="{{ .Control }}">
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -10,13 +10,12 @@ import (
|
|||
|
||||
type IndexData struct {
|
||||
Assets string
|
||||
Bucket string
|
||||
Development bool
|
||||
Metadata Metadata
|
||||
}
|
||||
Control string
|
||||
|
||||
type Metadata struct {
|
||||
Bucket string
|
||||
Title string
|
||||
|
||||
Development bool
|
||||
}
|
||||
|
||||
var data = IndexData{
|
||||
|
@ -28,9 +27,10 @@ var out string
|
|||
func main() {
|
||||
flag.StringVar(&tmpl, "t", "", "template file")
|
||||
flag.StringVar(&out, "o", "", "output file")
|
||||
flag.StringVar(&data.Bucket, "b", "", ".Bucket")
|
||||
flag.StringVar(&data.Assets, "a", "", ".Assets")
|
||||
flag.StringVar(&data.Metadata.Title, "mt", "", ".Metadata.Title")
|
||||
flag.StringVar(&data.Control, "c", "", ".Control")
|
||||
flag.StringVar(&data.Bucket, "b", "", ".Bucket")
|
||||
flag.StringVar(&data.Title, "title", "", ".Title")
|
||||
flag.Parse()
|
||||
|
||||
f, err := os.Open(tmpl)
|
||||
|
|
|
@ -1,24 +1,62 @@
|
|||
<script>
|
||||
export let bucket;
|
||||
|
||||
import { fetcher, request, listPhotos } from './http.js';
|
||||
import Gallery from './Gallery.svelte';
|
||||
import Uploader from './Uploader.svelte';
|
||||
|
||||
let photos = [
|
||||
"photo/Screenshot from 2020-04-23 19-27-53.png",
|
||||
"photo/Screenshot_2020-04-13 Looking Glass - Hurricane Electric (AS6939).png",
|
||||
];
|
||||
async function getMetadataTitle() {
|
||||
const resp = await fetcher(request('GET', 'metadata/title'));
|
||||
return resp.text();
|
||||
}
|
||||
let title = getMetadataTitle();
|
||||
|
||||
async function getPhotos() {
|
||||
const resp = await fetcher(request('GET', listPhotos(10000)));
|
||||
const text = await resp.text();
|
||||
|
||||
const xml = new window.DOMParser().parseFromString(text, "text/xml");
|
||||
const contents = xml.querySelectorAll('ListBucketResult > Contents');
|
||||
|
||||
const photos = [];
|
||||
contents.forEach(c => {
|
||||
photos.push(c.querySelector('Key').innerHTML);
|
||||
});
|
||||
return photos;
|
||||
}
|
||||
let photos = getPhotos();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#await title then title}
|
||||
<title>{title}</title>
|
||||
{:catch error}
|
||||
<title>Error</title>
|
||||
{/await}
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="..">Gallery</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<header>
|
||||
<h1>{bucket}</h1>
|
||||
{#await title}
|
||||
<h1 class="dim">Loading...</h1>
|
||||
{:then title}
|
||||
<h1>{title}</h1>
|
||||
{:catch error}
|
||||
<h1 class="dim" title="{error}">Error getting title</h1>
|
||||
{/await}
|
||||
</header>
|
||||
<Gallery {photos} />
|
||||
|
||||
<Uploader />
|
||||
|
||||
{#await photos then photos}
|
||||
<Gallery {photos} />
|
||||
{:catch error}
|
||||
<main>Unable to load photos: {error}</main>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<!-- vim: set ft=html: -->
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
<script>
|
||||
import { onMount, afterUpdate } from 'svelte';
|
||||
import Gallery from '../build/shared/js/gallery.js';
|
||||
import Photo from './Photo.svelte';
|
||||
|
||||
export let photos;
|
||||
|
||||
import Photo from './Photo.svelte';
|
||||
let galleryEle;
|
||||
let gallery;
|
||||
|
||||
onMount(() => {
|
||||
gallery = new Gallery(galleryEle);
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<main class="gallery" bind:this={galleryEle}>
|
||||
{#each photos as photo}
|
||||
<Photo {photo} />
|
||||
<Photo {photo} on:sizechange={gallery.recompute()} />
|
||||
{/each}
|
||||
</main>
|
||||
|
||||
|
|
|
@ -1,38 +1,97 @@
|
|||
<script>
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { fetcher, request } from './http.js';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let photo;
|
||||
$: raw = request('GET', photo).url;
|
||||
|
||||
async function ar() {
|
||||
let title = '';
|
||||
|
||||
}
|
||||
async function preview() {
|
||||
let size = { width: 1, height: 1 };
|
||||
$: ar = size.width / size.height;
|
||||
|
||||
}
|
||||
let tags = [];
|
||||
|
||||
async function getPhotoMetadataTitle() {
|
||||
const objectBase = photo.replace('photo/', 'photometadata/');
|
||||
const resp = await fetcher(request('GET', objectBase + '/title'));
|
||||
return resp.text();
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
title = await getPhotoMetadataTitle();
|
||||
} catch (e) {
|
||||
// We can ignore missing titles
|
||||
}
|
||||
});
|
||||
async function getPhotoMetadataSize() {
|
||||
const objectBase = photo.replace('photo/', 'photometadata/');
|
||||
const resp = await fetcher(request('GET', objectBase + '/size'));
|
||||
return resp.json();
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
size = await getPhotoMetadataSize();
|
||||
dispatch('sizechange');
|
||||
} catch (e) {
|
||||
// TODO: Emit error
|
||||
}
|
||||
});
|
||||
async function getPhotoMetadataTags() {
|
||||
const objectBase = photo.replace('photo/', 'photometadata/');
|
||||
const resp = await fetcher(request('GET', objectBase + '/tags'));
|
||||
const str = await resp.text();
|
||||
return str.split(",");
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
tags = await getPhotoMetadataTags();
|
||||
} catch (e) {
|
||||
// We can ignore missing tags
|
||||
}
|
||||
});
|
||||
|
||||
function preview(height, format, quality) {
|
||||
const objectBase = photo.replace('photo/', 'preview/');
|
||||
const extIndex = objectBase.lastIndexOf(".");
|
||||
const base = objectBase.substring(0, extIndex);
|
||||
const res = base + `_h${height}q${quality}`;
|
||||
const ext = extensionByType(format)
|
||||
return request('GET', res + ext).url;
|
||||
}
|
||||
|
||||
function extensionByType(format) {
|
||||
return {
|
||||
"image/webp": ".webp",
|
||||
"image/jpeg": ".jpg",
|
||||
}[format];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="gallery-item preloaded-thumbnail" data-ar="{ar(photo)}">
|
||||
<a href="{photo}" data-enlarge="{photo}">
|
||||
<div class="gallery-item preloaded-thumbnail" title="{title}" data-ar="{ar}">
|
||||
<picture style="display: none;">
|
||||
<source srcset="{preview(photo, 320, 'image/webp', 70)} 2x, {preview(photo, 160, 'image/webp', 70)} 1x" type="image/webp" media="(max-width: 900px)">
|
||||
<source srcset="{preview(photo, 640, 'image/webp', 70)} 2x, {preview(photo, 320, 'image/webp', 70)} 1x" type="image/webp">
|
||||
<source srcset="{preview(photo, 320, 'image/jpeg', 70)} 2x, {preview(photo, 160, 'image/jpeg', 70)} 1x" media="(max-width: 900px)">
|
||||
<source srcset="{preview(photo, 640, 'image/jpeg', 70)} 2x, {preview(photo, 320, 'image/jpeg', 70)} 1x">
|
||||
<img src="{preview(photo, 320, 'image/jpeg', 70)}"
|
||||
<source srcset="{preview(320, 'image/webp', 70)} 2x, {preview(160, 'image/webp', 70)} 1x" type="image/webp" media="(max-width: 900px)">
|
||||
<source srcset="{preview(640, 'image/webp', 70)} 2x, {preview(320, 'image/webp', 70)} 1x" type="image/webp">
|
||||
<source srcset="{preview(320, 'image/jpeg', 70)} 2x, {preview(160, 'image/jpeg', 70)} 1x" media="(max-width: 900px)">
|
||||
<source srcset="{preview(640, 'image/jpeg', 70)} 2x, {preview(320, 'image/jpeg', 70)} 1x">
|
||||
<img src="{preview(320, 'image/jpeg', 70)}"
|
||||
alt=""
|
||||
width="{ar(photo) * 320}"
|
||||
width="{ar * 320}"
|
||||
height="320"
|
||||
loading="lazy"
|
||||
data-enlarge-preload-for="{photo}">
|
||||
</picture>
|
||||
<picture class="preloaded-thumbnail-image">
|
||||
<source srcset="{preview(photo, 30, 'image/webp', 34)} 1x" type="image/webp" media="(max-width: 900px)">
|
||||
<source srcset="{preview(photo, 60, 'image/webp', 34)} 1x" type="image/webp">
|
||||
<source srcset="{preview(photo, 30, 'image/jpeg', 34)} 1x" media="(max-width: 900px)">
|
||||
<source srcset="{preview(photo, 60, 'image/jpeg', 34)} 1x">
|
||||
<img src="{preview(photo, 30, 'image/jpeg', 34)}"
|
||||
<source srcset="{preview(30, 'image/webp', 34)} 1x" type="image/webp" media="(max-width: 900px)">
|
||||
<source srcset="{preview(60, 'image/webp', 34)} 1x" type="image/webp">
|
||||
<source srcset="{preview(30, 'image/jpeg', 34)} 1x" media="(max-width: 900px)">
|
||||
<source srcset="{preview(60, 'image/jpeg', 34)} 1x">
|
||||
<img src="{preview(30, 'image/jpeg', 34)}"
|
||||
alt=""
|
||||
width="{ar(photo) * 320}"
|
||||
width="{ar * 320}"
|
||||
height="320">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
<!-- vim: set ft=html: -->
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
let isdragenter = false;
|
||||
|
||||
function dragenter(event) {
|
||||
isdragenter = true;
|
||||
}
|
||||
function dragleave(event) {
|
||||
isdragenter = false;
|
||||
}
|
||||
function dragover(event) {
|
||||
// Do nothing
|
||||
}
|
||||
function drop(event) {
|
||||
console.log(event.dataTransfer.items);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div on:dragenter={dragenter} on:dragleave={dragleave} on:dragover|preventDefault={dragover} on:drop|preventDefault={drop}>
|
||||
<div class="instructions" class:isdragenter>
|
||||
Drop photos here
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.instructions {
|
||||
margin: calc(var(--spacing) * 2);
|
||||
padding: 1rem;
|
||||
|
||||
border: 2px dashed #000;
|
||||
border-radius: 0.5rem;
|
||||
opacity: 0.333;
|
||||
|
||||
text-align: center;
|
||||
font-weight: 550;
|
||||
pointer-events: none;
|
||||
}
|
||||
.instructions.isdragenter {
|
||||
opacity: 0.666;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- vim: set ft=html: -->
|
|
@ -0,0 +1,54 @@
|
|||
import { control, bucket, token } from './stores.js';
|
||||
|
||||
let control_value;
|
||||
control.subscribe(value => {
|
||||
control_value = value;
|
||||
});
|
||||
let bucket_value;
|
||||
bucket.subscribe(value => {
|
||||
bucket_value = value;
|
||||
});
|
||||
let token_value;
|
||||
token.subscribe(value => {
|
||||
token_value = value;
|
||||
});
|
||||
|
||||
class RequestError extends Error {
|
||||
constructor(res) {
|
||||
super(`Request returned status ${res.status} ${res.statusText}`);
|
||||
this.res = res;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetcher(request) {
|
||||
const resp = await fetch(request);
|
||||
if (resp.status != 200) {
|
||||
throw new RequestError(resp);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
export function request(method, resource, body=null) {
|
||||
const sign = new URL(control_value + '/sign');
|
||||
sign.searchParams.append('bucket', bucket_value);
|
||||
sign.searchParams.append('token', token_value);
|
||||
sign.searchParams.append('resource', resource);
|
||||
const req = new Request(sign.href, {
|
||||
method,
|
||||
body,
|
||||
});
|
||||
return req;
|
||||
}
|
||||
|
||||
export function listPhotos(maxKeys=1000, startAfter='') {
|
||||
const prefix = "photo/";
|
||||
const params = new URLSearchParams();
|
||||
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", maxKeys);
|
||||
params.set("start-after", startAfter);
|
||||
return '?' + params.toString();
|
||||
}
|
|
@ -2,9 +2,6 @@ import App from './App.svelte';
|
|||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
props: {
|
||||
bucket: document.body.dataset.bucket,
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { readable, writable } from 'svelte/store';
|
||||
|
||||
export const control = readable(document.body.dataset.control);
|
||||
export const bucket = readable(document.body.dataset.bucket);
|
||||
|
||||
export const token = writable('todo');
|
|
@ -48,6 +48,10 @@ h4, h5, h6 {
|
|||
font-size: 1.272em;
|
||||
}
|
||||
|
||||
.dim {
|
||||
opacity: 0.333;
|
||||
}
|
||||
|
||||
/*
|
||||
* Molecules
|
||||
*/
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
main.svelte-1tky8bj{text-align:center;padding:1em;max-width:240px;margin:0 auto}h1.svelte-1tky8bj{color:#ff3e00;text-transform:uppercase;font-size:4em;font-weight:100}@media(min-width: 640px){main.svelte-1tky8bj{max-width:none}}
|
||||
|
||||
/*# sourceMappingURL=manage.css.map */
|
|
@ -1,48 +1,90 @@
|
|||
class LayoutEngine {
|
||||
/*
|
||||
rects = [];
|
||||
export default class Gallery {
|
||||
constructor(ele) {
|
||||
this.ele = ele;
|
||||
|
||||
gap = 12;
|
||||
baseHeight = 100;
|
||||
// maxHeight = 320;
|
||||
viewportWidth = 1024;
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
this.rects = [];
|
||||
this.gap = 12;
|
||||
this.baseHeight = 100;
|
||||
this.viewportWidth = 1024;
|
||||
|
||||
this.grabGap();
|
||||
this.grabBaseHeight();
|
||||
this.grabViewportWidth();
|
||||
|
||||
this.registerViewport();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
// Register a new rectangle with an aspect ratio of ar at index
|
||||
insert(ar=1, index=-1) {
|
||||
const rect = {
|
||||
ar: ar,
|
||||
};
|
||||
if (index == -1) {
|
||||
this.rects.push(rect);
|
||||
} else {
|
||||
this.rects.splice(index, 0, rect);
|
||||
// Extract gap values
|
||||
grabGap() {
|
||||
// Simple implementation to guess from padding values
|
||||
const px = window.getComputedStyle(this.ele).getPropertyValue('padding-left').replace('px', '');
|
||||
this.gap = parseInt(px) * 2 || 0;
|
||||
}
|
||||
|
||||
// Extract baseHeight
|
||||
grabBaseHeight() {
|
||||
const px = window.getComputedStyle(this.ele).getPropertyValue('--gallery-base-height').replace('px', '');
|
||||
this.baseHeight = parseInt(px) || 0;
|
||||
}
|
||||
|
||||
// Extract viewport width
|
||||
grabViewportWidth() {
|
||||
this.viewportWidth = this.ele.clientWidth;
|
||||
}
|
||||
|
||||
// Inform our engine of viewport width
|
||||
registerViewport() {
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.viewportWidth == window.innerWidth) {
|
||||
return;
|
||||
}
|
||||
this.recompute();
|
||||
});
|
||||
window.addEventListener('load', () => {
|
||||
this.recompute();
|
||||
});
|
||||
setInterval(this.recompute.bind(this), 500);
|
||||
}
|
||||
|
||||
recompute() {
|
||||
this.grabGap();
|
||||
this.grabBaseHeight();
|
||||
this.grabViewportWidth();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
|
||||
// Calculate dimensions and draw them
|
||||
draw() {
|
||||
const elements = this.ele.querySelectorAll('.gallery-item');
|
||||
const rects = Array.from(elements).map(element => parseFloat(element.dataset.ar));
|
||||
const dimensions = this.layout(rects);
|
||||
|
||||
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
||||
for (const [index, ele] of galleryItemEles.entries()) {
|
||||
const dimension = dimensions[index];
|
||||
if (!dimension) {
|
||||
console.error(`Missing dimensions for element at ${index}`);
|
||||
return;
|
||||
}
|
||||
for (const imgEle of ele.querySelectorAll('img')) {
|
||||
imgEle.style.height = `${Math.floor(dimension.h)}px`;
|
||||
imgEle.style.width = `${dimension.w}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister a new rectangle at index
|
||||
pop(index=-1) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Generate a list of dimensions for each rectangle
|
||||
calculate() {
|
||||
// Perform layout
|
||||
layout(rects) {
|
||||
let dimensions = [];
|
||||
|
||||
let currentWidth = this.gap;
|
||||
let startIndex = 0;
|
||||
|
||||
// Behave like a browser: try to fit as many in a row with baseHeight
|
||||
for (let index = 0; index < this.rects.length; index++) {
|
||||
for (let index = 0; index < rects.length; index++) {
|
||||
// Add this rectangle width
|
||||
const currentRectWidth = (this.baseHeight * this.rects[index].ar);
|
||||
const currentRectWidth = (this.baseHeight * rects[index]);
|
||||
currentWidth += currentRectWidth + this.gap;
|
||||
|
||||
if (currentWidth > this.viewportWidth) {
|
||||
|
@ -51,8 +93,8 @@ class LayoutEngine {
|
|||
}
|
||||
|
||||
// Get the next width to add
|
||||
const hasNextRect = index + 1 < this.rects.length;
|
||||
const nextRectWidth = hasNextRect ? (this.baseHeight * this.rects[index+1].ar) : 0;
|
||||
const hasNextRect = index + 1 < rects.length;
|
||||
const nextRectWidth = hasNextRect ? (this.baseHeight * rects[index+1]) : 0;
|
||||
const nextRectOverflow = currentWidth + nextRectWidth + this.gap > this.viewportWidth;
|
||||
|
||||
// If the next width is too wide, resolve the current rectangles
|
||||
|
@ -68,7 +110,7 @@ class LayoutEngine {
|
|||
for (; startIndex <= index; startIndex++) {
|
||||
dimensions.push({
|
||||
h: rectHeight,
|
||||
w: rectHeight * this.rects[startIndex].ar,
|
||||
w: rectHeight * rects[startIndex],
|
||||
})
|
||||
}
|
||||
currentWidth = this.gap;
|
||||
|
@ -78,104 +120,4 @@ class LayoutEngine {
|
|||
return dimensions;
|
||||
}
|
||||
|
||||
/*
|
||||
* Code for a future implementation that emits events
|
||||
|
||||
update() {
|
||||
console.debug('updating layout');
|
||||
|
||||
}
|
||||
|
||||
// Schedule an update at maximum 10Hz
|
||||
scheduleUpdate() {
|
||||
if (this.scheduledUpdate) {
|
||||
return;
|
||||
}
|
||||
this.scheduledUpdate = setTimeout(() => {
|
||||
this.scheduledUpdate = null;
|
||||
this.update();
|
||||
}, 100);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
export default class Gallery {
|
||||
constructor(ele) {
|
||||
this.ele = ele;
|
||||
this.layoutEngine = new LayoutEngine();
|
||||
|
||||
this.grabGap();
|
||||
this.grabBaseHeight();
|
||||
this.grabViewportWidth();
|
||||
|
||||
this.registerViewport();
|
||||
this.registerInitial();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
// Extract gap values
|
||||
grabGap() {
|
||||
// Simple implementation to guess from padding values
|
||||
const px = window.getComputedStyle(this.ele).getPropertyValue('padding-left').replace('px', '');
|
||||
this.layoutEngine.gap = parseInt(px) * 2 || 0;
|
||||
}
|
||||
|
||||
// Extract baseHeight
|
||||
grabBaseHeight() {
|
||||
const px = window.getComputedStyle(this.ele).getPropertyValue('--gallery-base-height').replace('px', '');
|
||||
this.layoutEngine.baseHeight = parseInt(px) || 0;
|
||||
}
|
||||
|
||||
// Extract viewport width
|
||||
grabViewportWidth() {
|
||||
this.layoutEngine.viewportWidth = this.ele.clientWidth;
|
||||
}
|
||||
|
||||
// Inform our engine of viewport width
|
||||
registerViewport() {
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.layoutEngine.viewportWidth == window.innerWidth) {
|
||||
return;
|
||||
}
|
||||
this.recompute();
|
||||
});
|
||||
window.addEventListener('load', () => {
|
||||
this.recompute();
|
||||
});
|
||||
setInterval(this.recompute.bind(this), 500);
|
||||
}
|
||||
|
||||
recompute() {
|
||||
this.grabGap();
|
||||
this.grabBaseHeight();
|
||||
this.grabViewportWidth();
|
||||
//this.layoutEngine.scheduleUpdate();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
// Register initial elements
|
||||
registerInitial() {
|
||||
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
||||
for (const ele of galleryItemEles) {
|
||||
const ar = ele.dataset.ar;
|
||||
this.layoutEngine.insert(ar);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate dimensions and draw them
|
||||
draw() {
|
||||
const dimensions = this.layoutEngine.calculate();
|
||||
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
||||
for (const [index, ele] of galleryItemEles.entries()) {
|
||||
const dimension = dimensions[index];
|
||||
if (!dimension) {
|
||||
console.error(`Missing dimensions for element at ${index}`);
|
||||
return;
|
||||
}
|
||||
for (const imgEle of ele.querySelectorAll('img')) {
|
||||
imgEle.style.height = `${Math.floor(dimension.h)}px`;
|
||||
imgEle.style.width = `${dimension.w}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ .Metadata.Title }}</title>
|
||||
<title>{{ bucketmetadatatitle .Title }}</title>
|
||||
<link rel="stylesheet" href="{{ .Assets }}css/base.css">
|
||||
<link rel="stylesheet" href="{{ .Assets }}css/gallery.css">
|
||||
<link rel="stylesheet" href="{{ .Assets }}css/enlarge.css">
|
||||
|
@ -18,11 +18,11 @@
|
|||
</ul>
|
||||
</nav>
|
||||
<header>
|
||||
<h1>{{ .Metadata.Title }}</h1>
|
||||
<h1>{{ bucketmetadatatitle .Title }}</h1>
|
||||
</header>
|
||||
<main class="gallery">
|
||||
{{ range .Photos }}
|
||||
<div class="gallery-item preloaded-thumbnail" data-ar="{{ ar . }}">
|
||||
<div class="gallery-item preloaded-thumbnail" data-ar="{{ ar . }}" title="{{ photometadatatitle .Title }}" data-tags="{{ .Tags }}">
|
||||
<a href="{{ photo . }}" data-enlarge="{{ photo . }}">
|
||||
<picture style="display: none;">
|
||||
<source srcset="{{ preview . 320 "image/webp" 70 }} 2x, {{ preview . 160 "image/webp" 70 }} 1x" type="image/webp" media="(max-width: 900px)">
|
||||
|
|
Loading…
Reference in New Issue