add domain parameter to createS3Storage and update related components

This commit is contained in:
nyne
2025-05-16 14:57:54 +08:00
parent 1b31af411d
commit a827b67c41
4 changed files with 103 additions and 77 deletions

View File

@@ -448,8 +448,14 @@ class Network {
} }
} }
async createS3Storage(name: string, endPoint: string, accessKeyID: string, async createS3Storage(
secretAccessKey: string, bucketName: string, maxSizeInMB: number): Promise<Response<any>> { name: string,
endPoint: string,
accessKeyID: string,
secretAccessKey: string,
bucketName: string,
maxSizeInMB: number,
domain: string): Promise<Response<any>> {
try { try {
const response = await axios.post(`${this.apiBaseUrl}/storage/s3`, { const response = await axios.post(`${this.apiBaseUrl}/storage/s3`, {
name, name,
@@ -457,7 +463,8 @@ class Network {
accessKeyID, accessKeyID,
secretAccessKey, secretAccessKey,
bucketName, bucketName,
maxSizeInMB maxSizeInMB,
domain
}); });
return response.data; return response.data;
} catch (e: any) { } catch (e: any) {

View File

@@ -1,10 +1,10 @@
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import {Storage} from "../network/models.ts"; import { Storage } from "../network/models.ts";
import {network} from "../network/network.ts"; import { network } from "../network/network.ts";
import showToast from "../components/toast.ts"; import showToast from "../components/toast.ts";
import Loading from "../components/loading.tsx"; import Loading from "../components/loading.tsx";
import {MdAdd, MdDelete} from "react-icons/md"; import { MdAdd, MdDelete } from "react-icons/md";
import {ErrorAlert} from "../components/alert.tsx"; import { ErrorAlert } from "../components/alert.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { app } from "../app.ts"; import { app } from "../app.ts";
@@ -30,15 +30,15 @@ export default function StorageView() {
}, []); }, []);
if (!app.user) { if (!app.user) {
return <ErrorAlert className={"m-4"} message={t("You are not logged in. Please log in to access this page.")}/> return <ErrorAlert className={"m-4"} message={t("You are not logged in. Please log in to access this page.")} />
} }
if (!app.user?.is_admin) { if (!app.user?.is_admin) {
return <ErrorAlert className={"m-4"} message={t("You are not authorized to access this page.")}/> return <ErrorAlert className={"m-4"} message={t("You are not authorized to access this page.")} />
} }
if (storages == null) { if (storages == null) {
return <Loading/> return <Loading />
} }
const updateStorages = async () => { const updateStorages = async () => {
@@ -77,9 +77,9 @@ export default function StorageView() {
return <> return <>
<div role="alert" className={`alert alert-info alert-outline ${storages.length !== 0 && "hidden"} mx-4 mb-4`}> <div role="alert" className={`alert alert-info alert-outline ${storages.length !== 0 && "hidden"} mx-4 mb-4`}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
className="h-6 w-6 shrink-0 stroke-current"> className="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg> </svg>
<span> <span>
{t("No storage found. Please create a new storage.")} {t("No storage found. Please create a new storage.")}
@@ -88,60 +88,60 @@ export default function StorageView() {
<div className={`rounded-box border border-base-content/10 bg-base-100 mx-4 mb-4 overflow-x-auto ${storages.length === 0 ? "hidden" : ""}`}> <div className={`rounded-box border border-base-content/10 bg-base-100 mx-4 mb-4 overflow-x-auto ${storages.length === 0 ? "hidden" : ""}`}>
<table className={"table"}> <table className={"table"}>
<thead> <thead>
<tr> <tr>
<td>{t("Name")}</td> <td>{t("Name")}</td>
<td>{t("Created At")}</td> <td>{t("Created At")}</td>
<td>{t("Space")}</td> <td>{t("Space")}</td>
<td>{t("Action")}</td> <td>{t("Action")}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {
storages.map((s) => { storages.map((s) => {
return <tr key={s.id} className={"hover"}> return <tr key={s.id} className={"hover"}>
<td> <td>
{s.name} {s.name}
</td> </td>
<td> <td>
{(new Date(s.createdAt)).toLocaleString()} {(new Date(s.createdAt)).toLocaleString()}
</td> </td>
<td> <td>
{(s.currentSize/1024/1024).toFixed(2)} / {s.maxSize/1024/1024} MB {(s.currentSize / 1024 / 1024).toFixed(2)} / {s.maxSize / 1024 / 1024} MB
</td> </td>
<td> <td>
<button className={"btn btn-square"} type={"button"} onClick={() => { <button className={"btn btn-square"} type={"button"} onClick={() => {
const dialog = document.getElementById(`confirm_delete_dialog_${s.id}`) as HTMLDialogElement; const dialog = document.getElementById(`confirm_delete_dialog_${s.id}`) as HTMLDialogElement;
dialog.showModal(); dialog.showModal();
}}> }}>
{loadingId === s.id ? <span className={"loading loading-spinner loading-sm"}></span> : <MdDelete size={24}/>} {loadingId === s.id ? <span className={"loading loading-spinner loading-sm"}></span> : <MdDelete size={24} />}
</button> </button>
<dialog id={`confirm_delete_dialog_${s.id}`} className="modal"> <dialog id={`confirm_delete_dialog_${s.id}`} className="modal">
<div className="modal-box"> <div className="modal-box">
<h3 className="text-lg font-bold">{t("Delete Storage")}</h3> <h3 className="text-lg font-bold">{t("Delete Storage")}</h3>
<p className="py-4"> <p className="py-4">
{t("Are you sure you want to delete this storage? This action cannot be undone.")} {t("Are you sure you want to delete this storage? This action cannot be undone.")}
</p> </p>
<div className="modal-action"> <div className="modal-action">
<form method="dialog"> <form method="dialog">
<button className="btn">{t("Cancel")}</button> <button className="btn">{t("Cancel")}</button>
</form> </form>
<button className="btn btn-error" onClick={() => { <button className="btn btn-error" onClick={() => {
handleDelete(s.id); handleDelete(s.id);
}}> }}>
{t("Delete")} {t("Delete")}
</button> </button>
</div>
</div> </div>
</div> </dialog>
</dialog> </td>
</td> </tr>
</tr> })
}) }
}
</tbody> </tbody>
</table> </table>
</div> </div>
<div className={"flex flex-row-reverse px-4"}> <div className={"flex flex-row-reverse px-4"}>
<NewStorageDialog onAdded={updateStorages}/> <NewStorageDialog onAdded={updateStorages} />
</div> </div>
</> </>
} }
@@ -151,7 +151,7 @@ enum StorageType {
s3, s3,
} }
function NewStorageDialog({onAdded}: { onAdded: () => void }) { function NewStorageDialog({ onAdded }: { onAdded: () => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [storageType, setStorageType] = useState<StorageType | null>(null); const [storageType, setStorageType] = useState<StorageType | null>(null);
@@ -163,6 +163,7 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
secretAccessKey: "", secretAccessKey: "",
bucketName: "", bucketName: "",
maxSizeInMB: 0, maxSizeInMB: 0,
domain: "",
}); });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -189,7 +190,7 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
setIsLoading(false); setIsLoading(false);
return; return;
} }
response = await network.createS3Storage(params.name, params.endPoint, params.accessKeyID, params.secretAccessKey, params.bucketName, params.maxSizeInMB); response = await network.createS3Storage(params.name, params.endPoint, params.accessKeyID, params.secretAccessKey, params.bucketName, params.maxSizeInMB, params.domain);
} }
if (response!.success) { if (response!.success) {
@@ -206,11 +207,11 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
} }
return <> return <>
<button className="btn" onClick={()=> { <button className="btn" onClick={() => {
const dialog = document.getElementById("new_storage_dialog") as HTMLDialogElement; const dialog = document.getElementById("new_storage_dialog") as HTMLDialogElement;
dialog.showModal(); dialog.showModal();
}}> }}>
<MdAdd/> <MdAdd />
{t("New Storage")} {t("New Storage")}
</button> </button>
<dialog id="new_storage_dialog" className="modal"> <dialog id="new_storage_dialog" className="modal">
@@ -221,13 +222,13 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
<form className="filter mb-2"> <form className="filter mb-2">
<input className="btn btn-square" type="reset" value="×" onClick={() => { <input className="btn btn-square" type="reset" value="×" onClick={() => {
setStorageType(null); setStorageType(null);
}}/> }} />
<input className="btn" type="radio" name="type" aria-label={t("Local")} onInput={() => { <input className="btn" type="radio" name="type" aria-label={t("Local")} onInput={() => {
setStorageType(StorageType.local); setStorageType(StorageType.local);
}}/> }} />
<input className="btn" type="radio" name="type" aria-label={t("S3")} onInput={() => { <input className="btn" type="radio" name="type" aria-label={t("S3")} onInput={() => {
setStorageType(StorageType.s3); setStorageType(StorageType.s3);
}}/> }} />
</form> </form>
{ {
@@ -239,7 +240,7 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
...params, ...params,
name: e.target.value, name: e.target.value,
}) })
}}/> }} />
</label> </label>
<label className="input w-full my-2"> <label className="input w-full my-2">
{t("Path")} {t("Path")}
@@ -248,7 +249,7 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
...params, ...params,
path: e.target.value, path: e.target.value,
}) })
}}/> }} />
</label> </label>
<label className="input w-full my-2"> <label className="input w-full my-2">
{t("Max Size (MB)")} {t("Max Size (MB)")}
@@ -278,7 +279,7 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
...params, ...params,
name: e.target.value, name: e.target.value,
}) })
}}/> }} />
</label> </label>
<label className="input w-full my-2"> <label className="input w-full my-2">
{t("Endpoint")} {t("Endpoint")}
@@ -287,7 +288,7 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
...params, ...params,
endPoint: e.target.value, endPoint: e.target.value,
}) })
}}/> }} />
</label> </label>
<label className="input w-full my-2"> <label className="input w-full my-2">
{t("Access Key ID")} {t("Access Key ID")}
@@ -296,7 +297,7 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
...params, ...params,
accessKeyID: e.target.value, accessKeyID: e.target.value,
}) })
}}/> }} />
</label> </label>
<label className="input w-full my-2"> <label className="input w-full my-2">
{t("Secret Access Key")} {t("Secret Access Key")}
@@ -305,7 +306,7 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
...params, ...params,
secretAccessKey: e.target.value, secretAccessKey: e.target.value,
}) })
}}/> }} />
</label> </label>
<label className="input w-full my-2"> <label className="input w-full my-2">
{t("Bucket Name")} {t("Bucket Name")}
@@ -314,7 +315,16 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
...params, ...params,
bucketName: e.target.value, bucketName: e.target.value,
}) })
}}/> }} />
</label>
<label className="input w-full my-2">
{t("Domain")}
<input type="text" placeholder={t("Optional")} className="w-full" value={params.domain} onChange={(e) => {
setParams({
...params,
domain: e.target.value,
})
}} />
</label> </label>
<label className="input w-full my-2"> <label className="input w-full my-2">
{t("Max Size (MB)")} {t("Max Size (MB)")}
@@ -335,7 +345,7 @@ function NewStorageDialog({onAdded}: { onAdded: () => void }) {
</> </>
} }
{error !== "" && <ErrorAlert message={error} className={"my-2"}/>} {error !== "" && <ErrorAlert message={error} className={"my-2"} />}
<div className="modal-action"> <div className="modal-action">
<form method="dialog"> <form method="dialog">

