diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx index d52f5c1b2b6..81961749565 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx @@ -169,7 +169,9 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender let errors: string[] = []; rejectedFiles.map((rejectedFile) => { rejectedFile.errors.map((error) => { - errors.push(error.message); + if (errors.indexOf(error.message) === -1) { + errors.push(error.message); + } }); }); diff --git a/pkg/services/store/http.go b/pkg/services/store/http.go index a2fcb6db191..912d75f1cf2 100644 --- a/pkg/services/store/http.go +++ b/pkg/services/store/http.go @@ -75,13 +75,14 @@ func (s *httpStorage) Upload(c *models.ReqContext) response.Response { }) } - folder, ok := c.Req.MultipartForm.Value["folder"] - if !ok || len(folder) != 1 { + folder, ok := getMultipartFormValue(c.Req, "folder") + if !ok || folder == "" { return response.JSON(400, map[string]interface{}{ "message": "please specify the upload folder", "err": true, }) } + overwriteExistingFile, _ := getMultipartFormValue(c.Req, "overwriteExistingFile") fileHeader := files[0] if fileHeader.Size > MAX_UPLOAD_SIZE { @@ -107,7 +108,7 @@ func (s *httpStorage) Upload(c *models.ReqContext) response.Response { return errFileTooBig } - path := folder[0] + "/" + fileHeader.Filename + path := folder + "/" + fileHeader.Filename mimeType := http.DetectContentType(data) @@ -116,7 +117,7 @@ func (s *httpStorage) Upload(c *models.ReqContext) response.Response { MimeType: mimeType, EntityType: EntityTypeImage, Path: path, - OverwriteExistingFile: true, + OverwriteExistingFile: overwriteExistingFile == "true", }) if err != nil { @@ -131,6 +132,14 @@ func (s *httpStorage) Upload(c *models.ReqContext) response.Response { }) } +func getMultipartFormValue(req *http.Request, key string) (string, bool) { + v, ok := req.MultipartForm.Value[key] + if !ok || len(v) != 1 { + return "", false + } + return v[0], ok +} + func (s *httpStorage) Read(c *models.ReqContext) response.Response { // full path is api/storage/read/upload/example.jpg, but we only want the part after read scope, path := getPathAndScope(c) diff --git a/public/app/features/storage/FolderView.tsx b/public/app/features/storage/FolderView.tsx index 9a10b8beaf1..02d5d7a6545 100644 --- a/public/app/features/storage/FolderView.tsx +++ b/public/app/features/storage/FolderView.tsx @@ -13,9 +13,10 @@ interface Props { path: string; onPathChange: (p: string, view?: StorageView) => void; view: StorageView; + fileNames: string[]; } -export function FolderView({ listing, path, onPathChange, view }: Props) { +export function FolderView({ listing, path, onPathChange, view, fileNames }: Props) { const styles = useStyles2(getStyles); switch (view) { @@ -35,6 +36,7 @@ export function FolderView({ listing, path, onPathChange, view }: Props) { onPathChange(path); // back to data } }} + fileNames={fileNames} /> ); } diff --git a/public/app/features/storage/StoragePage.tsx b/public/app/features/storage/StoragePage.tsx index 64639923997..81f94cf045d 100644 --- a/public/app/features/storage/StoragePage.tsx +++ b/public/app/features/storage/StoragePage.tsx @@ -18,7 +18,7 @@ import { ExportView } from './ExportView'; import { FileView } from './FileView'; import { FolderView } from './FolderView'; import { RootView } from './RootView'; -import { getGrafanaStorage } from './helper'; +import { getGrafanaStorage, filenameAlreadyExists } from './helper'; import { StorageView } from './types'; interface RouteParams { @@ -211,7 +211,7 @@ export default function StoragePage(props: Props) { ))} {isFolder ? ( - + ) : ( )} @@ -231,10 +231,8 @@ export default function StoragePage(props: Props) { }} validate={(folderName) => { const lowerCase = folderName.toLowerCase(); - const trimmedLowerCase = lowerCase.trim(); - const existingTrimmedLowerCaseNames = fileNames.map((f) => f.trim().toLowerCase()); - if (existingTrimmedLowerCaseNames.includes(trimmedLowerCase)) { + if (filenameAlreadyExists(folderName, fileNames)) { return 'A file or a folder with the same name already exists'; } diff --git a/public/app/features/storage/UploadView.tsx b/public/app/features/storage/UploadView.tsx index 370f5370666..c201268f445 100644 --- a/public/app/features/storage/UploadView.tsx +++ b/public/app/features/storage/UploadView.tsx @@ -3,14 +3,15 @@ import React, { useState } from 'react'; import SVG from 'react-inlinesvg'; import { GrafanaTheme2 } from '@grafana/data'; -import { Button, ButtonGroup, Field, FileDropzone, useStyles2 } from '@grafana/ui'; +import { Alert, Button, ButtonGroup, Checkbox, Field, FileDropzone, useStyles2 } from '@grafana/ui'; -import { getGrafanaStorage } from './helper'; +import { filenameAlreadyExists, getGrafanaStorage } from './helper'; import { UploadReponse } from './types'; interface Props { folder: string; onUpload: (rsp: UploadReponse) => void; + fileNames: string[]; } interface ErrorResponse { @@ -27,12 +28,13 @@ const FileDropzoneCustomChildren = ({ secondaryText = 'Drag and drop here or bro ); }; -export const UploadView = ({ folder, onUpload }: Props) => { +export const UploadView = ({ folder, onUpload, fileNames }: Props) => { const [file, setFile] = useState(undefined); const styles = useStyles2(getStyles); const [error, setError] = useState({ message: '' }); + const [overwriteExistingFile, setOverwriteExistingFile] = useState(false); const Preview = () => { if (!file) { @@ -58,7 +60,7 @@ export const UploadView = ({ folder, onUpload }: Props) => { return; } - const rsp = await getGrafanaStorage().upload(folder, file); + const rsp = await getGrafanaStorage().upload(folder, file, overwriteExistingFile); if (rsp.status !== 200) { setError(rsp); } else { @@ -66,6 +68,9 @@ export const UploadView = ({ folder, onUpload }: Props) => { } }; + const filenameExists = file ? filenameAlreadyExists(file.name, fileNames) : false; + const isUploadDisabled = !file || (filenameExists && !overwriteExistingFile); + return ( { {error.message !== '' ? {error.message} : Boolean(file) ? : } + {file && filenameExists && ( + + + setOverwriteExistingFile(!overwriteExistingFile)} + label="Overwrite existing file" + /> + + + )} + - + Upload @@ -133,4 +150,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ color: ${theme.colors.text.secondary}; margin-bottom: ${theme.spacing(2)}; `, + alert: css` + padding-top: 10px; + `, }); diff --git a/public/app/features/storage/helper.ts b/public/app/features/storage/helper.ts index 366fe02867a..51f01daf3e9 100644 --- a/public/app/features/storage/helper.ts +++ b/public/app/features/storage/helper.ts @@ -7,7 +7,7 @@ import { UploadReponse } from './types'; export interface GrafanaStorage { get: (path: string) => Promise; list: (path: string) => Promise; - upload: (folder: string, file: File) => Promise; + upload: (folder: string, file: File, overwriteExistingFile: boolean) => Promise; createFolder: (path: string) => Promise<{ error?: string }>; delete: (path: { isFolder: boolean; path: string }) => Promise<{ error?: string }>; } @@ -82,10 +82,11 @@ class SimpleStorage implements GrafanaStorage { return req.isFolder ? this.deleteFolder({ path: req.path, force: true }) : this.deleteFile({ path: req.path }); } - async upload(folder: string, file: File): Promise { + async upload(folder: string, file: File, overwriteExistingFile: boolean): Promise { const formData = new FormData(); formData.append('folder', folder); formData.append('file', file); + formData.append('overwriteExistingFile', String(overwriteExistingFile)); const res = await fetch('/api/storage/upload', { method: 'POST', body: formData, @@ -112,3 +113,11 @@ export function getGrafanaStorage() { } return storage; } + +export function filenameAlreadyExists(folderName: string, fileNames: string[]) { + const lowerCase = folderName.toLowerCase(); + const trimmedLowerCase = lowerCase.trim(); + const existingTrimmedLowerCaseNames = fileNames.map((f) => f.trim().toLowerCase()); + + return existingTrimmedLowerCaseNames.includes(trimmedLowerCase); +}
{error.message}