commit 19b72999666b82d6d75f54073c2d25d7c0fdf94e Author: matst80 Date: Thu May 15 19:28:34 2025 +0200 slask diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..9f64647 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,30 @@ +name: Build and Publish +run-name: ${{ gitea.actor }} is building 🚀 +on: [push] + +jobs: +# BuildAndDeployAmd64: +# runs-on: amd64 +# steps: +# - name: Check out repository code +# uses: actions/checkout@v4 +# - name: Build docker image +# run: docker build --progress=plain -t registry.knatofs.se/go-cart-actor-amd64:latest . +# - name: Push to registry +# run: docker push registry.knatofs.se/go-cart-actor-amd64:latest +# - name: Deploy to Kubernetes +# run: kubectl apply -f deployment/deployment.yaml -n cart +# - name: Rollout amd64 deployment +# run: kubectl rollout restart deployment/cart-actor-x86 -n cart + + BuildAndDeploy: + runs-on: arm64 + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Build docker image + run: docker build --progress=plain -t registry.knatofs.se/go-persist . + - name: Push to registry + run: docker push registry.knatofs.se/go-persist + - name: Rollout arm64 deployment + run: kubectl rollout restart deployment/persist-arm64 -n dev \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8494c84 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1 + +FROM golang:alpine AS build-stage +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY *.go ./ +COPY pkg ./pkg + +RUN CGO_ENABLED=0 GOOS=linux go build -o /go-persist + +FROM gcr.io/distroless/base-debian11 +WORKDIR / + +COPY --from=build-stage /go-persist /go-persist +ENTRYPOINT ["/go-persist"] \ No newline at end of file diff --git a/deployment/order-manager.yaml b/deployment/order-manager.yaml new file mode 100644 index 0000000..28c2a40 --- /dev/null +++ b/deployment/order-manager.yaml @@ -0,0 +1,124 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: persist + arch: arm64 + name: persist-arm64 +spec: + replicas: 1 + selector: + matchLabels: + app: persist + arch: arm64 + template: + metadata: + labels: + app: persist + arch: arm64 + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: NotIn + values: + - masterpi + - key: kubernetes.io/arch + operator: In + values: + - arm64 + volumes: + - name: data + nfs: + path: /i-data/7a8af061/nfs/persist + server: 10.10.1.10 + imagePullSecrets: + - name: regcred + containers: + - image: registry.knatofs.se/go-persist:latest + name: persist-arm64 + imagePullPolicy: Always + lifecycle: + preStop: + exec: + command: ["sleep", "15"] + ports: + - containerPort: 8080 + name: web + volumeMounts: + - mountPath: "/data" + name: data + resources: + limits: + memory: "768Mi" + requests: + memory: "70Mi" + cpu: "1200m" + env: + - name: TZ + value: "Europe/Stockholm" + - name: KLARNA_API_USERNAME + valueFrom: + secretKeyRef: + name: klarna-api-credentials + key: username + - name: KLARNA_API_PASSWORD + valueFrom: + secretKeyRef: + name: klarna-api-credentials + key: password + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: AMQP_URL + value: "amqp://admin:12bananer@rabbitmq:5672/" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name +--- +kind: Service +apiVersion: v1 +metadata: + name: persist + annotations: + prometheus.io/port: "8080" +spec: + selector: + app: persist + ports: + - name: web + port: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: persist-ingress + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + # nginx.ingress.kubernetes.io/affinity: "cookie" + # nginx.ingress.kubernetes.io/session-cookie-name: "cart-affinity" + # nginx.ingress.kubernetes.io/session-cookie-expires: "172800" + # nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" + nginx.ingress.kubernetes.io/proxy-body-size: 4m +spec: + ingressClassName: nginx + tls: + - hosts: + - storage.tornberg.me + secretName: persist-tls-secret + rules: + - host: storage.tornberg.me + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: persist + port: + number: 8080 \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..44a9a12 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.tornberg.me/go-persist + +go 1.24.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..878199a --- /dev/null +++ b/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "encoding/json" + "io" + "log" + "net/http" + "net/url" + "strings" + + "git.tornberg.me/go-persist/pkg/storage" +) + +type Folder struct { + Id string + parent *Folder + Storage storage.Storage + Children map[string]*Folder +} + +func (f *Folder) GetId() string { + return f.Id +} +func (f *Folder) GetPathIds() []string { + p := f.parent + ids := make([]string, 0) + ids = append(ids, f.GetId()) + for p != nil { + ids = append(ids, p.GetId()) + p = p.parent + } + return ids +} + +type App struct { + Root *Folder + spawnStorage func(path []string) (storage.Storage, error) +} + +func (app *App) GetFolder(pth []string) (*Folder, bool) { + current := app.Root + for _, id := range pth { + if child, exists := current.Children[id]; exists { + current = child + } else { + return nil, false + } + } + return current, true +} + +func (app *App) GetOrSpawn(pth []string) (*Folder, error) { + current := app.Root + level := []string{} + for _, id := range pth { + if child, exists := current.Children[id]; exists { + current = child + level = append(level, id) + } else { + level = append(level, id) + s, err := app.spawnStorage(level) + if err != nil { + return nil, err + } + child := &Folder{ + Id: id, + parent: current, + Storage: s, + Children: make(map[string]*Folder), + } + current.Children[id] = child + current = child + } + } + return current, nil +} + +func (app *App) AddFolder(id string, parent *Folder) (*Folder, error) { + strg, err := app.spawnStorage(parent.GetPathIds()) + if err != nil { + return nil, err + } + folder := &Folder{ + Id: id, + parent: parent, + Storage: strg, + Children: make(map[string]*Folder), + } + if parent != nil { + parent.Children[id] = folder + } + return folder, nil +} + +func GetPathAndFileFromUrl(u *url.URL) ([]string, string) { + if u == nil { + return []string{}, "" + } + parts := strings.Split(u.Path, "/")[1:] + fileName := parts[len(parts)-1] + return parts[:len(parts)-1], fileName +} + +func esimateMimeType(filename string) string { + if strings.HasSuffix(filename, ".jpg") { + return "image/jpeg" + } + if strings.HasSuffix(filename, ".png") { + return "image/png" + } + if strings.HasSuffix(filename, ".gif") { + return "image/gif" + } + if strings.HasSuffix(filename, ".bmp") { + return "image/bmp" + } + if strings.HasSuffix(filename, ".webp") { + return "image/webp" + } + if strings.HasSuffix(filename, ".json") { + return "application/json" + } + if strings.HasSuffix(filename, ".html") || strings.HasSuffix(filename, ".htm") { + return "text/html" + } + if strings.HasSuffix(filename, ".css") { + return "text/css" + } + if strings.HasSuffix(filename, ".txt") { + return "text/plain" + } + return "application/octet-stream" +} + +func main() { + // Initialize the application + rootDir, err := storage.NewDiskStorage([]string{}) + if err != nil { + log.Fatal(err) + } + app := &App{ + Root: &Folder{ + Id: "data", + Storage: rootDir, + Children: make(map[string]*Folder), + }, + spawnStorage: func(path []string) (storage.Storage, error) { + if path[0] == "tmp" { + return storage.NewMemoryStorage(path) + } + return storage.NewDiskStorage(path) + }, + } + + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + pth, fileName := GetPathAndFileFromUrl(r.URL) + log.Printf("Request path parts: %+v, fileName:%s", pth, fileName) + folder, err := app.GetOrSpawn(pth) + if err != nil { + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + } + log.Printf("Retrieved folder: %+v, exists: %v", folder) + if fileName == "" { + content, err := folder.Storage.List("") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(content) + } else { + if r.Method == "GET" { + content, err := folder.Storage.Get(fileName) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", esimateMimeType(fileName)) + w.Header().Set("Cache-Control", "public; max-age=60") + w.Header().Set("Content-Disposition", "attachment; filename=\""+fileName+"\"") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(content) + } else { + defer r.Body.Close() + data, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + } + folder.Storage.Put(fileName, data) + w.WriteHeader(http.StatusCreated) + } + } + w.WriteHeader(http.StatusOK) + }) + if err := http.ListenAndServe(":8080", mux); err != nil { + panic(err) + } +} diff --git a/pkg/storage/disk-storage.go b/pkg/storage/disk-storage.go new file mode 100644 index 0000000..61e9ac4 --- /dev/null +++ b/pkg/storage/disk-storage.go @@ -0,0 +1,78 @@ +package storage + +import ( + "io" + "os" + "path" +) + +var ( + DiskStorageBasePath = []string{"data"} +) + +type DiskStorage struct { + dir string +} + +func NewDiskStorage(ids []string) (Storage, error) { + pth := path.Join(DiskStorageBasePath...) + idPth := path.Join(ids...) + dir := path.Join(pth, idPth) + s := &DiskStorage{dir: dir} + err := s.ensureDir() + if err != nil { + return nil, err + } + return s, nil +} + +func (ds *DiskStorage) ensureDir() error { + pth := path.Dir(ds.dir + "/") + return os.MkdirAll(pth, 0777) +} + +func (ds *DiskStorage) Get(key string) (io.Reader, error) { + if err := ds.ensureDir(); err != nil { + return nil, err + } + f, err := os.Open(path.Join(ds.dir, key)) + if err != nil { + return nil, err + } + return f, nil +} + +func (ds *DiskStorage) Put(key string, data []byte) error { + if err := ds.ensureDir(); err != nil { + return err + } + f, err := os.Open(path.Join(ds.dir, key)) + if err != nil { + return err + } + _, err = f.Write(data) + if err != nil { + return err + } + return f.Close() +} + +func (ds *DiskStorage) Delete(key string) error { + // Implement disk storage delete logic here + return os.Remove(path.Join(ds.dir, key)) +} + +func (ds *DiskStorage) List(prefix string) ([]string, error) { + if err := ds.ensureDir(); err != nil { + return nil, err + } + data, err := os.ReadDir(path.Join(ds.dir, prefix)) + if err != nil { + return nil, err + } + res := make([]string, 0, len(data)) + for _, file := range data { + res = append(res, file.Name()) + } + return res, nil +} diff --git a/pkg/storage/mem-storage.go b/pkg/storage/mem-storage.go new file mode 100644 index 0000000..7e0bf0d --- /dev/null +++ b/pkg/storage/mem-storage.go @@ -0,0 +1,44 @@ +package storage + +import ( + "bytes" + "io" + "os" +) + +type MemoryStorage struct { + cache map[string][]byte +} + +func NewMemoryStorage(ids []string) (Storage, error) { + s := &MemoryStorage{ + cache: make(map[string][]byte), + } + return s, nil +} + +func (ms *MemoryStorage) Get(key string) (io.Reader, error) { + content, ok := ms.cache[key] + if !ok { + return nil, os.ErrNotExist + } + return bytes.NewReader(content), nil +} + +func (ms *MemoryStorage) Put(key string, data []byte) error { + ms.cache[key] = data + return nil +} + +func (ms *MemoryStorage) Delete(key string) error { + delete(ms.cache, key) + return nil +} + +func (ms *MemoryStorage) List(prefix string) ([]string, error) { + res := make([]string, 0, len(ms.cache)) + for file, _ := range ms.cache { + res = append(res, file) + } + return res, nil +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go new file mode 100644 index 0000000..3bad54e --- /dev/null +++ b/pkg/storage/storage.go @@ -0,0 +1,12 @@ +package storage + +import ( + "io" +) + +type Storage interface { + Get(key string) (io.Reader, error) + Put(key string, data []byte) error + Delete(key string) error + List(prefix string) ([]string, error) +}