View File

@@ -14,6 +14,7 @@ type CreateS3StorageParams struct {
EndPoint string `json:"endPoint"` EndPoint string `json:"endPoint"`
AccessKeyID string `json:"accessKeyID"` AccessKeyID string `json:"accessKeyID"`
SecretAccessKey string `json:"secretAccessKey"` SecretAccessKey string `json:"secretAccessKey"`
Domain string `json:"domain"`
BucketName string `json:"bucketName"` BucketName string `json:"bucketName"`
MaxSizeInMB uint `json:"maxSizeInMB"` MaxSizeInMB uint `json:"maxSizeInMB"`
} }
@@ -32,6 +33,7 @@ func CreateS3Storage(uid uint, params CreateS3StorageParams) error {
AccessKeyID: params.AccessKeyID, AccessKeyID: params.AccessKeyID,
SecretAccessKey: params.SecretAccessKey, SecretAccessKey: params.SecretAccessKey,
BucketName: params.BucketName, BucketName: params.BucketName,
Domain: params.Domain,
} }
s := model.Storage{ s := model.Storage{
Name: params.Name, Name: params.Name,

View File

@@ -5,12 +5,13 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/url"
"time"
"github.com/gofiber/fiber/v3/log" "github.com/gofiber/fiber/v3/log"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
"net/url"
"time"
) )
type S3Storage struct { type S3Storage struct {
@@ -18,6 +19,7 @@ type S3Storage struct {
AccessKeyID string AccessKeyID string
SecretAccessKey string SecretAccessKey string
BucketName string BucketName string
Domain string
} }
func (s *S3Storage) Upload(filePath string, fileName string) (string, error) { func (s *S3Storage) Upload(filePath string, fileName string) (string, error) {
@@ -43,6 +45,10 @@ func (s *S3Storage) Upload(filePath string, fileName string) (string, error) {
} }
func (s *S3Storage) Download(storageKey string, fileName string) (string, error) { func (s *S3Storage) Download(storageKey string, fileName string) (string, error) {
if s.Domain != "" {
return s.Domain + "/" + storageKey, nil
}
minioClient, err := minio.New(s.EndPoint, &minio.Options{ minioClient, err := minio.New(s.EndPoint, &minio.Options{
Creds: credentials.NewStaticV4(s.AccessKeyID, s.SecretAccessKey, ""), Creds: credentials.NewStaticV4(s.AccessKeyID, s.SecretAccessKey, ""),
Secure: true, Secure: true,
@@ -80,6 +86,7 @@ func (s *S3Storage) FromString(config string) error {
s.AccessKeyID = s3Config.AccessKeyID s.AccessKeyID = s3Config.AccessKeyID
s.SecretAccessKey = s3Config.SecretAccessKey s.SecretAccessKey = s3Config.SecretAccessKey
s.BucketName = s3Config.BucketName s.BucketName = s3Config.BucketName
s.Domain = s3Config.Domain
if s.EndPoint == "" || s.AccessKeyID == "" || s.SecretAccessKey == "" || s.BucketName == "" { if s.EndPoint == "" || s.AccessKeyID == "" || s.SecretAccessKey == "" || s.BucketName == "" {
return errors.New("invalid S3 configuration") return errors.New("invalid S3 configuration")
} }