Storage: Optionally overwrite existing files (#52067)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Adela Almasan 2022-07-12 09:13:57 -05:00 committed by GitHub
parent 2090534635
commit 2f942c57e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 58 additions and 18 deletions

View File

@ -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);
}
});
});

View File

@ -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)

View File

@ -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}
/>
);
}

View File

@ -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) {
))}
</TabsBar>
{isFolder ? (
<FolderView path={path} listing={frame} onPathChange={setPath} view={view} />
<FolderView path={path} listing={frame} onPathChange={setPath} view={view} fileNames={fileNames} />
) : (
<FileView path={path} listing={frame} onPathChange={setPath} view={view} />
)}
@ -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';
}

View File

@ -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<File | undefined>(undefined);
const styles = useStyles2(getStyles);
const [error, setError] = useState<ErrorResponse>({ 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 (
<div>
<FileDropzone
@ -84,8 +89,20 @@ export const UploadView = ({ folder, onUpload }: Props) => {
{error.message !== '' ? <p>{error.message}</p> : Boolean(file) ? <Preview /> : <FileDropzoneCustomChildren />}
</FileDropzone>
{file && filenameExists && (
<div className={styles.alert}>
<Alert title={`${file.name} already exists`} severity="error">
<Checkbox
value={overwriteExistingFile}
onChange={() => setOverwriteExistingFile(!overwriteExistingFile)}
label="Overwrite existing file"
/>
</Alert>
</div>
)}
<ButtonGroup>
<Button className={styles.button} variant={'primary'} disabled={!file} onClick={doUpload}>
<Button className={styles.button} variant={'primary'} disabled={isUploadDisabled} onClick={doUpload}>
Upload
</Button>
</ButtonGroup>
@ -133,4 +150,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
color: ${theme.colors.text.secondary};
margin-bottom: ${theme.spacing(2)};
`,
alert: css`
padding-top: 10px;
`,
});

View File

@ -7,7 +7,7 @@ import { UploadReponse } from './types';
export interface GrafanaStorage {
get: <T = any>(path: string) => Promise<T>;
list: (path: string) => Promise<DataFrame | undefined>;
upload: (folder: string, file: File) => Promise<UploadReponse>;
upload: (folder: string, file: File, overwriteExistingFile: boolean) => Promise<UploadReponse>;
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<UploadReponse> {
async upload(folder: string, file: File, overwriteExistingFile: boolean): Promise<UploadReponse> {
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);
}