feat(filetransfer): allow file deletion and add user delete config option (#683)

* feat(filetransfer): allow file deletion and add user delete config option

* fix(client): make user_delete required boolean in payload

* lint.

* feat(filetransfer): add confirmation dialog for file deletion.

* feat(filetransfer): add delete file confirmation messages in multiple languages

* lint.

---------

Co-authored-by: Miroslav Šedivý <sedivy.miro@gmail.com>
This commit is contained in:
Abdellatif Anaflous
2026-08-07 17:12:02 +01:00
committed by GitHub
parent 30d8e1c595
commit d6a4200b36
26 changed files with 205 additions and 10 deletions
+28
View File
@@ -388,6 +388,34 @@ func (h *LegacyHandler) Route(r types.Router) {
return err
})
r.Delete("/file", func(w http.ResponseWriter, r *http.Request) error {
if h.isBanned(r) {
return utils.HttpForbidden("banned ip")
}
s := h.newSession(r)
// create a new session
username := r.URL.Query().Get("usr")
password := r.URL.Query().Get("pwd")
err := s.create(username, password)
if err != nil {
return utils.HttpForbidden(err.Error())
}
defer s.destroy()
filename := r.URL.Query().Get("filename")
body, _, err := s.req(http.MethodDelete, "/api/filetransfer?filename="+url.QueryEscape(filename), r.Header, nil)
if err != nil {
return utils.HttpInternalServerError().WithInternalErr(err)
}
// copy the body to the response writer
_, err = io.Copy(w, body)
return err
})
r.Get("/health", func(w http.ResponseWriter, r *http.Request) error {
_, err := w.Write([]byte("true"))
return err
@@ -113,6 +113,7 @@ type FileTransferList struct {
Cwd string `json:"cwd"`
UserDownload bool `json:"user_download"`
UserUpload bool `json:"user_upload"`
UserDelete bool `json:"user_delete"`
Files []types.FileListItem `json:"files"`
}
@@ -611,6 +611,7 @@ func (s *session) wsToClient(msg []byte) error {
Cwd: request.RootDir,
UserDownload: request.UserDownload,
UserUpload: request.UserUpload,
UserDelete: request.UserDelete,
Files: files,
})
@@ -14,6 +14,7 @@ type Config struct {
RefreshInterval time.Duration
UserDownload bool
UserUpload bool
UserDelete bool
}
func (Config) Init(cmd *cobra.Command) error {
@@ -42,6 +43,11 @@ func (Config) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().Bool("filetransfer.user_delete", false, "allow non-admin users to delete files")
if err := viper.BindPFlag("filetransfer.user_delete", cmd.PersistentFlags().Lookup("filetransfer.user_delete")); err != nil {
return err
}
// v2 config
cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature")
@@ -64,6 +70,7 @@ func (s *Config) Set() {
s.RefreshInterval = viper.GetDuration("filetransfer.refresh_interval")
s.UserDownload = viper.GetBool("filetransfer.user_download")
s.UserUpload = viper.GetBool("filetransfer.user_upload")
s.UserDelete = viper.GetBool("filetransfer.user_delete")
// v2 config
@@ -107,6 +107,7 @@ func (m *Manager) broadcastUpdate() {
RootDir: m.config.RootDir,
UserDownload: m.config.UserDownload,
UserUpload: m.config.UserUpload,
UserDelete: m.config.UserDelete,
Files: fileList,
})
}
@@ -121,6 +122,7 @@ func (m *Manager) sendUpdate(session types.Session) {
RootDir: m.config.RootDir,
UserDownload: m.config.UserDownload,
UserUpload: m.config.UserUpload,
UserDelete: m.config.UserDelete,
Files: fileList,
})
}
@@ -206,6 +208,59 @@ func (m *Manager) Start() error {
return nil
}
func (m *Manager) deleteFileHandler(w http.ResponseWriter, r *http.Request) error {
session, ok := auth.GetSession(r)
if !ok {
return utils.HttpUnauthorized("session not found")
}
enabled, err := m.isEnabledForSession(session)
if err != nil {
return utils.HttpInternalServerError().
WithInternalErr(err).
Msg("error checking file transfer permissions")
}
if !enabled {
return utils.HttpForbidden("file transfer is disabled")
}
if !session.Profile().IsAdmin && !m.config.UserDelete {
return utils.HttpForbidden("file delete is not allowed for non-admin users")
}
filename := r.URL.Query().Get("filename")
badChars, err := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename)
if filename == "" || badChars || err != nil {
return utils.HttpBadRequest().
WithInternalErr(err).
Msg("bad filename")
}
filename = filepath.Clean(filename)
filename = filepath.Base(filename)
filePath := filepath.Join(m.config.RootDir, filename)
if err := os.Remove(filePath); err != nil {
if os.IsNotExist(err) {
return utils.HttpNotFound("file not found")
}
return utils.HttpInternalServerError().
WithInternalErr(err).
Msg("error deleting file")
}
err, changed := m.refresh()
if err != nil {
m.logger.Err(err).Msg("unable to refresh file list after delete")
}
if changed {
m.broadcastUpdate()
}
return nil
}
func (m *Manager) Shutdown() error {
close(m.shutdown)
return nil
@@ -214,6 +269,7 @@ func (m *Manager) Shutdown() error {
func (m *Manager) Route(r types.Router) {
r.Get("/", m.downloadFileHandler)
r.Post("/", m.uploadFileHandler)
r.Delete("/", m.deleteFileHandler)
}
func (m *Manager) WebSocketHandler(session types.Session, msg types.WebSocketMessage) bool {
@@ -15,6 +15,7 @@ type Message struct {
RootDir string `json:"root_dir"`
UserDownload bool `json:"user_download"`
UserUpload bool `json:"user_upload"`
UserDelete bool `json:"user_delete"`
Files []Item `json:"files"`
}