Skip to content
Snippets Groups Projects
Verified Commit ee0b3c6c authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

feat: merge frontend and backend into one project

parent 35d1ac71
No related branches found
No related tags found
No related merge requests found
Pipeline #2798 failed
Showing
with 242 additions and 26 deletions
/Makefile
/Dockerfile
/.gitlab-ci.yml
/.gitignore
build:
backend:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
entrypoint: [ "" ]
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} --destination $CI_REGISTRY_IMAGE:latest
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/backend/Dockerfile --destination $CI_REGISTRY_IMAGE:backend-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} --destination $CI_REGISTRY_IMAGE:backend
frontend:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [ "" ]
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/frontend/Dockerfile --destination $CI_REGISTRY_IMAGE:frontend-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} --destination $CI_REGISTRY_IMAGE:frontend
IMAGE := k8r.eu/justjanne/$(shell basename $(shell git remote get-url origin) .git)
TAGS := $(shell git describe --always --tags HEAD)
.PHONY: build
build:
docker build --pull -t $(IMAGE):$(TAGS) .
docker tag $(IMAGE):$(TAGS) $(IMAGE):latest
@echo Successfully tagged $(IMAGE):$(TAGS) as latest
.PHONY: push
push: build
docker push $(IMAGE):$(TAGS)
docker push $(IMAGE):latest
@echo Successfully pushed $(IMAGE):$(TAGS) as latest
.PHONY: tags
tags:
@echo "$(TAGS)"
FROM golang:1.17-alpine3.15 AS builder
RUN apk --no-cache add \
--virtual .build-deps \
alpine-sdk \
cmake \
sudo \
libssh2 libssh2-dev \
git \
dep \
bash \
curl \
imagemagick \
imagemagick-dev
WORKDIR /go/src/app
COPY go.* ./
RUN go mod download
COPY *.go ./
RUN go build -o app backend
FROM alpine:3.15
RUN apk --no-cache add imagemagick
RUN addgroup -g 1000 -S app && \
adduser -u 1000 -G app -S app
COPY --from=builder /go/src/app /
USER app
ENTRYPOINT ["/app"]
package main
func readAllErrors(amount int, errorChannel chan error) []error {
errors := make([]error, 0)
for i := 0; i < amount; i++ {
err := <-errorChannel
if err != nil {
errors = append(errors, err)
}
}
return errors
}
func runMany(amount int, function func(index int) error) []error {
errorChannel := make(chan error)
for i := 0; i < amount; i++ {
index := i
go func() { errorChannel <- function(index) }()
}
return readAllErrors(amount, errorChannel)
}
package main
import (
"context"
"git.kuschku.de/justjanne/imghost-frontend/shared"
"github.com/hibiken/asynq"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/sys/unix"
"gopkg.in/gographics/imagick.v3/imagick"
"log"
"net/http"
"os"
"os/signal"
)
var imageProcessDuration = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "imghost_process_duration",
Help: "The amount of time spent processing images",
}, []string{"task"})
var imageProcessDurationRead = imageProcessDuration.WithLabelValues("read")
var imageProcessDurationClone = imageProcessDuration.WithLabelValues("clone")
var imageProcessDurationCrop = imageProcessDuration.WithLabelValues("crop")
var imageProcessDurationResize = imageProcessDuration.WithLabelValues("resize")
var imageProcessDurationWrite = imageProcessDuration.WithLabelValues("write")
func main() {
configFile, err := os.Open("config.yaml")
if err != nil {
log.Fatalf("Could not open config file: %s", err.Error())
}
config := shared.LoadConfigFromFile(configFile)
imagick.Initialize()
defer imagick.Terminate()
srv := asynq.NewServer(
config.AsynqOpts(),
asynq.Config{Concurrency: config.Concurrency},
)
mux := asynq.NewServeMux()
mux.HandleFunc(shared.TypeImageResize, ProcessImageHandler(&config))
metricsMux := http.NewServeMux()
metricsMux.Handle("/metrics", promhttp.Handler())
metricsMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("OK"))
})
metrics := &http.Server{
Addr: ":2112",
Handler: metricsMux,
}
done := make(chan struct{})
go func() {
if err := metrics.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("Error: metrics server error: %s", err.Error())
}
close(done)
}()
if err := srv.Run(mux); err != nil {
log.Fatal(err)
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, unix.SIGTERM, unix.SIGINT)
<-sigs
srv.Shutdown()
if err := metrics.Shutdown(context.Background()); err != nil {
log.Printf("Error: metrics shutdown error: %s", err.Error())
}
<-done
}
package main
import (
"context"
"encoding/json"
"fmt"
"git.kuschku.de/justjanne/imghost-frontend/shared"
"github.com/hibiken/asynq"
"github.com/prometheus/client_golang/prometheus"
"os"
"path/filepath"
"strings"
"time"
)
func trackTimeSince(counter prometheus.Counter, start time.Time) time.Time {
now := time.Now().UTC()
counter.Add(float64(now.Sub(start).Milliseconds()) / 1000.0)
return now
}
func ProcessImageHandler(config *shared.Config) asynq.HandlerFunc {
return func(ctx context.Context, t *asynq.Task) error {
task := shared.ImageTaskPayload{}
if err := json.Unmarshal(t.Payload(), &task); err != nil {
return err
}
errors := ResizeImage(config, task.ImageId)
_ = os.Remove(filepath.Join(config.SourceFolder, task.ImageId))
errorMessages := make([]string, len(errors))
for i, err := range errors {
errorMessages[i] = err.Error()
}
if len(errors) != 0 {
return fmt.Errorf(
"errors occured while processing task %s (%s): %s",
t.Type(),
task.ImageId,
strings.Join(errorMessages, "\n"),
)
}
return nil
}
}
package main
import (
"fmt"
"git.kuschku.de/justjanne/imghost-frontend/shared"
"github.com/justjanne/imgconv"
"gopkg.in/gographics/imagick.v3/imagick"
"path/filepath"
"time"
)
func ResizeImage(config *shared.Config, imageId string) []error {
var err error
wand := imagick.NewMagickWand()
defer wand.Destroy()
startRead := time.Now().UTC()
if err = wand.ReadImage(filepath.Join(config.SourceFolder, imageId)); err != nil {
return []error{err}
}
var originalImage imgconv.ImageHandle
if originalImage, err = imgconv.NewImage(wand); err != nil {
return []error{err}
}
trackTimeSince(imageProcessDurationRead, startRead)
return runMany(len(config.Sizes), func(index int) error {
definition := config.Sizes[index]
path := filepath.Join(config.TargetFolder, fmt.Sprintf("%s%s", imageId, definition.Suffix))
startClone := time.Now().UTC()
image := originalImage.CloneImage()
startCrop := trackTimeSince(imageProcessDurationClone, startClone)
if err := image.Crop(definition.Size); err != nil {
return err
}
startResize := trackTimeSince(imageProcessDurationCrop, startCrop)
if err := image.Resize(definition.Size); err != nil {
return err
}
startWrite := trackTimeSince(imageProcessDurationResize, startResize)
if err := image.Write(config.Quality, path); err != nil {
return err
}
trackTimeSince(imageProcessDurationWrite, startWrite)
return nil
})
}
......@@ -6,7 +6,7 @@ WORKDIR /go/src/app
COPY go.* ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=false go build -o app .
RUN CGO_ENABLED=false go build -o app frontend
FROM node:alpine as asset_builder
RUN apk --no-cache add \
......@@ -20,9 +20,9 @@ RUN apk --no-cache add \
curl \
python3
WORKDIR /app
COPY package* /app/
COPY frontend/package* /app/
RUN npm ci
COPY assets /app/assets
COPY frontend/assets /app/assets
RUN npm run build
FROM alpine:3.15
......@@ -31,7 +31,7 @@ RUN apk --no-cache add imagemagick
RUN addgroup -g 1000 -S app && \
adduser -u 1000 -G app -S app
COPY --from=go_builder /go/src/app/app /
COPY templates /templates
COPY frontend/templates /templates
COPY --from=asset_builder /app/assets /assets
USER app
ENTRYPOINT ["/app"]
@font-face{font-family:'Lato';src:url("/assets/fonts/Lato-Regular.eot");src:url("/assets/fonts/Lato-Regular.eot?#iefix") format("embedded-opentype"),url("/assets/fonts/Lato-Regular.woff2") format("woff2"),url("/assets/fonts/Lato-Regular.woff") format("woff"),url("/assets/fonts/Lato-Regular.ttf") format("truetype");font-style:normal;font-weight:normal;text-rendering:optimizeLegibility}@font-face{font-family:'Lato';src:url("/assets/fonts/Lato-Bold.eot");src:url("/assets/fonts/Lato-Bold.eot?#iefix") format("embedded-opentype"),url("/assets/fonts/Lato-Bold.woff2") format("woff2"),url("/assets/fonts/Lato-Bold.woff") format("woff"),url("/assets/fonts/Lato-Bold.ttf") format("truetype");font-style:normal;font-weight:bold;text-rendering:optimizeLegibility}@font-face{font-family:'Lato';src:url("/assets/fonts/Lato-BoldItalic.eot");src:url("/assets/fonts/Lato-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/assets/fonts/Lato-BoldItalic.woff2") format("woff2"),url("/assets/fonts/Lato-BoldItalic.woff") format("woff"),url("/assets/fonts/Lato-BoldItalic.ttf") format("truetype");font-style:italic;font-weight:bold;text-rendering:optimizeLegibility}@font-face{font-family:'Lato';src:url("/assets/fonts/Lato-Italic.eot");src:url("/assets/fonts/Lato-Italic.eot?#iefix") format("embedded-opentype"),url("/assets/fonts/Lato-Italic.woff2") format("woff2"),url("/assets/fonts/Lato-Italic.woff") format("woff"),url("/assets/fonts/Lato-Italic.ttf") format("truetype");font-style:italic;font-weight:normal;text-rendering:optimizeLegibility}
*{margin:0;padding:0}*:focus{outline:none}*::-moz-focus-inner{border:0}html{height:100%}body{background:#282828;font-family:'Lato', sans-serif;font-size:81.25%;min-height:100%;display:flex;flex-direction:column}.container.centered{flex-grow:1;display:flex;margin:0 auto 64px auto;padding:0 16px;flex-direction:column;justify-content:center;width:100%;max-width:640px}.button{display:block;background:#FFC107;padding:4px 16px;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);color:#282828;text-decoration:none;line-height:24px;cursor:pointer;min-width:40px;text-align:center}.button:hover,.button:focus{background:#FFD54F;box-shadow:0 2px 4px rgba(0,0,0,0.2)}.button[aria-disabled=true],.button:disabled{cursor:default;background:#838383;box-shadow:0 -1px 2px rgba(0,0,0,0.1)}nav.navigation{position:sticky;top:0;background:#333333;box-shadow:0 2px 4px rgba(0,0,0,0.2);padding:0 16px;z-index:100}nav.navigation ul{display:flex;max-width:1024px;margin:0 auto;height:56px;align-items:center}nav.navigation ul li{display:block}@media (max-width: 640px){nav.navigation ul li.images,nav.navigation ul li.albums{display:none}}nav.navigation ul li.title a{display:inline-block;line-height:56px}nav.navigation ul li.title a img{width:32px;height:32px;vertical-align:middle}nav.navigation ul li.spacer{flex-grow:1}nav.navigation ul li:not(.spacer){margin:0 8px}nav.navigation ul li:not(.spacer):first-child{margin-left:0}nav.navigation ul li:not(.spacer):last-child{margin-right:0}.page.upload{flex-grow:1;display:flex;flex-direction:column}.page.upload .alert{padding:16px;margin:16px 0;box-shadow:0 2px 4px rgba(33,33,33,0.2);text-decoration:none;border-radius:2px}.page.upload .alert.success{background:#DCEDC8;color:#689F38;border-color:#689F38}.page.upload .alert.success a{color:#33691E}.page.upload .alert.error{background:#FFEBEE;color:#F44336;border-color:#F44336}.page.upload .alert.error a{color:#D32F2F}.page.upload form.upload{padding:96px 0;box-shadow:0 2px 4px rgba(0,0,0,0.2);text-decoration:none;border-radius:2px;text-align:center;background:#333333}.page.upload form.upload .upload-label{font-size:18pt;color:#fff}.page.upload form.upload label{position:relative;display:inline-block;overflow:hidden}.page.upload form.upload label span.text{position:relative;display:inline-block;background:#FFC107;padding:4px 16px;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);line-height:24px;color:#282828;cursor:pointer;z-index:1}.page.upload form.upload label span.text:hover,.page.upload form.upload label span.text:focus{background:#FFD54F;box-shadow:0 2px 4px rgba(0,0,0,0.2)}.page.upload form.upload label input[type=file]{position:absolute;left:0;right:0;top:0;bottom:0;opacity:0}.page.upload .uploading-images{display:flex;flex-direction:row}.page.upload .uploading-images .images{display:flex;flex-direction:column;align-items:stretch;flex-grow:1}.page.upload .uploading-images .images .detail{margin-bottom:32px}.page.upload .uploading-images .images .detail .image{position:relative}.page.upload .uploading-images .images .detail .image .progress{position:absolute;top:-4px;left:0;right:0;height:4px;display:block;background-color:rgba(255,193,7,0.2);overflow:hidden;transition:opacity 400ms}.page.upload .uploading-images .images .detail .image .progress .indeterminate{background-color:rgba(255,193,7,0.8)}.page.upload .uploading-images .images .detail .image .progress .indeterminate::before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.page.upload .uploading-images .images .detail .image .progress .indeterminate::after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation-delay:1.125s}.page.upload .uploading-images .images .detail:not(.uploading) .progress{opacity:0}.page.upload.submitted .container.centered{display:none}.page.upload:not(.submitted) .uploading-images{display:none}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.page.image.detail,.page.upload .uploading-images{display:flex;flex-direction:row;align-items:start;align-self:center;padding:32px 32px 128px;justify-content:center}@media (max-width: 1024px){.page.image.detail,.page.upload .uploading-images{flex-direction:column;padding:32px 0;align-items:stretch;justify-content:start}}.page.image.detail .sidebar,.page.upload .uploading-images .sidebar{width:250px;max-width:640px;margin-left:32px}.page.image.detail .sidebar .url,.page.upload .uploading-images .sidebar .url{background:#333333;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,0.4);padding:8px;margin-bottom:8px}@media (max-width: 1024px){.page.image.detail .sidebar .url,.page.upload .uploading-images .sidebar .url{padding:16px;margin-bottom:32px}}.page.image.detail .sidebar .url p,.page.upload .uploading-images .sidebar .url p{color:#ffffff}.page.image.detail .sidebar .url div,.page.upload .uploading-images .sidebar .url div{display:flex;flex-direction:row}.page.image.detail .sidebar .url div input,.page.upload .uploading-images .sidebar .url div input{background:none;color:#fff;opacity:0.4;border:none;flex-shrink:1;display:block;flex-grow:1;width:0;text-overflow:ellipsis}.page.image.detail .sidebar .url div button.copy,.page.upload .uploading-images .sidebar .url div button.copy{display:block;background:#FFC107;padding:4px 16px;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);border:none;font-family:'Lato', sans-serif;font-size:10pt;line-height:24px;margin-left:8px;cursor:pointer}.page.image.detail .sidebar .url div button.copy:hover,.page.image.detail .sidebar .url div button.copy:focus,.page.upload .uploading-images .sidebar .url div button.copy:hover,.page.upload .uploading-images .sidebar .url div button.copy:focus{background:#FFD54F;box-shadow:0 2px 4px rgba(0,0,0,0.2)}.page.image.detail .sidebar .actions,.page.upload .uploading-images .sidebar .actions{background:#333333;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,0.4);display:flex;flex-direction:row;margin-bottom:8px}@media (max-width: 1024px){.page.image.detail .sidebar .actions,.page.upload .uploading-images .sidebar .actions{margin-bottom:32px}}.page.image.detail .sidebar .actions .delete-form,.page.image.detail .sidebar .actions .update-form,.page.upload .uploading-images .sidebar .actions .delete-form,.page.upload .uploading-images .sidebar .actions .update-form{display:flex;flex-grow:1;flex-basis:0}.page.image.detail .sidebar .actions .delete-form input[type=submit],.page.image.detail .sidebar .actions .update-form input[type=submit],.page.upload .uploading-images .sidebar .actions .delete-form input[type=submit],.page.upload .uploading-images .sidebar .actions .update-form input[type=submit]{display:block;background:#FFC107;padding:4px 16px;border-radius:2px;box-shadow:0 1px 2px rgba(0,0,0,0.1);border:none;font-family:'Lato', sans-serif;font-size:10pt;line-height:24px;margin:8px;flex-grow:1;cursor:pointer}@media (max-width: 1024px){.page.image.detail .sidebar .actions .delete-form input[type=submit],.page.image.detail .sidebar .actions .update-form input[type=submit],.page.upload .uploading-images .sidebar .actions .delete-form input[type=submit],.page.upload .uploading-images .sidebar .actions .update-form input[type=submit]{margin:16px}}.page.image.detail .sidebar .actions .delete-form input[type=submit]:hover,.page.image.detail .sidebar .actions .delete-form input[type=submit]:focus,.page.image.detail .sidebar .actions .update-form input[type=submit]:hover,.page.image.detail .sidebar .actions .update-form input[type=submit]:focus,.page.upload .uploading-images .sidebar .actions .delete-form input[type=submit]:hover,.page.upload .uploading-images .sidebar .actions .delete-form input[type=submit]:focus,.page.upload .uploading-images .sidebar .actions .update-form input[type=submit]:hover,.page.upload .uploading-images .sidebar .actions .update-form input[type=submit]:focus{background:#FFD54F;box-shadow:0 2px 4px rgba(0,0,0,0.2)}@media (max-width: 1024px){.page.image.detail .sidebar,.page.upload .uploading-images .sidebar{width:auto;margin-left:0;margin-top:32px}}.page.image.detail .detail,.page.upload .uploading-images .detail{max-width:640px;width:100%;background:#333333;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,0.4)}.page.image.detail .detail .title,.page.upload .uploading-images .detail .title{line-height:1.25;padding:16px;vertical-align:middle;font-size:14pt;font-weight:normal;color:#eee;background:none;border:none;width:100%;box-sizing:border-box}.page.image.detail .detail .title:not(.fake-input):empty,.page.upload .uploading-images .detail .title:not(.fake-input):empty{display:none}.page.image.detail .detail .title:not(.fake-input):empty+.image,.page.upload .uploading-images .detail .title:not(.fake-input):empty+.image{border-top-left-radius:4px;border-top-right-radius:4px}.page.image.detail .detail .image,.page.upload .uploading-images .detail .image{background:#000;display:flex;flex-direction:row;justify-content:center;align-items:start}.page.image.detail .detail .image img,.page.upload .uploading-images .detail .image img{max-width:100%}.page.image.detail .detail .description,.page.upload .uploading-images .detail .description{line-height:1.25;padding:16px;vertical-align:middle;font-size:11pt;font-weight:normal;color:#eee;background:none;border:none;width:100%;box-sizing:border-box;font-family:'Lato', sans-serif;resize:vertical;white-space:pre-line}.page.image.detail .detail .description:not(.fake-input):empty,.page.upload .uploading-images .detail .description:not(.fake-input):empty{display:none}.page.image.detail .fake-input[contenteditable]:empty:before,.page.upload .uploading-images .fake-input[contenteditable]:empty:before{opacity:0.4;content:attr(placeholder)}.page.image.list{max-width:1024px;margin:8px auto 48px;display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));grid-gap:8px;align-self:stretch;width:calc(100% - 16px)}@media (max-width: 600px){.page.image.list{grid-template-columns:repeat(auto-fill, minmax(160px, 1fr))}}.page.image.list .image{padding:8px;position:relative;box-shadow:0 2px 4px rgba(0,0,0,0.2);transition:all 200ms;text-decoration:none;background:#333333;border-radius:2px;will-change:transform}.page.image.list .image:hover,.page.image.list .image:focus{transform:translate(0, -2px);box-shadow:0 4px 6px rgba(0,0,0,0.4)}.page.image.list .image .image-container{display:flex;justify-content:stretch;align-content:stretch;justify-items:stretch;align-items:stretch;flex-direction:column;background:#000000}.page.image.list .image .image-container img{aspect-ratio:1;object-fit:contain}.page.image.list .image .info{display:block;z-index:1;color:#eeeeee;line-height:1.25;font-size:10pt;padding-top:12px}.page.image.list .image .info p{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.page.image.list .image .info p.title{font-weight:600}.page.image.list .image .info p.title span.placeholder{opacity:0.4}ul.pagination{display:flex;flex-direction:row;background:#333;box-shadow:0 2px 4px rgba(0,0,0,0.2);padding:4px 8px;color:#fff;justify-self:center;grid-column-start:1;grid-column-end:-1;max-width:480px;width:calc(100% - 16px)}ul.pagination li.page{appearance:none;list-style:none;margin:4px;padding:0;line-height:24px}ul.pagination li.page.current{display:block;padding:4px 16px;flex-grow:1;text-align:center}
File moved
File moved
File moved
File moved
File moved
File moved
File moved
File moved
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment