mirror of
				https://github.com/khairul169/garage-webui.git
				synced 2025-10-30 14:49:32 +07:00 
			
		
		
		
	Compare commits
	
		
			No commits in common. "b8b87d8289daea1fc0605d4294a959338184e8ce" and "611258d0dbd80fced31b2029d7ab3d5053c53913" have entirely different histories.
		
	
	
		
			b8b87d8289
			...
			611258d0db
		
	
		
| @ -55,7 +55,7 @@ services: | ||||
| Get the latest binary from the [release page](https://github.com/khairul169/garage-webui/releases/latest) according to your OS architecture. For example: | ||||
| 
 | ||||
| ```sh | ||||
| $ wget -O garage-webui https://github.com/khairul169/garage-webui/releases/download/1.0.5/garage-webui-v1.0.5-linux-amd64 | ||||
| $ wget -O garage-webui https://github.com/khairul169/garage-webui/releases/download/1.0.4/garage-webui-v1.0.4-linux-amd64 | ||||
| $ chmod +x garage-webui | ||||
| $ sudo cp garage-webui /usr/local/bin | ||||
| ``` | ||||
| @ -134,8 +134,6 @@ However, if it fails to load, you can set these environment variables instead: | ||||
| - `CONFIG_PATH`: Path to the Garage `config.toml` file. Defaults to `/etc/garage.toml`. | ||||
| - `API_BASE_URL`: Garage admin API endpoint URL. | ||||
| - `API_ADMIN_KEY`: Admin API key. | ||||
| - `S3_REGION`: S3 Region. | ||||
| - `S3_ENDPOINT_URL`: S3 Endpoint url. | ||||
| 
 | ||||
| ### Running | ||||
| 
 | ||||
|  | ||||
| @ -8,7 +8,6 @@ require ( | ||||
| 	github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0 | ||||
| 	github.com/aws/smithy-go v1.20.4 | ||||
| 	github.com/joho/godotenv v1.5.1 | ||||
| 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 | ||||
| 	github.com/pelletier/go-toml/v2 v2.2.2 | ||||
| ) | ||||
| 
 | ||||
|  | ||||
| @ -27,8 +27,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||
| github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= | ||||
| github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= | ||||
| 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
|  | ||||
| @ -16,7 +16,7 @@ func main() { | ||||
| 	utils.InitCacheManager() | ||||
| 
 | ||||
| 	if err := utils.Garage.LoadConfig(); err != nil { | ||||
| 		log.Println("Cannot load garage config!", err) | ||||
| 		log.Fatal("Failed to load config! ", err) | ||||
| 	} | ||||
| 
 | ||||
| 	http.Handle("/api/", http.StripPrefix("/api", router.HandleApiRouter())) | ||||
|  | ||||
| @ -73,7 +73,8 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { | ||||
| 			ObjectKey:    &key, | ||||
| 			LastModified: object.LastModified, | ||||
| 			Size:         object.Size, | ||||
| 			Url:          fmt.Sprintf("/browse/%s/%s", bucket, *object.Key), | ||||
| 			ViewUrl:      fmt.Sprintf("/browse/%s/%s?view=1", bucket, *object.Key), | ||||
| 			DownloadUrl:  fmt.Sprintf("/browse/%s/%s?dl=1", bucket, *object.Key), | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| @ -83,10 +84,8 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { | ||||
| func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { | ||||
| 	bucket := r.PathValue("bucket") | ||||
| 	key := r.PathValue("key") | ||||
| 	queryParams := r.URL.Query() | ||||
| 	view := queryParams.Get("view") == "1" | ||||
| 	thumbnail := queryParams.Get("thumb") == "1" | ||||
| 	download := queryParams.Get("dl") == "1" | ||||
| 	view := r.URL.Query().Get("view") == "1" | ||||
| 	download := r.URL.Query().Get("dl") == "1" | ||||
| 
 | ||||
| 	client, err := getS3Client(bucket) | ||||
| 	if err != nil { | ||||
| @ -94,18 +93,6 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !view && !download && !thumbnail { | ||||
| 		object, err := client.HeadObject(context.Background(), &s3.HeadObjectInput{ | ||||
| 			Bucket: aws.String(bucket), | ||||
| 			Key:    aws.String(key), | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			utils.ResponseError(w, err) | ||||
| 		} | ||||
| 		utils.ResponseSuccess(w, object) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	object, err := client.GetObject(context.Background(), &s3.GetObjectInput{ | ||||
| 		Bucket: aws.String(bucket), | ||||
| 		Key:    aws.String(key), | ||||
| @ -122,42 +109,31 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if view || download { | ||||
| 		defer object.Body.Close() | ||||
| 		keys := strings.Split(key, "/") | ||||
| 
 | ||||
| 	if download { | ||||
| 		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", keys[len(keys)-1])) | ||||
| 	} else if thumbnail { | ||||
| 		body, err := io.ReadAll(object.Body) | ||||
| 		if err != nil { | ||||
| 			utils.ResponseError(w, err) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		thumb, err := utils.CreateThumbnailImage(body, 64, 64) | ||||
| 		if err != nil { | ||||
| 
 | ||||
| 			utils.ResponseError(w, err) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		w.Header().Set("Content-Type", "image/png") | ||||
| 		w.Write(thumb) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 		w.Header().Set("Content-Type", *object.ContentType) | ||||
| 		w.Header().Set("Content-Length", strconv.FormatInt(*object.ContentLength, 10)) | ||||
| 		w.Header().Set("Cache-Control", "max-age=86400") | ||||
| 		w.Header().Set("Last-Modified", object.LastModified.Format(time.RFC1123)) | ||||
| 		w.Header().Set("Etag", *object.ETag) | ||||
| 
 | ||||
| 		if download { | ||||
| 			w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", keys[len(keys)-1])) | ||||
| 		} | ||||
| 
 | ||||
| 		w.WriteHeader(http.StatusOK) | ||||
| 		_, err = io.Copy(w, object.Body) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			utils.ResponseError(w, err) | ||||
| 			return | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	utils.ResponseSuccess(w, object) | ||||
| } | ||||
| 
 | ||||
| func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) { | ||||
| @ -324,7 +300,7 @@ func getS3Client(bucket string) (*s3.Client, error) { | ||||
| 
 | ||||
| 	awsConfig := aws.Config{ | ||||
| 		Credentials:  creds, | ||||
| 		Region:       utils.Garage.GetS3Region(), | ||||
| 		Region:       "garage", | ||||
| 		BaseEndpoint: aws.String(utils.Garage.GetS3Endpoint()), | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -13,5 +13,6 @@ type BrowserObject struct { | ||||
| 	ObjectKey    *string    `json:"objectKey"` | ||||
| 	LastModified *time.Time `json:"lastModified"` | ||||
| 	Size         *int64     `json:"size"` | ||||
| 	Url          string     `json:"url"` | ||||
| 	ViewUrl      string     `json:"viewUrl"` | ||||
| 	DownloadUrl  string     `json:"downloadUrl"` | ||||
| } | ||||
|  | ||||
| @ -3,7 +3,6 @@ package utils | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"khairul169/garage-webui/schema" | ||||
| @ -74,17 +73,6 @@ func (g *garage) GetS3Endpoint() string { | ||||
| 	return endpoint | ||||
| } | ||||
| 
 | ||||
| func (g *garage) GetS3Region() string { | ||||
| 	endpoint := os.Getenv("S3_REGION") | ||||
| 	if len(endpoint) > 0 { | ||||
| 		return endpoint | ||||
| 	} | ||||
| 	if len(g.Config.S3API.S3Region) == 0 { | ||||
| 		return "garage" | ||||
| 	} | ||||
| 	return g.Config.S3API.S3Region | ||||
| } | ||||
| 
 | ||||
| func (g *garage) GetAdminKey() string { | ||||
| 	key := os.Getenv("API_ADMIN_KEY") | ||||
| 	if len(key) > 0 { | ||||
| @ -165,7 +153,7 @@ func (g *garage) Fetch(url string, options *FetchOptions) ([]byte, error) { | ||||
| 			message = fmt.Sprintf("%v", data["message"]) | ||||
| 		} | ||||
| 
 | ||||
| 		return nil, errors.New(message) | ||||
| 		return nil, fmt.Errorf(message) | ||||
| 	} | ||||
| 
 | ||||
| 	body, err := io.ReadAll(res.Body) | ||||
|  | ||||
| @ -1,26 +0,0 @@ | ||||
| package utils | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"image" | ||||
| 	_ "image/gif" | ||||
| 	"image/jpeg" | ||||
| 	_ "image/png" | ||||
| 
 | ||||
| 	"github.com/nfnt/resize" | ||||
| ) | ||||
| 
 | ||||
| func CreateThumbnailImage(buffer []byte, width uint, height uint) ([]byte, error) { | ||||
| 	img, _, err := image.Decode(bytes.NewReader(buffer)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	thumb := resize.Thumbnail(width, height, img, resize.NearestNeighbor) | ||||
| 	buf := new(bytes.Buffer) | ||||
| 	if err := jpeg.Encode(buf, thumb, nil); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return buf.Bytes(), nil | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "garage-webui", | ||||
|   "private": true, | ||||
|   "version": "1.0.5", | ||||
|   "version": "1.0.4", | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "dev:client": "vite", | ||||
|  | ||||
| @ -30,21 +30,23 @@ const api = { | ||||
|       headers: { ...headers, ...(options?.headers || {}) }, | ||||
|     }); | ||||
| 
 | ||||
|     const isJson = res.headers | ||||
|       .get("Content-Type") | ||||
|       ?.includes("application/json"); | ||||
|     const data = isJson ? await res.json() : await res.text(); | ||||
| 
 | ||||
|     if (!res.ok) { | ||||
|       const message = isJson | ||||
|         ? data?.message | ||||
|         : typeof data === "string" | ||||
|         ? data | ||||
|         : res.statusText; | ||||
|       const json = await res.json().catch(() => {}); | ||||
|       const message = json?.message || res.statusText; | ||||
|       throw new Error(message); | ||||
|     } | ||||
| 
 | ||||
|     return data as unknown as T; | ||||
|     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>) { | ||||
|  | ||||
| @ -12,7 +12,7 @@ import { shareDialog } from "./share-dialog"; | ||||
| 
 | ||||
| type Props = { | ||||
|   prefix?: string; | ||||
|   object: Pick<Object, "objectKey" | "url">; | ||||
|   object: Pick<Object, "objectKey" | "downloadUrl">; | ||||
|   end?: boolean; | ||||
| }; | ||||
| 
 | ||||
| @ -30,7 +30,7 @@ const ObjectActions = ({ prefix = "", object, end }: Props) => { | ||||
|   }); | ||||
| 
 | ||||
|   const onDownload = () => { | ||||
|     window.open(API_URL + object.url + "?dl=1", "_blank"); | ||||
|     window.open(API_URL + object.downloadUrl, "_blank"); | ||||
|   }; | ||||
| 
 | ||||
|   const onDelete = () => { | ||||
|  | ||||
| @ -1,16 +1,10 @@ | ||||
| import { Alert, Loading, Table } from "react-daisyui"; | ||||
| import { Table } from "react-daisyui"; | ||||
| import { useBrowseObjects } from "./hooks"; | ||||
| import { dayjs, readableBytes } from "@/lib/utils"; | ||||
| import mime from "mime/lite"; | ||||
| import { Object } from "./types"; | ||||
| import { API_URL } from "@/lib/api"; | ||||
| import { | ||||
|   CircleXIcon, | ||||
|   FileArchive, | ||||
|   FileIcon, | ||||
|   FileType, | ||||
|   Folder, | ||||
| } from "lucide-react"; | ||||
| import { FileArchive, FileIcon, FileType, Folder } from "lucide-react"; | ||||
| import { useBucketContext } from "../context"; | ||||
| import ObjectActions from "./object-actions"; | ||||
| import GotoTopButton from "@/components/ui/goto-top-btn"; | ||||
| @ -22,17 +16,14 @@ type Props = { | ||||
| 
 | ||||
| const ObjectList = ({ prefix, onPrefixChange }: Props) => { | ||||
|   const { bucketName } = useBucketContext(); | ||||
|   const { data, error, isLoading } = useBrowseObjects(bucketName, { | ||||
|     prefix, | ||||
|     limit: 1000, | ||||
|   }); | ||||
|   const { data } = useBrowseObjects(bucketName, { prefix, limit: 1000 }); | ||||
| 
 | ||||
|   const onObjectClick = (object: Object) => { | ||||
|     window.open(API_URL + object.url + "?view=1", "_blank"); | ||||
|     window.open(API_URL + object.viewUrl, "_blank"); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="overflow-x-auto min-h-[400px]"> | ||||
|     <div className="overflow-x-auto overflow-y-hidden"> | ||||
|       <Table> | ||||
|         <Table.Head> | ||||
|           <span>Name</span> | ||||
| @ -41,29 +32,13 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { | ||||
|         </Table.Head> | ||||
| 
 | ||||
|         <Table.Body> | ||||
|           {isLoading ? ( | ||||
|           {!data?.prefixes?.length && !data?.objects?.length && ( | ||||
|             <tr> | ||||
|               <td colSpan={3}> | ||||
|                 <div className="h-[320px] flex items-center justify-center"> | ||||
|                   <Loading /> | ||||
|                 </div> | ||||
|               </td> | ||||
|             </tr> | ||||
|           ) : error ? ( | ||||
|             <tr> | ||||
|               <td colSpan={3}> | ||||
|                 <Alert status="error" icon={<CircleXIcon />}> | ||||
|                   <span>{error.message}</span> | ||||
|                 </Alert> | ||||
|               </td> | ||||
|             </tr> | ||||
|           ) : !data?.prefixes?.length && !data?.objects?.length ? ( | ||||
|             <tr> | ||||
|               <td className="text-center py-16" colSpan={3}> | ||||
|               <td className="text-center py-8" colSpan={3}> | ||||
|                 No objects | ||||
|               </td> | ||||
|             </tr> | ||||
|           ) : null} | ||||
|           )} | ||||
| 
 | ||||
|           {data?.prefixes.map((prefix) => ( | ||||
|             <tr | ||||
| @ -84,7 +59,7 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { | ||||
|                 </span> | ||||
|               </td> | ||||
|               <td colSpan={2} /> | ||||
|               <ObjectActions object={{ objectKey: prefix, url: "" }} /> | ||||
|               <ObjectActions object={{ objectKey: prefix, downloadUrl: "" }} /> | ||||
|             </tr> | ||||
|           ))} | ||||
| 
 | ||||
| @ -121,9 +96,7 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { | ||||
|                 <ObjectActions | ||||
|                   prefix={data.prefix} | ||||
|                   object={object} | ||||
|                   end={ | ||||
|                     idx >= data.objects.length - 2 && data.objects.length > 5 | ||||
|                   } | ||||
|                   end={idx >= data.objects.length - 2} | ||||
|                 /> | ||||
|               </tr> | ||||
|             ); | ||||
| @ -152,10 +125,9 @@ const FilePreview = ({ ext, object }: FilePreviewProps) => { | ||||
|   } | ||||
| 
 | ||||
|   if (type === "image") { | ||||
|     const thumbnailSupport = ["jpg", "jpeg", "png", "gif"].includes(ext || ""); | ||||
|     return ( | ||||
|       <img | ||||
|         src={API_URL + object.url + (thumbnailSupport ? "?thumb=1" : "?view=1")} | ||||
|         src={API_URL + object.viewUrl} | ||||
|         alt={object.objectKey} | ||||
|         className="size-5 object-cover overflow-hidden mr-2" | ||||
|       /> | ||||
|  | ||||
| @ -15,7 +15,8 @@ export type Object = { | ||||
|   objectKey: string; | ||||
|   lastModified: Date; | ||||
|   size: number; | ||||
|   url: string; | ||||
|   viewUrl: string; | ||||
|   downloadUrl: string; | ||||
| }; | ||||
| 
 | ||||
| export type PutObjectPayload = { | ||||
|  | ||||
| @ -2,18 +2,12 @@ import { useParams } from "react-router-dom"; | ||||
| import { useBucket } from "./hooks"; | ||||
| import Page from "@/context/page-context"; | ||||
| import TabView, { Tab } from "@/components/containers/tab-view"; | ||||
| import { | ||||
|   ChartLine, | ||||
|   CircleXIcon, | ||||
|   FolderSearch, | ||||
|   LockKeyhole, | ||||
| } from "lucide-react"; | ||||
| import { ChartLine, FolderSearch, LockKeyhole } from "lucide-react"; | ||||
| import OverviewTab from "./overview/overview-tab"; | ||||
| import PermissionsTab from "./permissions/permissions-tab"; | ||||
| import MenuButton from "./components/menu-button"; | ||||
| import BrowseTab from "./browse/browse-tab"; | ||||
| import { BucketContext } from "./context"; | ||||
| import { Alert, Loading } from "react-daisyui"; | ||||
| 
 | ||||
| const tabs: Tab[] = [ | ||||
|   { | ||||
| @ -38,40 +32,26 @@ const tabs: Tab[] = [ | ||||
| 
 | ||||
| const ManageBucketPage = () => { | ||||
|   const { id } = useParams(); | ||||
|   const { data, error, isLoading, refetch } = useBucket(id); | ||||
|   const { data, refetch } = useBucket(id); | ||||
| 
 | ||||
|   const name = data?.globalAliases[0]; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|     <div className="container"> | ||||
|       <Page | ||||
|         title={name || "Manage Bucket"} | ||||
|         prev="/buckets" | ||||
|         actions={data ? <MenuButton /> : undefined} | ||||
|       /> | ||||
| 
 | ||||
|       {isLoading && ( | ||||
|         <div className="h-full flex items-center justify-center"> | ||||
|           <Loading size="lg" /> | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       {error != null && ( | ||||
|         <Alert status="error" icon={<CircleXIcon />}> | ||||
|           <span>{error.message}</span> | ||||
|         </Alert> | ||||
|       )} | ||||
| 
 | ||||
|       {data && ( | ||||
|         <div className="container"> | ||||
|         <BucketContext.Provider | ||||
|           value={{ bucket: data, refetch, bucketName: name || "" }} | ||||
|         > | ||||
|           <TabView tabs={tabs} className="bg-base-100 h-14 px-1.5" /> | ||||
|         </BucketContext.Provider> | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user