mirror of
https://github.com/usememos/memos.git
synced 2025-02-19 04:40:40 +01:00
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:
parent
5d06c8093c
commit
e6c9f2a00e
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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")}
|
||||
|
@ -140,6 +140,9 @@
|
||||
"file-name-placeholder": "File name",
|
||||
"type": "Type",
|
||||
"type-placeholder": "File type"
|
||||
},
|
||||
"download-link": {
|
||||
"option": "Download link"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -257,6 +257,9 @@
|
||||
"type": "类型",
|
||||
"type-placeholder": "文件类型"
|
||||
},
|
||||
"download-link": {
|
||||
"option": "从外部链接下载"
|
||||
},
|
||||
"local-file": {
|
||||
"choose": "选择文件...",
|
||||
"option": "本地文件"
|
||||
|
1
web/src/types/modules/resource.d.ts
vendored
1
web/src/types/modules/resource.d.ts
vendored
@ -19,6 +19,7 @@ interface ResourceCreate {
|
||||
filename: string;
|
||||
externalLink: string;
|
||||
type: string;
|
||||
downloadToLocal: boolean;
|
||||
}
|
||||
|
||||
interface ResourcePatch {
|
||||
|
Loading…
x
Reference in New Issue
Block a user