mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 17:15:40 -06:00
Storage: Optionally overwrite existing files (#52067)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
2090534635
commit
2f942c57e8
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
`,
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user