diff --git a/frontend/src/api/files.ts b/frontend/src/api/files.ts
index d9ee12d6..499d2527 100644
--- a/frontend/src/api/files.ts
+++ b/frontend/src/api/files.ts
@@ -213,6 +213,14 @@ export function getSubtitlesURL(file: ResourceItem) {
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
}
+export async function downloadFromURL(url: string, path: string) {
+ const res = await fetchURL(`/api/downloads`, {
+ method: "POST",
+ body: JSON.stringify({ url, path }),
+ });
+ return res.json();
+}
+
export async function usage(url: string, signal: AbortSignal) {
url = removePrefix(url);
diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json
index 7de128ed..3085a05f 100644
--- a/frontend/src/i18n/en.json
+++ b/frontend/src/i18n/en.json
@@ -11,6 +11,7 @@
"create": "Create",
"delete": "Delete",
"download": "Download",
+ "downloadFromUrl": "Download from URL",
"file": "File",
"folder": "Folder",
"fullScreen": "Toggle full screen",
diff --git a/frontend/src/views/files/FileListing.vue b/frontend/src/views/files/FileListing.vue
index 3f052d54..ccd88b8c 100644
--- a/frontend/src/views/files/FileListing.vue
+++ b/frontend/src/views/files/FileListing.vue
@@ -72,6 +72,13 @@
:label="t('buttons.upload')"
@action="uploadFunc"
/>
+
{
});
};
+const downloadUrl = () => {
+ layoutStore.showHover({
+ prompt: "url",
+ confirm: (event: Event, url: string) => {
+ event.preventDefault();
+ layoutStore.closeHovers();
+
+ if (url === "") {
+ return;
+ }
+
+ api.downloadFromURL(url, route.path)
+ .then(() => {
+ fileStore.reload = true;
+ })
+ .catch($showError);
+ },
+ });
+};
+
const switchView = async () => {
layoutStore.closeHovers();
diff --git a/http/download.go b/http/download.go
new file mode 100644
index 00000000..849e34fa
--- /dev/null
+++ b/http/download.go
@@ -0,0 +1,73 @@
+package http
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/url"
+ "path"
+
+ "github.com/filebrowser/filebrowser/v2/files"
+)
+
+type downloadBody struct {
+ URL string `json:"url"`
+ Path string `json:"path"`
+}
+
+var urlDownloadHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
+ if !d.user.Perm.Create {
+ return http.StatusForbidden, nil
+ }
+
+ var body downloadBody
+ err := json.NewDecoder(r.Body).Decode(&body)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ if body.URL == "" || body.Path == "" {
+ return http.StatusBadRequest, nil
+ }
+
+ _, err = url.ParseRequestURI(body.URL)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ fileName := path.Base(body.URL)
+ filePath := path.Join(body.Path, fileName)
+
+ if !d.Check(filePath) {
+ return http.StatusForbidden, nil
+ }
+
+ resp, err := http.Get(body.URL)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ defer resp.Body.Close()
+
+ err = d.RunHook(func() error {
+ _, writeErr := writeFile(d.user.Fs, filePath, resp.Body, d.settings.FileMode, d.settings.DirMode)
+ return writeErr
+ }, "upload", filePath, "", d.user)
+
+ if err != nil {
+ _ = d.user.Fs.RemoveAll(filePath)
+ return http.StatusInternalServerError, err
+ }
+
+ file, err := files.NewFileInfo(&files.FileOptions{
+ Fs: d.user.Fs,
+ Path: filePath,
+ Modify: d.user.Perm.Modify,
+ Expand: false,
+ ReadHeader: d.server.TypeDetectionByHeader,
+ Checker: d,
+ })
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return renderJSON(w, r, file)
+})
diff --git a/http/http.go b/http/http.go
index 2d87535f..f1da1160 100644
--- a/http/http.go
+++ b/http/http.go
@@ -70,6 +70,7 @@ func NewHandler(
api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD", "GET")
api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(), "/api/tus")).Methods("PATCH")
api.PathPrefix("/tus").Handler(monkey(tusDeleteHandler(), "/api/tus")).Methods("DELETE")
+ api.PathPrefix("/downloads").Handler(monkey(urlDownloadHandler, "/api/downloads")).Methods("POST")
api.PathPrefix("/usage").Handler(monkey(diskUsage, "/api/usage")).Methods("GET")