From b554cb4dbfad2cad09a9f6fdf3fd9bde77259c00 Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Sun, 18 Aug 2024 05:54:08 +0700 Subject: [PATCH] feat: rewrite backend to go --- .env.example | 2 +- backend/.env.example | 3 - backend/.gitignore | 175 +------------------------------------- backend/Makefile | 6 ++ backend/README.md | 15 ---- backend/go.mod | 5 ++ backend/go.sum | 16 ++++ backend/lib/api.ts | 83 ------------------ backend/lib/consts.ts | 2 - backend/lib/garage.ts | 16 ---- backend/lib/proxy-api.ts | 36 -------- backend/lib/utils.ts | 14 --- backend/main.go | 32 +++++++ backend/main.ts | 37 -------- backend/package.json | 20 ----- backend/pnpm-lock.yaml | 79 ----------------- backend/router/buckets.go | 51 +++++++++++ backend/router/config.go | 11 +++ backend/router/proxy.go | 28 ++++++ backend/routes/buckets.ts | 19 ----- backend/routes/config.ts | 21 ----- backend/routes/index.ts | 13 --- backend/schema/bucket.go | 45 ++++++++++ backend/schema/config.go | 34 ++++++++ backend/tsconfig.json | 27 ------ backend/types/garage.ts | 32 ------- backend/ui/ui.go | 6 ++ backend/ui/ui_prod.go | 34 ++++++++ backend/utils/garage.go | 148 ++++++++++++++++++++++++++++++++ backend/utils/utils.go | 30 +++++++ 30 files changed, 448 insertions(+), 592 deletions(-) delete mode 100644 backend/.env.example create mode 100644 backend/Makefile delete mode 100644 backend/README.md create mode 100644 backend/go.mod create mode 100644 backend/go.sum delete mode 100644 backend/lib/api.ts delete mode 100644 backend/lib/consts.ts delete mode 100644 backend/lib/garage.ts delete mode 100644 backend/lib/proxy-api.ts delete mode 100644 backend/lib/utils.ts create mode 100644 backend/main.go delete mode 100644 backend/main.ts delete mode 100644 backend/package.json delete mode 100644 backend/pnpm-lock.yaml create mode 100644 backend/router/buckets.go create mode 100644 backend/router/config.go create mode 100644 backend/router/proxy.go delete mode 100644 backend/routes/buckets.ts delete mode 100644 backend/routes/config.ts delete mode 100644 backend/routes/index.ts create mode 100644 backend/schema/bucket.go create mode 100644 backend/schema/config.go delete mode 100644 backend/tsconfig.json delete mode 100644 backend/types/garage.ts create mode 100644 backend/ui/ui.go create mode 100644 backend/ui/ui_prod.go create mode 100644 backend/utils/garage.go create mode 100644 backend/utils/utils.go diff --git a/.env.example b/.env.example index 8f584f2..92e5e5e 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -VITE_API_URL=http://localhost:3903 +VITE_API_URL=http://localhost:3909 diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index d7bb055..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -# App -API_BASE_URL=http://localhost:3903 -CONFIG_PATH=/app/garage/garage.toml diff --git a/backend/.gitignore b/backend/.gitignore index 9b1ee42..b16cdc0 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,175 +1,2 @@ -# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore - -# Logs - -logs -_.log -npm-debug.log_ -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Caches - -.cache - -# Diagnostic reports (https://nodejs.org/api/report.html) - -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# Runtime data - -pids -_.pid -_.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover - -lib-cov - -# Coverage directory used by tools like istanbul - -coverage -*.lcov - -# nyc test coverage - -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) - -.grunt - -# Bower dependency directory (https://bower.io/) - -bower_components - -# node-waf configuration - -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) - -build/Release - -# Dependency directories - -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) - -web_modules/ - -# TypeScript cache - -*.tsbuildinfo - -# Optional npm cache directory - -.npm - -# Optional eslint cache - -.eslintcache - -# Optional stylelint cache - -.stylelintcache - -# Microbundle cache - -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history - -.node_repl_history - -# Output of 'npm pack' - -*.tgz - -# Yarn Integrity file - -.yarn-integrity - -# dotenv environment variable files - -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) - -.parcel-cache - -# Next.js build output - -.next -out - -# Nuxt.js build / generate output - -.nuxt dist - -# Gatsby files - -# Comment in the public line in if your project uses Gatsby and not Next.js - -# https://nextjs.org/blog/next-9-1#public-directory-support - -# public - -# vuepress build output - -.vuepress/dist - -# vuepress v2.x temp and cache directory - -.temp - -# Docusaurus cache and generated files - -.docusaurus - -# Serverless directories - -.serverless/ - -# FuseBox cache - -.fusebox/ - -# DynamoDB Local files - -.dynamodb/ - -# TernJS port file - -.tern-port - -# Stores VSCode versions used for testing VSCode extensions - -.vscode-test - -# yarn v2 - -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store +main diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..b88a0bb --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,6 @@ + +build: + go build -o main -tags="prod" main.go + +run: + go run main.go diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 04a419d..0000000 --- a/backend/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# backend - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.1.18. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..0df54ba --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,5 @@ +module khairul169/garage-webui + +go 1.22.5 + +require github.com/pelletier/go-toml/v2 v2.2.2 // indirect diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..ca42a46 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/lib/api.ts b/backend/lib/api.ts deleted file mode 100644 index 5074d76..0000000 --- a/backend/lib/api.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { config } from "./garage"; - -type FetchOptions = Omit & { - params?: Record; - headers?: Record; - body?: any; -}; - -const adminPort = config?.admin?.api_bind_addr?.split(":").pop(); -const adminAddr = - import.meta.env.API_BASE_URL || - config?.rpc_public_addr?.split(":")[0] + ":" + adminPort || - ""; - -export const API_BASE_URL = - !adminAddr.startsWith("http") && !adminAddr.startsWith("https") - ? `http://${adminAddr}` - : adminAddr; - -export const API_ADMIN_KEY = - import.meta.env.API_ADMIN_KEY || config?.admin?.admin_token; - -const api = { - async fetch(url: string, options?: Partial) { - const headers: Record = { - Authorization: `Bearer ${API_ADMIN_KEY}`, - }; - const _url = new URL(API_BASE_URL + url); - - if (options?.params) { - Object.entries(options.params).forEach(([key, value]) => { - _url.searchParams.set(key, String(value)); - }); - } - - if ( - typeof options?.body === "object" && - !(options.body instanceof FormData) - ) { - options.body = JSON.stringify(options.body); - headers["Content-Type"] = "application/json"; - } - - const res = await fetch(_url, { - ...options, - headers: { ...headers, ...(options?.headers || {}) }, - }); - - if (!res.ok) { - const err = new Error(res.statusText); - (err as any).status = res.status; - throw err; - } - - const isJson = res.headers - .get("Content-Type") - ?.includes("application/json"); - - if (isJson) { - const json = (await res.json()) as T; - return json; - } - - const text = await res.text(); - return text as unknown as T; - }, - - async get(url: string, options?: Partial) { - return this.fetch(url, { - ...options, - method: "GET", - }); - }, - - async post(url: string, options?: Partial) { - return this.fetch(url, { - ...options, - method: "POST", - }); - }, -}; - -export default api; diff --git a/backend/lib/consts.ts b/backend/lib/consts.ts deleted file mode 100644 index a031531..0000000 --- a/backend/lib/consts.ts +++ /dev/null @@ -1,2 +0,0 @@ -// -export const __PROD = import.meta.env.NODE_ENV === "production"; diff --git a/backend/lib/garage.ts b/backend/lib/garage.ts deleted file mode 100644 index 9d0befc..0000000 --- a/backend/lib/garage.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Config } from "../types/garage"; -import { readTomlFile } from "./utils"; - -const CONFIG_PATH = process.env.CONFIG_PATH || "/etc/garage.toml"; - -export const config = await readTomlFile(CONFIG_PATH); - -if (!config?.rpc_public_addr) { - throw new Error( - "Cannot load garage config! Missing `rpc_public_addr` in config file." - ); -} - -if (!config?.admin?.admin_token) { - throw new Error("Missing `admin.admin_token` in config."); -} diff --git a/backend/lib/proxy-api.ts b/backend/lib/proxy-api.ts deleted file mode 100644 index c076fcc..0000000 --- a/backend/lib/proxy-api.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Context } from "hono"; -import { API_ADMIN_KEY, API_BASE_URL } from "./api"; - -export const proxyApi = async (c: Context) => { - const url = new URL(c.req.url); - const pathname = url.pathname.replace(/\/api\//, "/"); - const reqUrl = new URL(API_BASE_URL + pathname + url.search); - - try { - const headers = c.req.raw.headers; - let body: BodyInit | ReadableStream | null = c.req.raw.body; - - headers.set("authorization", `Bearer ${API_ADMIN_KEY}`); - - if (headers.get("content-type")?.includes("application/json")) { - const json = await c.req.json(); - body = JSON.stringify(json); - } - - const res = await fetch(reqUrl, { - ...c.req.raw, - method: c.req.method, - headers, - body, - }); - return res; - } catch (err) { - return c.json( - { - success: false, - error: (err as Error)?.message || "Server error", - }, - 500 - ); - } -}; diff --git a/backend/lib/utils.ts b/backend/lib/utils.ts deleted file mode 100644 index 197068f..0000000 --- a/backend/lib/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import toml from "toml"; - -export const readTomlFile = async (path?: string | null) => { - if (!path) { - return undefined; - } - - const file = Bun.file(path); - if (!(await file.exists())) { - return undefined; - } - - return toml.parse(await file.text()) as T; -}; diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..7e6ddf9 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "khairul169/garage-webui/router" + "khairul169/garage-webui/ui" + "khairul169/garage-webui/utils" + "log" + "net/http" +) + +func main() { + if err := utils.Garage.LoadConfig(); err != nil { + log.Fatal("Failed to load config! ", err) + } + + http.HandleFunc("/api/config", router.GetConfig) + http.HandleFunc("/api/buckets", router.GetAllBuckets) + http.HandleFunc("/api/*", router.ProxyHandler) + + ui.ServeUI() + + host := utils.GetEnv("HOST", "0.0.0.0") + port := utils.GetEnv("PORT", "3908") + + addr := fmt.Sprintf("%s:%s", host, port) + log.Printf("Starting server on http://%s", addr) + + if err := http.ListenAndServe(addr, nil); err != nil { + log.Fatal(err) + } +} diff --git a/backend/main.ts b/backend/main.ts deleted file mode 100644 index 8ab447b..0000000 --- a/backend/main.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Hono } from "hono"; -import { logger } from "hono/logger"; -import { serveStatic } from "hono/bun"; -import router from "./routes"; -import { __PROD } from "./lib/consts"; - -const HOST = import.meta.env.HOST || "0.0.0.0"; -const PORT = Number(import.meta.env.PORT) || 3909; -const DIST_ROOT = import.meta.env.DIST_ROOT || "./dist"; - -const app = new Hono(); - -app.use(logger()); - -// API router -app.route("/api", router); - -if (__PROD) { - // Serve client dist - app.use(serveStatic({ root: DIST_ROOT })); - app.use(async (c, next) => { - try { - const file = Bun.file(DIST_ROOT + "/index.html"); - return c.html(await file.text()); - } catch (err) { - next(); - } - }); - - console.log(`Listening on http://${HOST}:${PORT}`); -} - -export default { - fetch: app.fetch, - hostname: HOST, - port: PORT, -}; diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index d49165e..0000000 --- a/backend/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "backend", - "module": "main.ts", - "type": "module", - "scripts": { - "dev": "bun --watch main.ts", - "build": "bun build main.ts --minify --sourcemap --outdir ./dist", - "start": "NODE_ENV=production bun run ./dist/main.js" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "dependencies": { - "hono": "^4.5.5", - "toml": "^3.0.0" - } -} \ No newline at end of file diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml deleted file mode 100644 index cbfb90c..0000000 --- a/backend/pnpm-lock.yaml +++ /dev/null @@ -1,79 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - hono: - specifier: ^4.5.5 - version: 4.5.5 - toml: - specifier: ^3.0.0 - version: 3.0.0 - typescript: - specifier: ^5.0.0 - version: 5.5.4 - devDependencies: - '@types/bun': - specifier: latest - version: 1.1.6 - -packages: - - '@types/bun@1.1.6': - resolution: {integrity: sha512-uJgKjTdX0GkWEHZzQzFsJkWp5+43ZS7HC8sZPFnOwnSo1AsNl2q9o2bFeS23disNDqbggEgyFkKCHl/w8iZsMA==} - - '@types/node@20.12.14': - resolution: {integrity: sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==} - - '@types/ws@8.5.12': - resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} - - bun-types@1.1.17: - resolution: {integrity: sha512-Z4+OplcSd/YZq7ZsrfD00DKJeCwuNY96a1IDJyR73+cTBaFIS7SC6LhpY/W3AMEXO9iYq5NJ58WAwnwL1p5vKg==} - - hono@4.5.5: - resolution: {integrity: sha512-fXBXHqaVfimWofbelLXci8pZyIwBMkDIwCa4OwZvK+xVbEyYLELVP4DfbGaj1aEM6ZY3hHgs4qLvCO2ChkhgQw==} - engines: {node: '>=16.0.0'} - - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - -snapshots: - - '@types/bun@1.1.6': - dependencies: - bun-types: 1.1.17 - - '@types/node@20.12.14': - dependencies: - undici-types: 5.26.5 - - '@types/ws@8.5.12': - dependencies: - '@types/node': 20.12.14 - - bun-types@1.1.17: - dependencies: - '@types/node': 20.12.14 - '@types/ws': 8.5.12 - - hono@4.5.5: {} - - toml@3.0.0: {} - - typescript@5.5.4: {} - - undici-types@5.26.5: {} diff --git a/backend/router/buckets.go b/backend/router/buckets.go new file mode 100644 index 0000000..ac33bb1 --- /dev/null +++ b/backend/router/buckets.go @@ -0,0 +1,51 @@ +package router + +import ( + "encoding/json" + "fmt" + "khairul169/garage-webui/schema" + "khairul169/garage-webui/utils" + "net/http" +) + +func GetAllBuckets(w http.ResponseWriter, r *http.Request) { + body, err := utils.Garage.Fetch("/v1/bucket?list", &utils.FetchOptions{}) + if err != nil { + utils.ResponseError(w, err) + return + } + + var buckets []schema.GetBucketsRes + if err := json.Unmarshal(body, &buckets); err != nil { + utils.ResponseError(w, err) + return + } + + ch := make(chan schema.Bucket, len(buckets)) + + for _, bucket := range buckets { + go func() { + body, err := utils.Garage.Fetch(fmt.Sprintf("/v1/bucket?id=%s", bucket.ID), &utils.FetchOptions{}) + + if err != nil { + ch <- schema.Bucket{ID: bucket.ID, GlobalAliases: bucket.GlobalAliases} + return + } + + var bucket schema.Bucket + if err := json.Unmarshal(body, &bucket); err != nil { + ch <- schema.Bucket{ID: bucket.ID, GlobalAliases: bucket.GlobalAliases} + return + } + + ch <- bucket + }() + } + + res := make([]schema.Bucket, 0, len(buckets)) + for i := 0; i < len(buckets); i++ { + res = append(res, <-ch) + } + + utils.ResponseSuccess(w, res) +} diff --git a/backend/router/config.go b/backend/router/config.go new file mode 100644 index 0000000..a61cca4 --- /dev/null +++ b/backend/router/config.go @@ -0,0 +1,11 @@ +package router + +import ( + "khairul169/garage-webui/utils" + "net/http" +) + +func GetConfig(w http.ResponseWriter, r *http.Request) { + config := utils.Garage.Config + utils.ResponseSuccess(w, config) +} diff --git a/backend/router/proxy.go b/backend/router/proxy.go new file mode 100644 index 0000000..73c6a16 --- /dev/null +++ b/backend/router/proxy.go @@ -0,0 +1,28 @@ +package router + +import ( + "fmt" + "khairul169/garage-webui/utils" + "net/http" + "net/http/httputil" + "net/url" + "strings" +) + +func ProxyHandler(w http.ResponseWriter, r *http.Request) { + target, err := url.Parse(utils.Garage.GetAdminEndpoint()) + if err != nil { + utils.ResponseError(w, err) + return + } + + proxy := &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(target) + r.Out.URL.Path = strings.TrimPrefix(r.In.URL.Path, "/api") + r.Out.Header.Set("Authorization", fmt.Sprintf("Bearer %s", utils.Garage.GetAdminKey())) + }, + } + + proxy.ServeHTTP(w, r) +} diff --git a/backend/routes/buckets.ts b/backend/routes/buckets.ts deleted file mode 100644 index 0d0989e..0000000 --- a/backend/routes/buckets.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Hono } from "hono"; -import api from "../lib/api"; - -export const buckets = new Hono() - - /** - * Get all buckets - */ - .get("/", async (c) => { - const data = await api.get("/v1/bucket?list"); - - const buckets = await Promise.all( - data.map(async (bucket: any) => { - return api.get("/v1/bucket", { params: { id: bucket.id } }); - }) - ); - - return c.json(buckets); - }); diff --git a/backend/routes/config.ts b/backend/routes/config.ts deleted file mode 100644 index 0870986..0000000 --- a/backend/routes/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Hono } from "hono"; -import { config } from "../lib/garage"; - -export const configRoute = new Hono() - - /** - * Get garage config - */ - .get("/", async (c) => { - const data = { - ...(config || {}), - rpc_secret: undefined, - admin: { - ...(config?.admin || {}), - admin_token: undefined, - metrics_token: undefined, - }, - }; - - return c.json(data); - }); diff --git a/backend/routes/index.ts b/backend/routes/index.ts deleted file mode 100644 index aa187d4..0000000 --- a/backend/routes/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Hono } from "hono"; -import { buckets } from "./buckets"; -import { configRoute } from "./config"; -import { proxyApi } from "../lib/proxy-api"; - -const router = new Hono() - // - .route("/config", configRoute) - .route("/buckets", buckets) - - .all("*", proxyApi); - -export default router; diff --git a/backend/schema/bucket.go b/backend/schema/bucket.go new file mode 100644 index 0000000..1046da3 --- /dev/null +++ b/backend/schema/bucket.go @@ -0,0 +1,45 @@ +package schema + +type GetBucketsRes struct { + ID string `json:"id"` + GlobalAliases []string `json:"globalAliases"` + LocalAliases []string `json:"localAliases"` +} + +type Bucket struct { + ID string `json:"id"` + GlobalAliases []string `json:"globalAliases"` + WebsiteAccess bool `json:"websiteAccess"` + WebsiteConfig WebsiteConfig `json:"websiteConfig"` + Keys []KeyElement `json:"keys"` + Objects int64 `json:"objects"` + Bytes int64 `json:"bytes"` + UnfinishedUploads int64 `json:"unfinishedUploads"` + UnfinishedMultipartUploads int64 `json:"unfinishedMultipartUploads"` + UnfinishedMultipartUploadParts int64 `json:"unfinishedMultipartUploadParts"` + UnfinishedMultipartUploadBytes int64 `json:"unfinishedMultipartUploadBytes"` + Quotas Quotas `json:"quotas"` +} + +type KeyElement struct { + AccessKeyID string `json:"accessKeyId"` + Name string `json:"name"` + Permissions Permissions `json:"permissions"` + BucketLocalAliases []interface{} `json:"bucketLocalAliases"` +} + +type Permissions struct { + Read bool `json:"read"` + Write bool `json:"write"` + Owner bool `json:"owner"` +} + +type Quotas struct { + MaxSize int64 `json:"maxSize"` + MaxObjects int64 `json:"maxObjects"` +} + +type WebsiteConfig struct { + IndexDocument string `json:"indexDocument"` + ErrorDocument string `json:"errorDocument"` +} diff --git a/backend/schema/config.go b/backend/schema/config.go new file mode 100644 index 0000000..fea2cd4 --- /dev/null +++ b/backend/schema/config.go @@ -0,0 +1,34 @@ +package schema + +type Config struct { + CompressionLevel int64 `json:"compression_level" toml:"compression_level"` + DataDir string `json:"data_dir" toml:"data_dir"` + DBEngine string `json:"db_engine" toml:"db_engine"` + MetadataAutoSnapshotInterval string `json:"metadata_auto_snapshot_interval" toml:"metadata_auto_snapshot_interval"` + MetadataDir string `json:"metadata_dir" toml:"metadata_dir"` + ReplicationFactor int64 `json:"replication_factor" toml:"replication_factor"` + RPCBindAddr string `json:"rpc_bind_addr" toml:"rpc_bind_addr"` + RPCPublicAddr string `json:"rpc_public_addr" toml:"rpc_public_addr"` + RPCSecret string `json:"rpc_secret" toml:"rpc_secret"` + Admin Admin `json:"admin" toml:"admin"` + S3API S3API `json:"s3_api" toml:"s3_api"` + S3Web S3Web `json:"s3_web" toml:"s3_web"` +} + +type Admin struct { + AdminToken string `json:"admin_token" toml:"admin_token"` + APIBindAddr string `json:"api_bind_addr" toml:"api_bind_addr"` + MetricsToken string `json:"metrics_token" toml:"metrics_token"` +} + +type S3API struct { + APIBindAddr string `json:"api_bind_addr" toml:"api_bind_addr"` + RootDomain string `json:"root_domain" toml:"root_domain"` + S3Region string `json:"s3_region" toml:"s3_region"` +} + +type S3Web struct { + BindAddr string `json:"bind_addr" toml:"bind_addr"` + Index string `json:"index" toml:"index"` + RootDomain string `json:"root_domain" toml:"root_domain"` +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json deleted file mode 100644 index 238655f..0000000 --- a/backend/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} diff --git a/backend/types/garage.ts b/backend/types/garage.ts deleted file mode 100644 index 78a49eb..0000000 --- a/backend/types/garage.ts +++ /dev/null @@ -1,32 +0,0 @@ -export type Config = { - metadata_dir: string; - data_dir: string; - db_engine: string; - metadata_auto_snapshot_interval: string; - replication_factor: number; - compression_level: number; - rpc_bind_addr: string; - rpc_public_addr: string; - rpc_secret: string; - s3_api?: S3API; - s3_web?: S3Web; - admin?: Admin; -}; - -export type Admin = { - api_bind_addr: string; - admin_token: string; - metrics_token: string; -}; - -export type S3API = { - s3_region: string; - api_bind_addr: string; - root_domain: string; -}; - -export type S3Web = { - bind_addr: string; - root_domain: string; - index: string; -}; diff --git a/backend/ui/ui.go b/backend/ui/ui.go new file mode 100644 index 0000000..ab0f361 --- /dev/null +++ b/backend/ui/ui.go @@ -0,0 +1,6 @@ +//go:build !prod +// +build !prod + +package ui + +func ServeUI() {} diff --git a/backend/ui/ui_prod.go b/backend/ui/ui_prod.go new file mode 100644 index 0000000..7db5330 --- /dev/null +++ b/backend/ui/ui_prod.go @@ -0,0 +1,34 @@ +//go:build prod +// +build prod + +package ui + +import ( + "embed" + "io/fs" + "net/http" + "path" +) + +//go:embed dist +var embeddedFs embed.FS + +func ServeUI() { + distFs, _ := fs.Sub(embeddedFs, "dist") + fileServer := http.FileServer(http.FS(distFs)) + + http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _path := path.Clean(r.URL.Path)[1:] + + // Rewrite non-existing paths to index.html + if _, err := fs.Stat(distFs, _path); err != nil { + index, _ := fs.ReadFile(distFs, "index.html") + w.Header().Add("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + w.Write(index) + return + } + + fileServer.ServeHTTP(w, r) + })) +} diff --git a/backend/utils/garage.go b/backend/utils/garage.go new file mode 100644 index 0000000..af5dd60 --- /dev/null +++ b/backend/utils/garage.go @@ -0,0 +1,148 @@ +package utils + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "khairul169/garage-webui/schema" + "log" + "net/http" + "os" + "strings" + + "github.com/pelletier/go-toml/v2" +) + +type garage struct { + Config schema.Config +} + +var Garage = &garage{} + +func (g *garage) LoadConfig() error { + path := GetEnv("CONFIG_PATH", "/etc/garage/garage.toml") + data, err := os.ReadFile(path) + + if err != nil { + return err + } + + var cfg schema.Config + err = toml.Unmarshal(data, &cfg) + if err != nil { + log.Fatal(err) + } + + g.Config = cfg + + return nil +} + +func (g *garage) GetAdminEndpoint() string { + endpoint := os.Getenv("API_BASE_URL") + if len(endpoint) > 0 { + return endpoint + } + + host := strings.Split(g.Config.RPCPublicAddr, ":")[0] + port := LastString(strings.Split(g.Config.Admin.APIBindAddr, ":")) + + endpoint = fmt.Sprintf("%s:%s", host, port) + if !strings.HasPrefix(endpoint, "http") { + endpoint = fmt.Sprintf("http://%s", endpoint) + } + + return endpoint +} + +func (g *garage) GetAdminKey() string { + key := os.Getenv("API_ADMIN_KEY") + if len(key) > 0 { + return key + } + return g.Config.Admin.AdminToken +} + +type FetchOptions struct { + Method string + Params map[string]string + Body interface{} + Headers map[string]string +} + +func (g *garage) Fetch(url string, options *FetchOptions) ([]byte, error) { + var reqBody io.Reader + reqUrl := fmt.Sprintf("%s%s", g.GetAdminEndpoint(), url) + method := http.MethodGet + + if len(options.Method) > 0 { + method = options.Method + } + + if options.Body != nil { + body, err := json.Marshal(options.Body) + if err != nil { + return nil, err + } + reqBody = bytes.NewBuffer(body) + } + + req, err := http.NewRequest(method, reqUrl, reqBody) + if err != nil { + return nil, err + } + + if options.Params != nil { + q := req.URL.Query() + for k, v := range options.Params { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + // Add auth token + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", g.GetAdminKey())) + + if options.Headers != nil { + for k, v := range options.Headers { + req.Header.Add(k, v) + } + } + + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return nil, err + } + if res.Body != nil { + defer res.Body.Close() + } + + if res.StatusCode != 200 { + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var data map[string]interface{} + + if err := json.Unmarshal(body, &data); err != nil { + return nil, err + } + + message := fmt.Sprintf("unexpected status code: %d", res.StatusCode) + if data["message"] != nil { + message = fmt.Sprintf("%v", data["message"]) + } + + return nil, fmt.Errorf(message) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + return body, nil +} diff --git a/backend/utils/utils.go b/backend/utils/utils.go new file mode 100644 index 0000000..7f08b0c --- /dev/null +++ b/backend/utils/utils.go @@ -0,0 +1,30 @@ +package utils + +import ( + "encoding/json" + "net/http" + "os" +) + +func GetEnv(key, defaultValue string) string { + value := os.Getenv(key) + if len(value) == 0 { + return defaultValue + } + return value +} + +func LastString(str []string) string { + return str[len(str)-1] +} + +func ResponseError(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) +} + +func ResponseSuccess(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(data) +}