Add delete, inline preview, and mobile list layout to the file browser

- DELETE /files/{path} recursively removes a file/folder (root itself
  can't be deleted, 404 on missing paths); wired to a new "Delete"
  action in the web UI with a confirmation prompt.
- Files now open inline (Content-Disposition: inline) so browsers can
  play/preview natively-supported types (video, audio, images, PDF)
  directly instead of always forcing a download. A separate
  "Download" link (?download=1) still forces a save-as.
- Narrow screens get a stacked card list (name, size, modified date,
  download/delete actions) instead of a squeezed table, so nothing is
  hidden or requires horizontal scrolling; the table layout is kept
  unchanged for sm+ screens.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-21 09:15:40 +02:00
co-authored by Copilot
parent d0e1dab837
commit b2f39bd1f6
5 changed files with 459 additions and 96 deletions
+40 -2
View File
@@ -96,12 +96,42 @@ func (s *Server) handleFiles(w http.ResponseWriter, r *http.Request) {
return
}
s.handleFilesUpload(w, r, root, fullPath)
case http.MethodDelete:
s.handleFilesDelete(w, r, root, relPath, fullPath)
default:
w.Header().Set("Allow", "GET, POST")
w.Header().Set("Allow", "GET, POST, DELETE")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
// handleFilesDelete removes the file or folder (recursively) at fullPath,
// mounted at DELETE /files/{path}. The root itself (relPath == "") can
// never be deleted this way.
func (s *Server) handleFilesDelete(w http.ResponseWriter, r *http.Request, root, relPath, fullPath string) {
if relPath == "" {
http.Error(w, "cannot delete the root folder", http.StatusBadRequest)
return
}
if _, err := os.Lstat(fullPath); err != nil {
if errors.Is(err, os.ErrNotExist) {
http.NotFound(w, r)
return
}
s.logger.Error("stat path to delete", "path", fullPath, "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if err := os.RemoveAll(fullPath); err != nil {
s.logger.Error("deleting path", "path", fullPath, "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "deleted %q", filepath.Base(fullPath))
}
// handleFilesMkdir creates a new subdirectory named by the "name" form
// field directly inside fullPath (the current directory), mounted at
// POST /files/{path}?mkdir=1.
@@ -161,7 +191,15 @@ func (s *Server) handleFilesGet(w http.ResponseWriter, r *http.Request, username
return
}
defer f.Close()
w.Header().Set("Content-Disposition", `attachment; filename="`+filepath.Base(fullPath)+`"`)
// Force a download only when explicitly requested (the "Download"
// link/button); otherwise serve "inline" so the browser can render
// natively-supported types (video, audio, images, PDF, text) right
// in the tab instead of always saving to disk.
disposition := "inline"
if r.URL.Query().Has("download") {
disposition = "attachment"
}
w.Header().Set("Content-Disposition", disposition+`; filename="`+filepath.Base(fullPath)+`"`)
http.ServeContent(w, r, info.Name(), info.ModTime(), f)
return
}