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 <athurg@gooth.org>
Co-authored-by: boojack <stevenlgtm@gmail.com>
This commit is contained in:
Athurg Gooth 2023-06-08 22:35:33 +08:00 committed by GitHub
parent 5d06c8093c
commit e6c9f2a00e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 82 additions and 12 deletions

View File

@ -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 {

View File

@ -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)

View File

@ -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: Props) => {
filename: "",
externalLink: "",
type: "",
downloadToLocal: false,
});
const [fileList, setFileList] = useState<File[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);
@ -70,7 +71,7 @@ const CreateResourceDialog: React.FC<Props> = (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: 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: 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: Props) => {
>
<Option value="local-file">{t("resource.create-dialog.local-file.option")}</Option>
<Option value="external-link">{t("resource.create-dialog.external-link.option")}</Option>
<Option value="download-link">{t("resource.create-dialog.download-link.option")}</Option>
</Select>
{state.selectedMode === "local-file" && (
@ -279,6 +288,21 @@ const CreateResourceDialog: React.FC<Props> = (props: Props) => {
</>
)}
{state.selectedMode === "download-link" && (
<>
<Typography className="!mb-1" level="body2">
{t("resource.create-dialog.external-link.link")}
</Typography>
<Input
className="mb-2"
placeholder={t("resource.create-dialog.external-link.link-placeholder")}
value={resourceCreate.externalLink}
onChange={handleExternalLinkChanged}
fullWidth
/>
</>
)}
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
<Button variant="plain" color="neutral" onClick={handleCloseDialog}>
{t("common.cancel")}

View File

@ -140,6 +140,9 @@
"file-name-placeholder": "File name",
"type": "Type",
"type-placeholder": "File type"
},
"download-link": {
"option": "Download link"
}
}
},

View File

@ -257,6 +257,9 @@
"type": "类型",
"type-placeholder": "文件类型"
},
"download-link": {
"option": "从外部链接下载"
},
"local-file": {
"choose": "选择文件...",
"option": "本地文件"

View File

@ -19,6 +19,7 @@ interface ResourceCreate {
filename: string;
externalLink: string;
type: string;
downloadToLocal: boolean;
}
interface ResourcePatch {