feat: rewrite backend to go

This commit is contained in:
Khairul Hidayat 2024-08-18 05:54:08 +07:00
parent e1d2274bfb
commit b554cb4dbf
30 changed files with 448 additions and 592 deletions

View File

@ -1 +1 @@
VITE_API_URL=http://localhost:3903
VITE_API_URL=http://localhost:3909

View File

@ -1,3 +0,0 @@
# App
API_BASE_URL=http://localhost:3903
CONFIG_PATH=/app/garage/garage.toml

175
backend/.gitignore vendored
View File

@ -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

6
backend/Makefile Normal file
View File

@ -0,0 +1,6 @@
build:
go build -o main -tags="prod" main.go
run:
go run main.go

View File

@ -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.

5
backend/go.mod Normal file
View File

@ -0,0 +1,5 @@
module khairul169/garage-webui
go 1.22.5
require github.com/pelletier/go-toml/v2 v2.2.2 // indirect

16
backend/go.sum Normal file
View File

@ -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=

View File

@ -1,83 +0,0 @@
import { config } from "./garage";
type FetchOptions = Omit<RequestInit, "headers" | "body"> & {
params?: Record<string, any>;
headers?: Record<string, string>;
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<T = any>(url: string, options?: Partial<FetchOptions>) {
const headers: Record<string, string> = {
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<T = any>(url: string, options?: Partial<FetchOptions>) {
return this.fetch<T>(url, {
...options,
method: "GET",
});
},
async post<T = any>(url: string, options?: Partial<FetchOptions>) {
return this.fetch<T>(url, {
...options,
method: "POST",
});
},
};
export default api;

View File

@ -1,2 +0,0 @@
//
export const __PROD = import.meta.env.NODE_ENV === "production";

View File

@ -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>(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.");
}

View File

@ -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<Uint8Array> | 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
);
}
};

View File

@ -1,14 +0,0 @@
import toml from "toml";
export const readTomlFile = async <T = any>(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;
};

32
backend/main.go Normal file
View File

@ -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)
}
}

View File

@ -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,
};

View File

@ -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"
}
}

79
backend/pnpm-lock.yaml generated
View File

@ -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: {}

51
backend/router/buckets.go Normal file
View File

@ -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)
}

11
backend/router/config.go Normal file
View File

@ -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)
}

28
backend/router/proxy.go Normal file
View File

@ -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)
}

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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;

45
backend/schema/bucket.go Normal file
View File

@ -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"`
}

34
backend/schema/config.go Normal file
View File

@ -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"`
}

View File

@ -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
}
}

View File

@ -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;
};

6
backend/ui/ui.go Normal file
View File

@ -0,0 +1,6 @@
//go:build !prod
// +build !prod
package ui
func ServeUI() {}

34
backend/ui/ui_prod.go Normal file
View File

@ -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)
}))
}

148
backend/utils/garage.go Normal file
View File

@ -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
}

30
backend/utils/utils.go Normal file
View File

@ -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)
}