diff --git a/backend/go.mod b/backend/go.mod index c6aa4de..d6afbe0 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,6 +8,7 @@ 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 ) diff --git a/backend/go.sum b/backend/go.sum index 345e5aa..8a8a6b7 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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/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= diff --git a/backend/router/browse.go b/backend/router/browse.go index fb33ad0..72d0a3d 100644 --- a/backend/router/browse.go +++ b/backend/router/browse.go @@ -73,8 +73,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { ObjectKey: &key, LastModified: object.LastModified, Size: object.Size, - ViewUrl: fmt.Sprintf("/browse/%s/%s?view=1", bucket, *object.Key), - DownloadUrl: fmt.Sprintf("/browse/%s/%s?dl=1", bucket, *object.Key), + Url: fmt.Sprintf("/browse/%s/%s", 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) { bucket := r.PathValue("bucket") key := r.PathValue("key") - view := r.URL.Query().Get("view") == "1" - download := r.URL.Query().Get("dl") == "1" + queryParams := r.URL.Query() + view := queryParams.Get("view") == "1" + thumbnail := queryParams.Get("thumb") == "1" + download := queryParams.Get("dl") == "1" client, err := getS3Client(bucket) if err != nil { @@ -93,6 +94,18 @@ 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), @@ -109,31 +122,42 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { return } - if view || download { - defer object.Body.Close() - keys := strings.Split(key, "/") - - 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) + 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 } - utils.ResponseSuccess(w, object) + 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) + + _, err = io.Copy(w, object.Body) + + if err != nil { + utils.ResponseError(w, err) + return + } } func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) { diff --git a/backend/schema/browse.go b/backend/schema/browse.go index 3a7eb51..098ec6f 100644 --- a/backend/schema/browse.go +++ b/backend/schema/browse.go @@ -13,6 +13,5 @@ type BrowserObject struct { ObjectKey *string `json:"objectKey"` LastModified *time.Time `json:"lastModified"` Size *int64 `json:"size"` - ViewUrl string `json:"viewUrl"` - DownloadUrl string `json:"downloadUrl"` + Url string `json:"url"` } diff --git a/backend/utils/image.go b/backend/utils/image.go new file mode 100644 index 0000000..eacca4c --- /dev/null +++ b/backend/utils/image.go @@ -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 +} diff --git a/src/pages/buckets/manage/browse/object-actions.tsx b/src/pages/buckets/manage/browse/object-actions.tsx index 2583fff..97ed549 100644 --- a/src/pages/buckets/manage/browse/object-actions.tsx +++ b/src/pages/buckets/manage/browse/object-actions.tsx @@ -12,7 +12,7 @@ import { shareDialog } from "./share-dialog"; type Props = { prefix?: string; - object: Pick; + object: Pick; end?: boolean; }; @@ -30,7 +30,7 @@ const ObjectActions = ({ prefix = "", object, end }: Props) => { }); const onDownload = () => { - window.open(API_URL + object.downloadUrl, "_blank"); + window.open(API_URL + object.url + "?dl=1", "_blank"); }; const onDelete = () => { diff --git a/src/pages/buckets/manage/browse/object-list.tsx b/src/pages/buckets/manage/browse/object-list.tsx index c27f575..081fb80 100644 --- a/src/pages/buckets/manage/browse/object-list.tsx +++ b/src/pages/buckets/manage/browse/object-list.tsx @@ -28,11 +28,11 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { }); const onObjectClick = (object: Object) => { - window.open(API_URL + object.viewUrl, "_blank"); + window.open(API_URL + object.url + "?view=1", "_blank"); }; return ( -
+
Name @@ -44,7 +44,7 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { {isLoading ? ( @@ -84,7 +84,7 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { ))} @@ -121,7 +121,9 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { = data.objects.length - 2} + end={ + idx >= data.objects.length - 2 && data.objects.length > 5 + } /> ); @@ -150,9 +152,10 @@ const FilePreview = ({ ext, object }: FilePreviewProps) => { } if (type === "image") { + const thumbnailSupport = ["jpg", "jpeg", "png", "gif"].includes(ext || ""); return ( {object.objectKey} diff --git a/src/pages/buckets/manage/browse/types.ts b/src/pages/buckets/manage/browse/types.ts index 565b8ec..272ce8c 100644 --- a/src/pages/buckets/manage/browse/types.ts +++ b/src/pages/buckets/manage/browse/types.ts @@ -15,8 +15,7 @@ export type Object = { objectKey: string; lastModified: Date; size: number; - viewUrl: string; - downloadUrl: string; + url: string; }; export type PutObjectPayload = {
-
+
- +