mirror of
https://github.com/m1k1o/neko.git
synced 2026-11-03 00:11:48 -06:00
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:
committed by
GitHub
parent
30d8e1c595
commit
d6a4200b36
@@ -10,6 +10,12 @@
|
||||
<p class="file-name" :title="item.name">{{ item.name }}</p>
|
||||
<p class="file-size">{{ fileSize(item.size) }}</p>
|
||||
<i v-if="item.type !== 'dir' && canDownload" class="fas fa-download download" @click="download(item)" />
|
||||
<i
|
||||
v-if="item.type !== 'dir' && canDelete"
|
||||
class="fas fa-trash delete"
|
||||
:title="$t('files.delete')"
|
||||
@click="deleteFile(item)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="transfer-area">
|
||||
@@ -181,10 +187,15 @@
|
||||
|
||||
.refresh:hover,
|
||||
.download:hover,
|
||||
.delete:hover,
|
||||
.remove-transfer:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.transfer-area {
|
||||
margin-top: auto;
|
||||
}
|
||||
@@ -286,6 +297,10 @@
|
||||
return this.$accessor.user.admin || this.$accessor.files.userDownload
|
||||
}
|
||||
|
||||
get canDelete() {
|
||||
return this.$accessor.user.admin || this.$accessor.files.userDelete
|
||||
}
|
||||
|
||||
get canUpload() {
|
||||
return this.$accessor.user.admin || this.$accessor.files.userUpload
|
||||
}
|
||||
@@ -450,6 +465,28 @@
|
||||
this.$accessor.files.removeTransfer(transfer)
|
||||
}
|
||||
|
||||
async deleteFile(item: FileListItem) {
|
||||
const { isConfirmed } = await this.$swal({
|
||||
title: this.$t('files.delete_title', { name: item.name }) as string,
|
||||
text: this.$t('files.delete_confirm', { name: item.name }) as string,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: this.$t('context.confirm.button_yes') as string,
|
||||
cancelButtonText: this.$t('context.confirm.button_cancel') as string,
|
||||
})
|
||||
if (!isConfirmed) {
|
||||
return
|
||||
}
|
||||
const url =
|
||||
'/file?pwd=' + encodeURIComponent(this.$accessor.password) + '&filename=' + encodeURIComponent(item.name)
|
||||
try {
|
||||
await this.$http.delete(url)
|
||||
this.$accessor.files.refresh()
|
||||
} catch (error: any) {
|
||||
this.$log.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
fileIcon(file: FileListItem) {
|
||||
let className = 'file-icon fas '
|
||||
// if is directory
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'Herunterladen',
|
||||
uploads: 'Hochladen',
|
||||
upload_here: 'Klicken oder ziehen Sie Dateien zum Hochladen hierher',
|
||||
delete: 'Löschen',
|
||||
delete_title: '"{name}" löschen?',
|
||||
delete_confirm: 'Möchten Sie diese Datei wirklich entfernen?',
|
||||
}
|
||||
|
||||
@@ -127,4 +127,7 @@ export const files = {
|
||||
downloads: 'Downloads',
|
||||
uploads: 'Uploads',
|
||||
upload_here: 'Click or drag files here to upload',
|
||||
delete: 'Delete',
|
||||
delete_title: 'Delete "{name}"?',
|
||||
delete_confirm: 'Do you really want to remove this file?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'Descargas',
|
||||
uploads: 'Subidas',
|
||||
upload_here: 'Haz clic o arrastra archivos aquí para subirlos',
|
||||
delete: 'Eliminar',
|
||||
delete_title: '¿Eliminar "{name}"?',
|
||||
delete_confirm: '¿Realmente deseas eliminar este archivo?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'Lataukset',
|
||||
uploads: 'Lataa',
|
||||
upload_here: 'Klikkaa tai vedä tiedostoja tähän ladataksesi',
|
||||
delete: 'Poista',
|
||||
delete_title: 'Poistetaanko "{name}"?',
|
||||
delete_confirm: 'Haluatko varmasti poistaa tämän tiedoston?',
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export const logout = 'Se déconnecter'
|
||||
export const unsupported = 'ce navigateur ne prend pas en charge WebRTC'
|
||||
export const admin_loggedin = "Vous êtes connecté en tant qu'admin"
|
||||
export const you = 'Vous'
|
||||
export const somebody = 'Quelqu\'un' // TODO: vérifier la traduction
|
||||
export const somebody = "Quelqu'un" // TODO: vérifier la traduction
|
||||
export const send_a_message = 'Envoyer un message'
|
||||
|
||||
export const side = {
|
||||
@@ -18,7 +18,7 @@ export const connect = {
|
||||
password: 'Mot de passe',
|
||||
connect: 'Connexion',
|
||||
error: 'Erreur de connexion',
|
||||
empty_displayname: 'Le nom d\'affichage ne peut pas être vide.', // TODO: vérifier la traduction
|
||||
empty_displayname: "Le nom d'affichage ne peut pas être vide.", // TODO: vérifier la traduction
|
||||
}
|
||||
|
||||
export const context = {
|
||||
@@ -51,7 +51,7 @@ export const controls = {
|
||||
lock: 'Vérouiller le contrôle',
|
||||
unlock: 'Débloquer le contrôle',
|
||||
has: 'Vous avez le contrôle', // TODO: vérifier la traduction
|
||||
hasnot: 'Vous n\'avez pas le contrôle', // TODO: vérifier la traduction
|
||||
hasnot: "Vous n'avez pas le contrôle", // TODO: vérifier la traduction
|
||||
}
|
||||
|
||||
export const locks = {
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'Téléchargements',
|
||||
uploads: 'Envois',
|
||||
upload_here: 'Cliquez ou faites glisser les fichiers ici pour les envoyer',
|
||||
delete: 'Supprimer',
|
||||
delete_title: 'Supprimer "{name}" ?',
|
||||
delete_confirm: 'Voulez-vous vraiment supprimer ce fichier ?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'Unduhan',
|
||||
uploads: 'Unggahan',
|
||||
upload_here: 'Klik atau seret berkas ke sini untuk mengunggah',
|
||||
delete: 'Hapus',
|
||||
delete_title: 'Hapus "{name}"?',
|
||||
delete_confirm: 'Apakah Anda benar-benar ingin menghapus berkas ini?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'ダウンロード',
|
||||
uploads: 'アップロード',
|
||||
upload_here: 'アップロードするにはここをクリックするかファイルをドラッグしてください',
|
||||
delete: '削除',
|
||||
delete_title: '"{name}" を削除しますか?',
|
||||
delete_confirm: '本当にこのファイルを削除しますか?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: '다운로드',
|
||||
uploads: '업로드',
|
||||
upload_here: '업로드할 파일을 여기로 클릭하거나 드래그하세요',
|
||||
delete: '삭제',
|
||||
delete_title: '"{name}" 을(를) 삭제하시겠습니까?',
|
||||
delete_confirm: '정말로 이 파일을 삭제하시겠습니까?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'Nedlastinger',
|
||||
uploads: 'Opplastinger',
|
||||
upload_here: 'Klikk eller dra filer hit for å laste opp',
|
||||
delete: 'Slett',
|
||||
delete_title: 'Slette "{name}"?',
|
||||
delete_confirm: 'Er du sikker på at du vil slette denne filen?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'Pobrane pliki',
|
||||
uploads: 'Wysyłane pliki',
|
||||
upload_here: 'Kliknij lub przeciągnij pliki tutaj, aby przesłać',
|
||||
delete: 'Usuń',
|
||||
delete_title: 'Usunąć "{name}"?',
|
||||
delete_confirm: 'Czy na pewno chcesz usunąć ten plik?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'Загрузки',
|
||||
uploads: 'Загрузить',
|
||||
upload_here: 'Нажмите или перетащите сюда файлы для загрузки',
|
||||
delete: 'Удалить',
|
||||
delete_title: 'Удалить "{name}"?',
|
||||
delete_confirm: 'Вы действительно хотите удалить этот файл?',
|
||||
}
|
||||
|
||||
@@ -130,4 +130,7 @@ export const files = {
|
||||
downloads: 'Stiahnutia',
|
||||
uploads: 'Nahrávanie',
|
||||
upload_here: 'Kliknutím alebo pretiahnutím súborov sem ich môžete nahrať',
|
||||
delete: 'Odstrániť',
|
||||
delete_title: 'Odstrániť "{name}"?',
|
||||
delete_confirm: 'Naozaj chcete odstrániť tento súbor?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: 'Nedladdningar',
|
||||
uploads: 'Ladda upp',
|
||||
upload_here: 'Klicka eller dra filer hit för att ladda upp dem',
|
||||
delete: 'Ta bort',
|
||||
delete_title: 'Ta bort "{name}"?',
|
||||
delete_confirm: 'Vill du verkligen ta bort den här filen?',
|
||||
}
|
||||
|
||||
@@ -72,12 +72,12 @@ export const locks = {
|
||||
notif_unlocked: '房间已解锁',
|
||||
},
|
||||
file_transfer: {
|
||||
lock: '锁定文件传输(对用户)',
|
||||
unlock: '解锁文件传输(对用户)',
|
||||
locked: '文件传输已锁定(对用户)',
|
||||
unlocked: '文件传输已解锁(对用户)',
|
||||
notif_locked: '已锁定文件传输',
|
||||
notif_unlocked: '已解锁文件传输',
|
||||
lock: '锁定文件传输(对用户)',
|
||||
unlock: '解锁文件传输(对用户)',
|
||||
locked: '文件传输已锁定(对用户)',
|
||||
unlocked: '文件传输已解锁(对用户)',
|
||||
notif_locked: '已锁定文件传输',
|
||||
notif_unlocked: '已解锁文件传输',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: '下载',
|
||||
uploads: '上传',
|
||||
upload_here: '点击或拖动文件到此处上传',
|
||||
delete: '删除',
|
||||
delete_title: '删除"{name}"?',
|
||||
delete_confirm: '确定要删除此文件吗?',
|
||||
}
|
||||
|
||||
@@ -124,4 +124,7 @@ export const files = {
|
||||
downloads: '下載',
|
||||
uploads: '上傳',
|
||||
upload_here: '點選或拖曳檔案至此上傳',
|
||||
delete: '刪除',
|
||||
delete_title: '刪除「{name}」?',
|
||||
delete_confirm: '確定要刪除此檔案嗎?',
|
||||
}
|
||||
|
||||
@@ -361,11 +361,18 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
|
||||
/////////////////////////////
|
||||
// File Transfer Events
|
||||
/////////////////////////////
|
||||
protected [EVENT.FILETRANSFER.LIST]({ cwd, user_download, user_upload, files }: FileTransferListPayload) {
|
||||
protected [EVENT.FILETRANSFER.LIST]({
|
||||
cwd,
|
||||
user_download,
|
||||
user_upload,
|
||||
user_delete,
|
||||
files,
|
||||
}: FileTransferListPayload) {
|
||||
this.$accessor.files.setCwd(cwd)
|
||||
this.$accessor.files.setFileList(files)
|
||||
this.$accessor.files.setUserDownload(user_download)
|
||||
this.$accessor.files.setUserUpload(user_upload)
|
||||
this.$accessor.files.setUserDelete(user_delete)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
|
||||
@@ -206,6 +206,7 @@ export interface FileTransferListPayload {
|
||||
cwd: string
|
||||
user_download: boolean
|
||||
user_upload: boolean
|
||||
user_delete: boolean
|
||||
files: FileListItem[]
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const state = () => ({
|
||||
transfers: [] as FileTransfer[],
|
||||
userDownload: false,
|
||||
userUpload: false,
|
||||
userDelete: false,
|
||||
})
|
||||
|
||||
export const getters = getterTree(state, {
|
||||
@@ -32,6 +33,10 @@ export const mutations = mutationTree(state, {
|
||||
state.userUpload = val
|
||||
},
|
||||
|
||||
_setUserDelete(state, val: boolean) {
|
||||
state.userDelete = val
|
||||
},
|
||||
|
||||
_addTransfer(state, transfer: FileTransfer) {
|
||||
state.transfers = [...state.transfers, transfer]
|
||||
},
|
||||
@@ -60,6 +65,10 @@ export const actions = actionTree(
|
||||
accessor.files._setUserUpload(val)
|
||||
},
|
||||
|
||||
setUserDelete(store, val: boolean) {
|
||||
accessor.files._setUserDelete(val)
|
||||
},
|
||||
|
||||
addTransfer(store, transfer: FileTransfer) {
|
||||
if (transfer.status !== 'pending') {
|
||||
return
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ The file transfer plugin is a simple pre-loaded internal plugin that allows you
|
||||
},
|
||||
'filetransfer.user_download': false,
|
||||
'filetransfer.user_upload': false,
|
||||
'filetransfer.user_delete': false,
|
||||
}} />
|
||||
|
||||
- <Def id="filetransfer.enabled" /> enables the file transfer support. If set to `false`, the file transfer is disabled.
|
||||
@@ -68,6 +69,7 @@ The file transfer plugin is a simple pre-loaded internal plugin that allows you
|
||||
- <Def id="filetransfer.refresh_interval" /> refers to the interval at which the file list is refreshed.
|
||||
- <Def id="filetransfer.user_download" /> allows non-admin users to download files. If set to `false` (default), only admins can download files.
|
||||
- <Def id="filetransfer.user_upload" /> allows non-admin users to upload files. If set to `false` (default), only admins can upload files.
|
||||
- <Def id="filetransfer.user_delete" /> allows non-admin users to delete files. If set to `false` (default), only admins can delete files.
|
||||
|
||||
The file transfer plugin extends user profile and room settings by adding the following fields:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user