From e6c9f2a00eb4159ed41b0b3b4f505479f5fecf38 Mon Sep 17 00:00:00 2001 From: Athurg Gooth Date: Thu, 8 Jun 2023 22:35:33 +0800 Subject: [PATCH] feat: add support for download resource from link (#1800) * Add support for download resource from link * Parse external link and add file ext name from mime info * Add zh-Hans locale for `download-link` * fix typo on code and comments * Update server/resource.go --------- Co-authored-by: Athurg Feng Co-authored-by: boojack --- api/resource.go | 15 +++---- server/resource.go | 44 +++++++++++++++++++-- web/src/components/CreateResourceDialog.tsx | 28 ++++++++++++- web/src/locales/en.json | 3 ++ web/src/locales/zh-Hans.json | 3 ++ web/src/types/modules/resource.d.ts | 1 + 6 files changed, 82 insertions(+), 12 deletions(-) diff --git a/api/resource.go b/api/resource.go index 7db67288..749c9953 100644 --- a/api/resource.go +++ b/api/resource.go @@ -26,13 +26,14 @@ type ResourceCreate struct { CreatorID int `json:"-"` // Domain specific fields - Filename string `json:"filename"` - Blob []byte `json:"-"` - InternalPath string `json:"internalPath"` - ExternalLink string `json:"externalLink"` - Type string `json:"type"` - Size int64 `json:"-"` - PublicID string `json:"publicId"` + Filename string `json:"filename"` + Blob []byte `json:"-"` + InternalPath string `json:"internalPath"` + ExternalLink string `json:"externalLink"` + Type string `json:"type"` + Size int64 `json:"-"` + PublicID string `json:"publicId"` + DownloadToLocal bool `json:"downloadToLocal"` } type ResourceFind struct { diff --git a/server/resource.go b/server/resource.go index abfeaa78..18bcb079 100644 --- a/server/resource.go +++ b/server/resource.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "mime" "net/http" "net/url" "os" @@ -55,9 +56,46 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { } resourceCreate.CreatorID = userID - // Only allow those external links with http prefix. - if resourceCreate.ExternalLink != "" && !strings.HasPrefix(resourceCreate.ExternalLink, "http") { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link") + if resourceCreate.ExternalLink != "" { + // Only allow those external links scheme with http/https + linkURL, err := url.Parse(resourceCreate.ExternalLink) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link").SetInternal(err) + } + if linkURL.Scheme != "http" && linkURL.Scheme != "https" { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link scheme") + } + + if resourceCreate.DownloadToLocal { + resp, err := http.Get(linkURL.String()) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Failed to request "+resourceCreate.ExternalLink) + } + defer resp.Body.Close() + + blob, err := io.ReadAll(resp.Body) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Failed to read "+resourceCreate.ExternalLink) + } + resourceCreate.Blob = blob + + mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Failed to read mime from "+resourceCreate.ExternalLink) + } + resourceCreate.Type = mediaType + + filename := path.Base(linkURL.Path) + if path.Ext(filename) == "" { + extensions, _ := mime.ExtensionsByType(mediaType) + if len(extensions) > 0 { + filename += extensions[0] + } + } + resourceCreate.Filename = filename + resourceCreate.PublicID = common.GenUUID() + resourceCreate.ExternalLink = "" + } } resource, err := s.Store.CreateResource(ctx, resourceCreate) diff --git a/web/src/components/CreateResourceDialog.tsx b/web/src/components/CreateResourceDialog.tsx index aa25cf47..32d21825 100644 --- a/web/src/components/CreateResourceDialog.tsx +++ b/web/src/components/CreateResourceDialog.tsx @@ -13,7 +13,7 @@ interface Props extends DialogProps { onConfirm?: (resourceList: Resource[]) => void; } -type SelectedMode = "local-file" | "external-link"; +type SelectedMode = "local-file" | "external-link" | "download-link"; interface State { selectedMode: SelectedMode; @@ -32,6 +32,7 @@ const CreateResourceDialog: React.FC = (props: Props) => { filename: "", externalLink: "", type: "", + downloadToLocal: false, }); const [fileList, setFileList] = useState([]); const fileInputRef = useRef(null); @@ -70,7 +71,7 @@ const CreateResourceDialog: React.FC = (props: Props) => { destroy(); }; - const handleSelectedModeChanged = (mode: "local-file" | "external-link") => { + const handleSelectedModeChanged = (mode: "local-file" | "external-link" | "download-link") => { setState((state) => { return { ...state, @@ -129,6 +130,10 @@ const CreateResourceDialog: React.FC = (props: Props) => { if (resourceCreate.filename === "" || resourceCreate.externalLink === "" || resourceCreate.type === "") { return false; } + } else if (state.selectedMode === "download-link") { + if (resourceCreate.externalLink === "") { + return false; + } } return true; }; @@ -161,6 +166,9 @@ const CreateResourceDialog: React.FC = (props: Props) => { createdResourceList.push(resource); } } else { + if (state.selectedMode === "download-link") { + resourceCreate.downloadToLocal = true; + } const resource = await resourceStore.createResource(resourceCreate); createdResourceList.push(resource); } @@ -195,6 +203,7 @@ const CreateResourceDialog: React.FC = (props: Props) => { > + {state.selectedMode === "local-file" && ( @@ -279,6 +288,21 @@ const CreateResourceDialog: React.FC = (props: Props) => { )} + {state.selectedMode === "download-link" && ( + <> + + {t("resource.create-dialog.external-link.link")} + + + + )} +