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