mirror of
https://github.com/khairul169/garage-webui.git
synced 2025-04-27 14:29:31 +07:00
feat: rewrite backend to go
This commit is contained in:
parent
e1d2274bfb
commit
b554cb4dbf
@ -1 +1 @@
|
||||
VITE_API_URL=http://localhost:3903
|
||||
VITE_API_URL=http://localhost:3909
|
||||
|
@ -1,3 +0,0 @@
|
||||
# App
|
||||
API_BASE_URL=http://localhost:3903
|
||||
CONFIG_PATH=/app/garage/garage.toml
|
175
backend/.gitignore
vendored
175
backend/.gitignore
vendored
@ -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
6
backend/Makefile
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
build:
|
||||
go build -o main -tags="prod" main.go
|
||||
|
||||
run:
|
||||
go run main.go
|
@ -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
5
backend/go.mod
Normal 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
16
backend/go.sum
Normal 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=
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
//
|
||||
export const __PROD = import.meta.env.NODE_ENV === "production";
|
@ -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.");
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
};
|
@ -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
32
backend/main.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
@ -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
79
backend/pnpm-lock.yaml
generated
@ -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
51
backend/router/buckets.go
Normal 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
11
backend/router/config.go
Normal 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
28
backend/router/proxy.go
Normal 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)
|
||||
}
|
@ -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);
|
||||
});
|
@ -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);
|
||||
});
|
@ -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
45
backend/schema/bucket.go
Normal 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
34
backend/schema/config.go
Normal 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"`
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
6
backend/ui/ui.go
Normal file
@ -0,0 +1,6 @@
|
||||
//go:build !prod
|
||||
// +build !prod
|
||||
|
||||
package ui
|
||||
|
||||
func ServeUI() {}
|
34
backend/ui/ui_prod.go
Normal file
34
backend/ui/ui_prod.go
Normal 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
148
backend/utils/garage.go
Normal 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
30
backend/utils/utils.go
Normal 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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user