Added rewind functionality to console (#828)

* Added rewind functionality to console

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>

* Fix for object details

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Alex
2021-06-23 01:10:54 -05:00
committed by GitHub
parent fd86e65e5e
commit 52075681c3
29 changed files with 1761 additions and 526 deletions

View File

@@ -31,7 +31,7 @@ ENV CGO_ENABLED=0
COPY --from=uilayer /app/build /go/src/github.com/minio/console/portal-ui/build
RUN go build -ldflags "-w -s" -a -o console ./cmd/console
FROM scratch
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3
MAINTAINER MinIO Development "dev@min.io"
EXPOSE 9090

2
go.mod
View File

@@ -20,7 +20,7 @@ require (
github.com/minio/kes v0.11.0
github.com/minio/madmin-go v1.0.12
github.com/minio/mc v0.0.0-20210531030240-fbbae711bdb4
github.com/minio/minio-go/v7 v7.0.11-0.20210517200026-f0518ca447d6
github.com/minio/minio-go/v7 v7.0.12-0.20210617160455-b7103728fb87
github.com/minio/operator v0.0.0-20210616045941-65f31f5f78ae
github.com/minio/operator/logsearchapi v0.0.0-20210604224119-7e256f98cf90
github.com/minio/pkg v1.0.6

456
go.sum

File diff suppressed because it is too large Load Diff

82
models/rewind_item.go Normal file
View File

@@ -0,0 +1,82 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// RewindItem rewind item
//
// swagger:model rewindItem
type RewindItem struct {
// action
Action string `json:"action,omitempty"`
// delete flag
DeleteFlag bool `json:"delete_flag,omitempty"`
// last modified
LastModified string `json:"last_modified,omitempty"`
// name
Name string `json:"name,omitempty"`
// size
Size int64 `json:"size,omitempty"`
// version id
VersionID string `json:"version_id,omitempty"`
}
// Validate validates this rewind item
func (m *RewindItem) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this rewind item based on context it is used
func (m *RewindItem) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *RewindItem) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *RewindItem) UnmarshalBinary(b []byte) error {
var res RewindItem
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

129
models/rewind_response.go Normal file
View File

@@ -0,0 +1,129 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// RewindResponse rewind response
//
// swagger:model rewindResponse
type RewindResponse struct {
// objects
Objects []*RewindItem `json:"objects"`
}
// Validate validates this rewind response
func (m *RewindResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateObjects(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *RewindResponse) validateObjects(formats strfmt.Registry) error {
if swag.IsZero(m.Objects) { // not required
return nil
}
for i := 0; i < len(m.Objects); i++ {
if swag.IsZero(m.Objects[i]) { // not required
continue
}
if m.Objects[i] != nil {
if err := m.Objects[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("objects" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this rewind response based on the context it is used
func (m *RewindResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateObjects(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *RewindResponse) contextValidateObjects(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.Objects); i++ {
if m.Objects[i] != nil {
if err := m.Objects[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("objects" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *RewindResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *RewindResponse) UnmarshalBinary(b []byte) error {
var res RewindResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -26,14 +26,12 @@ const CreateIcon = () => {
width="2"
height="12"
transform="translate(-997 2555)"
fill="#fff"
/>
<rect
id="Rectangle_30"
width="2"
height="12"
transform="translate(-990 2560) rotate(90)"
fill="#fff"
/>
</g>
</svg>

View File

@@ -15,12 +15,22 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useRef, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { withRouter } from "react-router-dom";
import Grid from "@material-ui/core/Grid";
import get from "lodash/get";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { BucketObject, BucketObjectsList } from "./types";
import RefreshIcon from "@material-ui/icons/Refresh";
import RestoreIcon from "@material-ui/icons/Restore";
import {
BucketObject,
BucketObjectsList,
RewindObject,
RewindObjectList,
} from "./types";
import api from "../../../../../../common/api";
import TableWrapper from "../../../../Common/TableWrapper/TableWrapper";
import { niceBytes } from "../../../../../../common/utils";
@@ -33,20 +43,24 @@ import {
searchField,
} from "../../../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../../../Common/PageHeader/PageHeader";
import { Button, IconButton, Input, Typography } from "@material-ui/core";
import {
Badge,
Button,
IconButton,
Input,
Typography,
} from "@material-ui/core";
import * as reactMoment from "react-moment";
import { CreateIcon } from "../../../../../../icons";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
import get from "lodash/get";
import { withRouter } from "react-router-dom";
import {
addRoute,
setAllRoutes,
setLastAsFile,
fileIsBeingPrepared,
fileDownloadStarted,
resetRewind,
} from "../../../../ObjectBrowser/actions";
import { connect } from "react-redux";
import {
ObjectBrowserReducer,
Route,
@@ -59,7 +73,8 @@ import {
setSnackBarMessage,
setErrorSnackMessage,
} from "../../../../../../actions";
import RefreshIcon from "@material-ui/icons/Refresh";
import { BucketVersioning } from "../../../types";
import RewindEnable from "./RewindEnable";
const commonIcon = {
backgroundRepeat: "no-repeat",
@@ -133,6 +148,12 @@ const styles = (theme: Theme) =>
listButton: {
marginLeft: "10px",
},
badgeOverlap: {
"& .MuiBadge-badge": {
top: 35,
right: 10,
},
},
...actionsTray,
...searchField,
...objectBrowserCommon,
@@ -147,11 +168,15 @@ interface IListObjectsProps {
routesList: Route[];
downloadingFiles: string[];
setLastAsFile: () => any;
rewindEnabled: boolean;
rewindDate: any;
bucketToRewind: string;
setLoadingProgress: typeof setLoadingProgress;
setSnackBarMessage: typeof setSnackBarMessage;
setErrorSnackMessage: typeof setErrorSnackMessage;
fileIsBeingPrepared: typeof fileIsBeingPrepared;
fileDownloadStarted: typeof fileDownloadStarted;
resetRewind: typeof resetRewind;
}
function useInterval(callback: any, delay: number) {
@@ -185,15 +210,21 @@ const ListObjects = ({
setAllRoutes,
routesList,
downloadingFiles,
rewindEnabled,
rewindDate,
bucketToRewind,
setLastAsFile,
setLoadingProgress,
setSnackBarMessage,
setErrorSnackMessage,
fileIsBeingPrepared,
fileDownloadStarted,
resetRewind,
}: IListObjectsProps) => {
const [records, setRecords] = useState<BucketObject[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [rewind, setRewind] = useState<RewindObject[]>([]);
const [loadingRewind, setLoadingRewind] = useState<boolean>(true);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
const [selectedObject, setSelectedObject] = useState<string>("");
@@ -202,6 +233,11 @@ const ListObjects = ({
const [loadingStartTime, setLoadingStartTime] = useState<number>(0);
const [loadingMessage, setLoadingMessage] =
useState<React.ReactNode>(defLoading);
const [loadingVersioning, setLoadingVersioning] = useState<boolean>(true);
const [isVersioned, setIsVersioned] = useState<boolean>(false);
const [rewindSelect, setRewindSelect] = useState<boolean>(false);
const bucketName = match.params["bucket"];
const updateMessage = () => {
let timeDelta = Date.now() - loadingStartTime;
@@ -232,31 +268,113 @@ const ListObjects = ({
}, 1000);
useEffect(() => {
const bucketName = match.params["bucket"];
if (loadingVersioning) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
.then((res: BucketVersioning) => {
setIsVersioned(res.is_versioned);
setLoadingVersioning(false);
})
.catch((err: any) => {
setErrorSnackMessage(err);
setLoadingVersioning(false);
});
}
}, [bucketName, loadingVersioning, setErrorSnackMessage]);
// Rewind
useEffect(() => {
const internalPaths = match.params[0];
if (rewindEnabled) {
if (bucketToRewind !== bucketName) {
resetRewind();
return;
}
if (rewindDate) {
setLoadingRewind(true);
const rewindParsed = rewindDate.toISOString();
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}?prefix=${
internalPaths ? `${internalPaths}/` : ""
}`
)
.then((res: RewindObjectList) => {
setLoadingRewind(false);
if (res.objects) {
setRewind(res.objects);
} else {
setRewind([]);
}
})
.catch((err: any) => {
setLoadingRewind(false);
setErrorSnackMessage(err);
});
}
}
}, [
rewindEnabled,
rewindDate,
bucketToRewind,
bucketName,
match,
setErrorSnackMessage,
resetRewind,
]);
useEffect(() => {
const internalPaths = match.params[0];
const verifyIfIsFile = () => {
const bucketName = match.params["bucket"];
const internalPaths = match.params[0];
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${internalPaths}`
)
.then((res: BucketObjectsList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (res.objects !== null) {
setLastAsFile();
} else {
// It is a folder, we remove loader
if (rewindEnabled) {
const rewindParsed = rewindDate.toISOString();
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}?prefix=${
internalPaths ? `${internalPaths}/` : ""
}`
)
.then((res: RewindObjectList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (res.objects === null) {
setLastAsFile();
} else {
// It is a folder, we remove loader
setLoadingRewind(false);
setLoading(false);
}
})
.catch((err: any) => {
setLoadingRewind(false);
setLoading(false);
}
})
.catch((err: any) => {
setLoading(false);
setErrorSnackMessage(err);
});
setErrorSnackMessage(err);
});
} else {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${internalPaths}`
)
.then((res: BucketObjectsList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (res.objects !== null) {
setLastAsFile();
} else {
// It is a folder, we remove loader
setLoading(false);
}
})
.catch((err: any) => {
setLoading(false);
setErrorSnackMessage(err);
});
}
};
if (loading) {
@@ -303,7 +421,15 @@ const ListObjects = ({
setErrorSnackMessage(err);
});
}
}, [loading, match, setLastAsFile, setErrorSnackMessage]);
}, [
loading,
match,
setLastAsFile,
setErrorSnackMessage,
bucketName,
rewindEnabled,
rewindDate,
]);
useEffect(() => {
const url = get(match, "url", "/object-browser");
@@ -421,6 +547,10 @@ const ListObjects = ({
fileDownloadStarted(path);
};
const displayDeleteFlag = (state: boolean) => {
return state ? "Yes" : "No";
};
const downloadObject = (object: BucketObject) => {
fileIsBeingPrepared(`${selectedBucket}/${object.name}`);
if (object.size > 104857600) {
@@ -484,8 +614,26 @@ const ListObjects = ({
onClick: downloadObject,
showLoaderFunction: (item: string) =>
downloadingFiles.includes(`${match.params["bucket"]}/${item}`),
disableButtonFunction: (item: string) => {
if (rewindEnabled) {
const element = rewind.find((elm) => elm.name === item);
if (element && element.delete_flag) {
return true;
}
}
return false;
},
sendOnlyId: false,
},
{
type: "delete",
onClick: confirmDeleteObject,
sendOnlyId: true,
disableButtonFunction: () => {
return rewindEnabled;
},
},
{ type: "delete", onClick: confirmDeleteObject, sendOnlyId: true },
];
const displayName = (element: string) => {
@@ -521,6 +669,64 @@ const ListObjects = ({
}
});
const rewindCloseModal = (refresh: boolean) => {
setRewindSelect(false);
if (refresh) {
}
};
const listModeColumns = [
{
label: "Name",
elementKey: "name",
renderFunction: displayName,
},
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
renderFullObject: true,
},
{
label: "Size",
elementKey: "size",
renderFunction: displayNiceBytes,
renderFullObject: true,
width: 60,
contentTextAlign: "right",
},
];
const rewindModeColumns = [
{
label: "Name",
elementKey: "name",
renderFunction: displayName,
},
{
label: "Object Date",
elementKey: "last_modified",
renderFunction: displayParsedDate,
renderFullObject: true,
},
{
label: "Size",
elementKey: "size",
renderFunction: displayNiceBytes,
renderFullObject: true,
width: 60,
contentTextAlign: "right",
},
{
label: "Deleted",
elementKey: "delete_flag",
renderFunction: displayDeleteFlag,
width: 60,
contentTextAlign: "center",
},
];
return (
<React.Fragment>
{deleteOpen && (
@@ -538,6 +744,13 @@ const ListObjects = ({
onClose={closeAddFolderModal}
/>
)}
{rewindSelect && (
<RewindEnable
open={rewindSelect}
closeModalAndRefresh={rewindCloseModal}
bucketName={bucketName}
/>
)}
<PageHeader label="Object Browser" />
<Grid container>
<Grid item xs={12} className={classes.container}>
@@ -571,9 +784,29 @@ const ListObjects = ({
onClick={() => {
setLoading(true);
}}
disabled={rewindEnabled}
>
<RefreshIcon />
</IconButton>
<Badge
badgeContent=" "
color="secondary"
variant="dot"
invisible={!rewindEnabled}
className={classes.badgeOverlap}
>
<IconButton
color="primary"
aria-label="Rewind"
component="span"
onClick={() => {
setRewindSelect(true);
}}
disabled={!isVersioned}
>
<RestoreIcon />
</IconButton>
</Badge>
<Button
variant="contained"
color="primary"
@@ -583,6 +816,7 @@ const ListObjects = ({
setCreateFolderOpen(true);
}}
className={classes.listButton}
disabled={rewindEnabled}
>
Create Folder
</Button>
@@ -592,6 +826,7 @@ const ListObjects = ({
startIcon={<UploadFile />}
component="label"
className={classes.listButton}
disabled={rewindEnabled}
>
File
<Input
@@ -609,32 +844,12 @@ const ListObjects = ({
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "Name",
elementKey: "name",
renderFunction: displayName,
},
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
renderFullObject: true,
},
{
label: "Size",
elementKey: "size",
renderFunction: displayNiceBytes,
renderFullObject: true,
width: 60,
contentTextAlign: "right",
},
]}
isLoading={loading}
columns={rewindEnabled ? rewindModeColumns : listModeColumns}
isLoading={rewindEnabled ? loadingRewind : loading}
loadingMessage={loadingMessage}
entityName="Objects"
entityName="Rewind Objects"
idField="name"
records={filteredRecords}
records={rewindEnabled ? rewind : filteredRecords}
customPaperHeight={classes.browsePaper}
/>
</Grid>
@@ -647,6 +862,9 @@ const ListObjects = ({
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
routesList: get(objectBrowser, "routesList", []),
downloadingFiles: get(objectBrowser, "downloadingFiles", []),
rewindEnabled: get(objectBrowser, "rewind.rewindEnabled", false),
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
});
const mapDispatchToProps = {
@@ -658,6 +876,7 @@ const mapDispatchToProps = {
setErrorSnackMessage,
fileIsBeingPrepared,
fileDownloadStarted,
resetRewind,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -0,0 +1,153 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import { ObjectBrowserReducer } from "../../../../ObjectBrowser/reducers";
import { modalBasic } from "../../../../Common/FormComponents/common/styleLibrary";
import {
resetRewind,
setRewindEnable,
} from "../../../../ObjectBrowser/actions";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import DateTimePickerWrapper from "../../../../Common/FormComponents/DateTimePickerWrapper/DateTimePickerWrapper";
import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
interface IRewindEnable {
closeModalAndRefresh: (reload: boolean) => void;
classes: any;
open: boolean;
bucketName: string;
bucketToRewind: string;
rewindEnabled: boolean;
dateRewind: any;
resetRewind: typeof resetRewind;
setRewindEnable: typeof setRewindEnable;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
const RewindEnable = ({
closeModalAndRefresh,
classes,
open,
bucketName,
bucketToRewind,
rewindEnabled,
dateRewind,
resetRewind,
setRewindEnable,
}: IRewindEnable) => {
const [rewindEnabling, setRewindEnabling] = useState<boolean>(false);
const [rewindEnableButton, setRewindEnableButton] = useState<boolean>(true);
const [dateSelected, setDateSelected] = useState<any>(null);
useEffect(() => {
if (rewindEnabled) {
setRewindEnableButton(true);
setDateSelected(new Date(dateRewind));
}
}, [rewindEnabled, dateRewind]);
const rewindApply = () => {
if (!rewindEnableButton && rewindEnabled) {
resetRewind();
} else {
setRewindEnabling(true);
setRewindEnable(true, bucketName, dateSelected);
}
closeModalAndRefresh(true);
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh(false);
}}
title={`Rewind - ${bucketName}`}
>
<Grid item xs={12}>
<DateTimePickerWrapper
value={dateSelected}
onChange={setDateSelected}
id="rewind-selector"
label="Rewind to"
disabled={!rewindEnableButton}
/>
</Grid>
<Grid container>
{rewindEnabled && (
<Grid item xs={12}>
<FormSwitchWrapper
value="status"
id="status"
name="status"
checked={rewindEnableButton}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRewindEnableButton(false);
}}
label={"Current Status"}
indicatorLabels={["Enabled", "Disabled"]}
/>
</Grid>
)}
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="button"
variant="contained"
color="primary"
disabled={rewindEnabling || (!dateSelected && rewindEnableButton)}
onClick={rewindApply}
>
{!rewindEnableButton && rewindEnabled
? "Show Current Data"
: "Show Rewind Data"}
</Button>
</Grid>
{rewindEnabling && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</ModalWrapper>
);
};
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
bucketToRewind: objectBrowser.rewind.bucketToRewind,
rewindEnabled: objectBrowser.rewind.rewindEnabled,
dateRewind: objectBrowser.rewind.dateToRewind,
});
const mapDispatchToProps = {
resetRewind,
setRewindEnable,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withStyles(styles)(connector(RewindEnable));

View File

@@ -26,3 +26,15 @@ export interface BucketObjectsList {
objects: BucketObject[];
total: number;
}
export interface RewindObject {
last_modified: string;
delete_flag: boolean;
name: string;
version_id: string;
size: number;
}
export interface RewindObjectList {
objects: RewindObject[];
}

View File

@@ -51,7 +51,9 @@ import PageHeader from "../../../../Common/PageHeader/PageHeader";
import ShareIcon from "../../../../../../icons/ShareIcon";
import DownloadIcon from "../../../../../../icons/DownloadIcon";
import DeleteIcon from "../../../../../../icons/DeleteIcon";
import TableWrapper from "../../../../Common/TableWrapper/TableWrapper";
import TableWrapper, {
ItemActions,
} from "../../../../Common/TableWrapper/TableWrapper";
import PencilIcon from "../../../../Common/TableWrapper/TableActionIcons/PencilIcon";
import SetRetention from "./SetRetention";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
@@ -162,6 +164,9 @@ interface IObjectDetailsProps {
classes: any;
routesList: Route[];
downloadingFiles: string[];
rewindEnabled: boolean;
rewindDate: any;
bucketToRewind: string;
removeRouteLevel: (newRoute: string) => any;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSnackBarMessage: typeof setSnackBarMessage;
@@ -185,6 +190,9 @@ const ObjectDetails = ({
classes,
routesList,
downloadingFiles,
rewindEnabled,
rewindDate,
bucketToRewind,
removeRouteLevel,
setErrorSnackMessage,
setSnackBarMessage,
@@ -221,7 +229,7 @@ const ObjectDetails = ({
setActualInfo(
result.find((el: IFileInfo) => el.is_latest) || emptyFile
);
setVersions(result.filter((el: IFileInfo) => !el.is_latest));
setVersions(result);
setLoadObjectData(false);
})
.catch((error) => {
@@ -286,8 +294,19 @@ const ObjectDetails = ({
);
};
const tableActions = [
{ type: "share", onClick: shareObject, sendOnlyId: true },
const tableActions: ItemActions[] = [
{
type: "share",
onClick: shareObject,
sendOnlyId: true,
disableButtonFunction: (item: string) => {
const element = versions.find((elm) => elm.version_id === item);
if (element && element.is_delete_marker) {
return true;
}
return false;
},
},
{
type: "download",
onClick: (item: IFileInfo) => {
@@ -298,6 +317,13 @@ const ObjectDetails = ({
`${bucketName}/${objectName}-${version}`
);
},
disableButtonFunction: (item: string) => {
const element = versions.find((elm) => elm.version_id === item);
if (element && element.is_delete_marker) {
return true;
}
return false;
},
},
];
@@ -479,6 +505,7 @@ const ObjectDetails = ({
onClick={() => {
shareObject();
}}
disabled={actualInfo.is_delete_marker}
>
<ShareIcon />
</IconButton>
@@ -503,6 +530,7 @@ const ObjectDetails = ({
onClick={() => {
downloadObject(actualInfo);
}}
disabled={actualInfo.is_delete_marker}
>
<DownloadIcon />
</IconButton>
@@ -517,6 +545,7 @@ const ObjectDetails = ({
onClick={() => {
setDeleteOpen(true);
}}
disabled={actualInfo.is_delete_marker}
>
<DeleteIcon />
</IconButton>
@@ -585,12 +614,31 @@ const ObjectDetails = ({
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "",
width: 20,
renderFullObject: true,
renderFunction: (r) => {
const versOrd = versions.length - versions.indexOf(r);
return `v${versOrd}`;
},
},
{ label: "Version ID", elementKey: "version_id" },
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Deleted",
width: 60,
contentTextAlign: "center",
renderFullObject: true,
renderFunction: (r) => {
const versOrd = r.is_delete_marker ? "Yes" : "No";
return `${versOrd}`;
},
},
]}
isLoading={false}
entityName="Versions"
@@ -607,6 +655,9 @@ const ObjectDetails = ({
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
downloadingFiles: get(objectBrowser, "downloadingFiles", []),
rewindEnabled: get(objectBrowser, "rewind.rewindEnabled", false),
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
});
const mapDispatchToProps = {

View File

@@ -24,4 +24,5 @@ export interface IFileInfo {
size?: string;
tags?: object;
version_id: string | null;
is_delete_marker?: boolean;
}

View File

@@ -14,17 +14,26 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import React, { Fragment } from "react";
import MomentUtils from "@date-io/moment";
import { Grid, InputLabel, Tooltip } from "@material-ui/core";
import { DateTimePicker, MuiPickersUtilsProvider } from "@material-ui/pickers";
import InputAdornment from "@material-ui/core/InputAdornment";
import ScheduleIcon from "@material-ui/icons/Schedule";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import HelpIcon from "../../../../../icons/HelpIcon";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
interface IDateTimePicker {
value: any;
onChange: (value: any) => any;
classes: any;
forSearchBlock?: boolean;
label?: string;
required?: boolean;
tooltip?: string;
id: string;
disabled?: boolean;
}
const styles = (theme: Theme) =>
@@ -56,17 +65,36 @@ const styles = (theme: Theme) =>
color: "#393939",
},
},
dateSelectorFormOverride: {
width: "100%",
maxWidth: 840,
},
parentDateOverride: {
flexGrow: 1,
},
textBoxContainer: {
flexGrow: 1,
},
textBoxWithIcon: {
position: "relative",
paddingRight: 25,
},
...fieldBasic,
...tooltipHelper,
});
const DateTimePickerWrapper = ({
value,
onChange,
classes,
forSearchBlock = false,
label,
tooltip = "",
required,
id,
disabled = false,
}: IDateTimePicker) => {
return (
const inputItem = (
<MuiPickersUtilsProvider utils={MomentUtils}>
<DateTimePicker
value={value}
@@ -77,16 +105,52 @@ const DateTimePickerWrapper = ({
<ScheduleIcon />
</InputAdornment>
),
className: classes.dateSelectorOverride,
className: forSearchBlock ? classes.dateSelectorOverride : "",
}}
label=""
ampm={false}
variant={"inline"}
className={classes.parentDateOverride}
className={
forSearchBlock
? classes.parentDateOverride
: classes.dateSelectorFormOverride
}
format="MMMM Do YYYY, h:mm a"
id={id}
disabled={disabled}
/>
</MuiPickersUtilsProvider>
);
if (forSearchBlock) {
return inputItem;
}
return (
<Fragment>
<Grid item xs={12} className={`${classes.fieldContainer}`}>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>
{label}
{required ? "*" : ""}
</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<div>
<HelpIcon className={classes.tooltip} />
</div>
</Tooltip>
</div>
)}
</InputLabel>
)}
<div className={classes.textBoxContainer}>{inputItem}</div>
</Grid>
</Fragment>
);
};
export default withStyles(styles)(DateTimePickerWrapper);

View File

@@ -259,6 +259,10 @@ export const objectBrowserCommon = {
},
},
},
smallLabel: {
color: "#9C9C9C",
fontSize: 15,
},
};
export const selectorsCommon = {

View File

@@ -41,7 +41,7 @@ import CheckboxWrapper from "../FormComponents/CheckboxWrapper/CheckboxWrapper";
//Interfaces for table Items
interface ItemActions {
export interface ItemActions {
type: string;
to?: string;
sendOnlyId?: boolean;

View File

@@ -259,9 +259,19 @@ const PrDashboard = ({
className={`${classes.actionsTray} ${classes.timeContainers}`}
>
<span className={classes.label}>Start Time</span>
<DateTimePickerWrapper value={timeStart} onChange={setTimeStart} />
<DateTimePickerWrapper
value={timeStart}
onChange={setTimeStart}
forSearchBlock
id="stTime"
/>
<span className={classes.label}>End Time</span>
<DateTimePickerWrapper value={timeEnd} onChange={setTimeEnd} />
<DateTimePickerWrapper
value={timeEnd}
onChange={setTimeEnd}
forSearchBlock
id="endTime"
/>
<Button
type="button"
variant="contained"

View File

@@ -260,9 +260,19 @@ const LogsSearchMain = ({ classes, setErrorSnackMessage }: ILogSearchProps) => {
className={`${classes.actionsTray} ${classes.timeContainers}`}
>
<span className={classes.label}>Start Time</span>
<DateTimePickerWrapper value={timeStart} onChange={setTimeStart} />
<DateTimePickerWrapper
value={timeStart}
onChange={setTimeStart}
forSearchBlock
id="stTime"
/>
<span className={classes.label}>End Time</span>
<DateTimePickerWrapper value={timeEnd} onChange={setTimeEnd} />
<DateTimePickerWrapper
value={timeEnd}
onChange={setTimeEnd}
forSearchBlock
id="endTime"
/>
</Grid>
<Grid item xs={12} className={`${classes.advancedLabelContainer}`}>
<div

View File

@@ -17,6 +17,7 @@
import React from "react";
import get from "lodash/get";
import Grid from "@material-ui/core/Grid";
import Moment from "react-moment";
import { connect } from "react-redux";
import { withStyles } from "@material-ui/core";
import { createStyles, Theme } from "@material-ui/core/styles";
@@ -32,6 +33,8 @@ interface ObjectBrowserReducer {
interface IObjectBrowser {
classes: any;
objectsList: Route[];
rewindEnabled: boolean;
rewindDate: any;
removeRouteLevel: (path: string) => any;
}
@@ -43,6 +46,8 @@ const styles = (theme: Theme) =>
const BrowserBreadcrumbs = ({
classes,
objectsList,
rewindEnabled,
rewindDate,
removeRouteLevel,
}: IObjectBrowser) => {
const listBreadcrumbs = objectsList.map((objectItem, index) => {
@@ -60,7 +65,6 @@ const BrowserBreadcrumbs = ({
</React.Fragment>
);
});
return (
<React.Fragment>
<Grid item xs={12}>
@@ -68,6 +72,12 @@ const BrowserBreadcrumbs = ({
{objectsList && objectsList.length > 0
? objectsList.slice(-1)[0].label
: ""}
{rewindEnabled && objectsList.length > 1 && (
<small className={classes.smallLabel}>
&nbsp;(Rewind:{" "}
<Moment date={rewindDate} format="MMMM Do YYYY, h:mm a" /> )
</small>
)}
</div>
</Grid>
<Grid item xs={12} className={classes.breadcrumbs}>
@@ -79,6 +89,8 @@ const BrowserBreadcrumbs = ({
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
objectsList: get(objectBrowser, "routesList", []),
rewindEnabled: get(objectBrowser, "rewind.rewindEnabled", false),
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
});
const mapDispatchToProps = {

View File

@@ -26,6 +26,8 @@ export const OBJECT_BROWSER_SET_LAST_AS_FILE =
export const OBJECT_BROWSER_DOWNLOAD_FILE_LOADER =
"OBJECT_BROWSER/DOWNLOAD_FILE_LOADER";
export const OBJECT_BROWSER_DOWNLOADED_FILE = "OBJECT_BROWSER/DOWNLOADED_FILE";
export const REWIND_SET_ENABLE = "REWIND/SET_ENABLE";
export const REWIND_RESET_REWIND = "REWIND/RESET_REWIND";
interface AddRouteAction {
type: typeof OBJECT_BROWSER_ADD_ROUTE;
@@ -68,6 +70,17 @@ interface FileDownloaded {
path: string;
}
interface RewindSetEnabled {
type: typeof REWIND_SET_ENABLE;
bucket: string;
state: boolean;
dateRewind: any;
}
interface RewindReset {
type: typeof REWIND_RESET_REWIND;
}
export type ObjectBrowserActionTypes =
| AddRouteAction
| ResetRoutesList
@@ -76,7 +89,9 @@ export type ObjectBrowserActionTypes =
| CreateFolder
| SetLastAsFile
| SetFileDownload
| FileDownloaded;
| FileDownloaded
| RewindSetEnabled
| RewindReset;
export const addRoute = (route: string, label: string, routeType: string) => {
return {
@@ -134,3 +149,22 @@ export const fileDownloadStarted = (path: string) => {
path,
};
};
export const setRewindEnable = (
state: boolean,
bucket: string,
dateRewind: any
) => {
return {
type: REWIND_SET_ENABLE,
state,
bucket,
dateRewind,
};
};
export const resetRewind = () => {
return {
type: REWIND_RESET_REWIND,
};
};

View File

@@ -24,6 +24,8 @@ import {
OBJECT_BROWSER_SET_LAST_AS_FILE,
OBJECT_BROWSER_DOWNLOAD_FILE_LOADER,
OBJECT_BROWSER_DOWNLOADED_FILE,
REWIND_SET_ENABLE,
REWIND_RESET_REWIND,
ObjectBrowserActionTypes,
} from "./actions";
@@ -33,9 +35,16 @@ export interface Route {
type: string;
}
export interface RewindItem {
rewindEnabled: boolean;
bucketToRewind: string;
dateToRewind: any;
}
export interface ObjectBrowserState {
routesList: Route[];
downloadingFiles: string[];
rewind: RewindItem;
}
export interface ObjectBrowserReducer {
@@ -46,9 +55,18 @@ const initialRoute = [
{ route: "/object-browser", label: "All Buckets", type: "path" },
];
const defaultRewind = {
rewindEnabled: false,
bucketToRewind: "",
dateToRewind: null,
};
const initialState: ObjectBrowserState = {
routesList: initialRoute,
downloadingFiles: [],
rewind: {
...defaultRewind,
},
};
export function objectBrowserReducer(
@@ -157,6 +175,21 @@ export function objectBrowserReducer(
...state,
downloadingFiles: [...downloadingFiles],
};
case REWIND_SET_ENABLE:
const rewindSetEnabled = {
...state.rewind,
rewindEnabled: action.state,
bucketToRewind: action.bucket,
dateToRewind: action.dateRewind,
};
return { ...state, rewind: rewindSetEnabled };
case REWIND_RESET_REWIND:
const resetItem = {
rewindEnabled: false,
bucketToRewind: "",
dateToRewind: null,
};
return { ...state, rewind: resetItem };
default:
return state;
}

View File

@@ -45,7 +45,6 @@ import UserServiceAccountsPanel from "./UserServiceAccountsPanel";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import ChangeUserPasswordModal from "../Account/ChangeUserPasswordModal";
import DeleteUserString from "./DeleteUserString";
import DeleteUser from "./DeleteUser";
import { usersSort } from "../../../utils/sortFunctions";
const styles = (theme: Theme) =>
@@ -113,6 +112,7 @@ const styles = (theme: Theme) =>
},
...actionsTray,
...searchField,
actionsTray: { ...actionsTray.actionsTray, justifyContent: "flex-end" },
...containerForHeader(theme.spacing(4)),
});

View File

@@ -1475,6 +1475,48 @@ func init() {
}
}
},
"/buckets/{bucket_name}/rewind/{date}": {
"get": {
"tags": [
"UserAPI"
],
"summary": "Get objects in a bucket for a rewind date",
"operationId": "GetBucketRewind",
"parameters": [
{
"type": "string",
"name": "bucket_name",
"in": "path",
"required": true
},
{
"type": "string",
"name": "date",
"in": "path",
"required": true
},
{
"type": "string",
"name": "prefix",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/rewindResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/buckets/{bucket_name}/versioning": {
"get": {
"tags": [
@@ -2440,6 +2482,36 @@ func init() {
}
}
},
"/namespace": {
"post": {
"tags": [
"AdminAPI"
],
"summary": "Creates a new Namespace with given information",
"operationId": "CreateNamespace",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/namespace"
}
}
],
"responses": {
"201": {
"description": "A successful response."
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/namespaces/{namespace}/resourcequotas/{resource-quota-name}": {
"get": {
"tags": [
@@ -5912,6 +5984,17 @@ func init() {
}
}
},
"namespace": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
}
},
"nodeLabels": {
"type": "object",
"additionalProperties": {
@@ -6774,6 +6857,41 @@ func init() {
}
}
},
"rewindItem": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"delete_flag": {
"type": "boolean"
},
"last_modified": {
"type": "string"
},
"name": {
"type": "string"
},
"size": {
"type": "integer",
"format": "int64"
},
"version_id": {
"type": "string"
}
}
},
"rewindResponse": {
"type": "object",
"properties": {
"objects": {
"type": "array",
"items": {
"$ref": "#/definitions/rewindItem"
}
}
}
},
"serviceAccountCreds": {
"type": "object",
"properties": {
@@ -9155,6 +9273,48 @@ func init() {
}
}
},
"/buckets/{bucket_name}/rewind/{date}": {
"get": {
"tags": [
"UserAPI"
],
"summary": "Get objects in a bucket for a rewind date",
"operationId": "GetBucketRewind",
"parameters": [
{
"type": "string",
"name": "bucket_name",
"in": "path",
"required": true
},
{
"type": "string",
"name": "date",
"in": "path",
"required": true
},
{
"type": "string",
"name": "prefix",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/rewindResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/buckets/{bucket_name}/versioning": {
"get": {
"tags": [
@@ -10120,6 +10280,36 @@ func init() {
}
}
},
"/namespace": {
"post": {
"tags": [
"AdminAPI"
],
"summary": "Creates a new Namespace with given information",
"operationId": "CreateNamespace",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/namespace"
}
}
],
"responses": {
"201": {
"description": "A successful response."
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/namespaces/{namespace}/resourcequotas/{resource-quota-name}": {
"get": {
"tags": [
@@ -14284,6 +14474,17 @@ func init() {
}
}
},
"namespace": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
}
},
"nodeLabels": {
"type": "object",
"additionalProperties": {
@@ -15011,6 +15212,41 @@ func init() {
}
}
},
"rewindItem": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"delete_flag": {
"type": "boolean"
},
"last_modified": {
"type": "string"
},
"name": {
"type": "string"
},
"size": {
"type": "integer",
"format": "int64"
},
"version_id": {
"type": "string"
}
}
},
"rewindResponse": {
"type": "object",
"properties": {
"objects": {
"type": "array",
"items": {
"$ref": "#/definitions/rewindItem"
}
}
}
},
"serviceAccountCreds": {
"type": "object",
"properties": {

View File

@@ -183,6 +183,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
UserAPIGetBucketRetentionConfigHandler: user_api.GetBucketRetentionConfigHandlerFunc(func(params user_api.GetBucketRetentionConfigParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.GetBucketRetentionConfig has not yet been implemented")
}),
UserAPIGetBucketRewindHandler: user_api.GetBucketRewindHandlerFunc(func(params user_api.GetBucketRewindParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.GetBucketRewind has not yet been implemented")
}),
UserAPIGetBucketVersioningHandler: user_api.GetBucketVersioningHandlerFunc(func(params user_api.GetBucketVersioningParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.GetBucketVersioning has not yet been implemented")
}),
@@ -562,6 +565,8 @@ type ConsoleAPI struct {
UserAPIGetBucketReplicationHandler user_api.GetBucketReplicationHandler
// UserAPIGetBucketRetentionConfigHandler sets the operation handler for the get bucket retention config operation
UserAPIGetBucketRetentionConfigHandler user_api.GetBucketRetentionConfigHandler
// UserAPIGetBucketRewindHandler sets the operation handler for the get bucket rewind operation
UserAPIGetBucketRewindHandler user_api.GetBucketRewindHandler
// UserAPIGetBucketVersioningHandler sets the operation handler for the get bucket versioning operation
UserAPIGetBucketVersioningHandler user_api.GetBucketVersioningHandler
// AdminAPIGetDirectCSIDriveListHandler sets the operation handler for the get direct c s i drive list operation
@@ -930,6 +935,9 @@ func (o *ConsoleAPI) Validate() error {
if o.UserAPIGetBucketRetentionConfigHandler == nil {
unregistered = append(unregistered, "user_api.GetBucketRetentionConfigHandler")
}
if o.UserAPIGetBucketRewindHandler == nil {
unregistered = append(unregistered, "user_api.GetBucketRewindHandler")
}
if o.UserAPIGetBucketVersioningHandler == nil {
unregistered = append(unregistered, "user_api.GetBucketVersioningHandler")
}
@@ -1437,6 +1445,10 @@ func (o *ConsoleAPI) initHandlerCache() {
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/buckets/{bucket_name}/rewind/{date}"] = user_api.NewGetBucketRewind(o.context, o.UserAPIGetBucketRewindHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/buckets/{bucket_name}/versioning"] = user_api.NewGetBucketVersioning(o.context, o.UserAPIGetBucketVersioningHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)

View File

@@ -0,0 +1,88 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// GetBucketRewindHandlerFunc turns a function with the right signature into a get bucket rewind handler
type GetBucketRewindHandlerFunc func(GetBucketRewindParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn GetBucketRewindHandlerFunc) Handle(params GetBucketRewindParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// GetBucketRewindHandler interface for that can handle valid get bucket rewind params
type GetBucketRewindHandler interface {
Handle(GetBucketRewindParams, *models.Principal) middleware.Responder
}
// NewGetBucketRewind creates a new http.Handler for the get bucket rewind operation
func NewGetBucketRewind(ctx *middleware.Context, handler GetBucketRewindHandler) *GetBucketRewind {
return &GetBucketRewind{Context: ctx, Handler: handler}
}
/* GetBucketRewind swagger:route GET /buckets/{bucket_name}/rewind/{date} UserAPI getBucketRewind
Get objects in a bucket for a rewind date
*/
type GetBucketRewind struct {
Context *middleware.Context
Handler GetBucketRewindHandler
}
func (o *GetBucketRewind) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewGetBucketRewindParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,142 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
)
// NewGetBucketRewindParams creates a new GetBucketRewindParams object
//
// There are no default values defined in the spec.
func NewGetBucketRewindParams() GetBucketRewindParams {
return GetBucketRewindParams{}
}
// GetBucketRewindParams contains all the bound params for the get bucket rewind operation
// typically these are obtained from a http.Request
//
// swagger:parameters GetBucketRewind
type GetBucketRewindParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: path
*/
BucketName string
/*
Required: true
In: path
*/
Date string
/*
In: query
*/
Prefix *string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewGetBucketRewindParams() beforehand.
func (o *GetBucketRewindParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
rBucketName, rhkBucketName, _ := route.Params.GetOK("bucket_name")
if err := o.bindBucketName(rBucketName, rhkBucketName, route.Formats); err != nil {
res = append(res, err)
}
rDate, rhkDate, _ := route.Params.GetOK("date")
if err := o.bindDate(rDate, rhkDate, route.Formats); err != nil {
res = append(res, err)
}
qPrefix, qhkPrefix, _ := qs.GetOK("prefix")
if err := o.bindPrefix(qPrefix, qhkPrefix, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindBucketName binds and validates parameter BucketName from path.
func (o *GetBucketRewindParams) bindBucketName(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.BucketName = raw
return nil
}
// bindDate binds and validates parameter Date from path.
func (o *GetBucketRewindParams) bindDate(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.Date = raw
return nil
}
// bindPrefix binds and validates parameter Prefix from query.
func (o *GetBucketRewindParams) bindPrefix(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
o.Prefix = &raw
return nil
}

View File

@@ -0,0 +1,133 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/console/models"
)
// GetBucketRewindOKCode is the HTTP code returned for type GetBucketRewindOK
const GetBucketRewindOKCode int = 200
/*GetBucketRewindOK A successful response.
swagger:response getBucketRewindOK
*/
type GetBucketRewindOK struct {
/*
In: Body
*/
Payload *models.RewindResponse `json:"body,omitempty"`
}
// NewGetBucketRewindOK creates GetBucketRewindOK with default headers values
func NewGetBucketRewindOK() *GetBucketRewindOK {
return &GetBucketRewindOK{}
}
// WithPayload adds the payload to the get bucket rewind o k response
func (o *GetBucketRewindOK) WithPayload(payload *models.RewindResponse) *GetBucketRewindOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the get bucket rewind o k response
func (o *GetBucketRewindOK) SetPayload(payload *models.RewindResponse) {
o.Payload = payload
}
// WriteResponse to the client
func (o *GetBucketRewindOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}
/*GetBucketRewindDefault Generic error response.
swagger:response getBucketRewindDefault
*/
type GetBucketRewindDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewGetBucketRewindDefault creates GetBucketRewindDefault with default headers values
func NewGetBucketRewindDefault(code int) *GetBucketRewindDefault {
if code <= 0 {
code = 500
}
return &GetBucketRewindDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the get bucket rewind default response
func (o *GetBucketRewindDefault) WithStatusCode(code int) *GetBucketRewindDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the get bucket rewind default response
func (o *GetBucketRewindDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the get bucket rewind default response
func (o *GetBucketRewindDefault) WithPayload(payload *models.Error) *GetBucketRewindDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the get bucket rewind default response
func (o *GetBucketRewindDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *GetBucketRewindDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,138 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// GetBucketRewindURL generates an URL for the get bucket rewind operation
type GetBucketRewindURL struct {
BucketName string
Date string
Prefix *string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *GetBucketRewindURL) WithBasePath(bp string) *GetBucketRewindURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *GetBucketRewindURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *GetBucketRewindURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/buckets/{bucket_name}/rewind/{date}"
bucketName := o.BucketName
if bucketName != "" {
_path = strings.Replace(_path, "{bucket_name}", bucketName, -1)
} else {
return nil, errors.New("bucketName is required on GetBucketRewindURL")
}
date := o.Date
if date != "" {
_path = strings.Replace(_path, "{date}", date, -1)
} else {
return nil, errors.New("date is required on GetBucketRewindURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
var prefixQ string
if o.Prefix != nil {
prefixQ = *o.Prefix
}
if prefixQ != "" {
qs.Set("prefix", prefixQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *GetBucketRewindURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *GetBucketRewindURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *GetBucketRewindURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on GetBucketRewindURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on GetBucketRewindURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *GetBucketRewindURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -23,6 +23,7 @@ import (
"strings"
"time"
"github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/sse"
@@ -148,6 +149,14 @@ func registerBucketsHandlers(api *operations.ConsoleAPI) {
}
return user_api.NewGetBucketObjectLockingStatusOK().WithPayload(getBucketObjectLockingStatus)
})
// get objects rewind for a bucket
api.UserAPIGetBucketRewindHandler = user_api.GetBucketRewindHandlerFunc(func(params user_api.GetBucketRewindParams, session *models.Principal) middleware.Responder {
getBucketRewind, err := getBucketRewindResponse(session, params)
if err != nil {
return user_api.NewGetBucketRewindDefault(500).WithPayload(err)
}
return user_api.NewGetBucketRewindOK().WithPayload(getBucketRewind)
})
}
type VersionState string
@@ -749,3 +758,55 @@ func getBucketObLockingResponse(session *models.Principal, bucketName string) (*
}
return listBucketsResponse, nil
}
func getBucketRewindResponse(session *models.Principal, params user_api.GetBucketRewindParams) (*models.RewindResponse, *models.Error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
var prefix = ""
if params.Prefix != nil {
prefix = *params.Prefix
}
s3Client, err := newS3BucketClient(session, params.BucketName, prefix)
if err != nil {
LogError("error creating S3Client: %v", err)
return nil, prepareError(err)
}
// create a mc S3Client interface implementation
// defining the client to be used
mcClient := mcClient{client: s3Client}
parsedDate, errDate := time.Parse(time.RFC3339, params.Date)
if errDate != nil {
return nil, prepareError(errDate)
}
var rewindItems []*models.RewindItem
for content := range mcClient.client.List(ctx, cmd.ListOptions{TimeRef: parsedDate, WithDeleteMarkers: true}) {
// build object name
name := strings.Replace(content.URL.Path, fmt.Sprintf("/%s/", params.BucketName), "", -1)
listElement := &models.RewindItem{
LastModified: content.Time.Format(time.RFC3339),
Size: content.Size,
VersionID: content.VersionID,
DeleteFlag: content.IsDeleteMarker,
Action: "",
Name: name,
}
cont, _ := json.Marshal(content)
fmt.Println(string(cont))
rewindItems = append(rewindItems, listElement)
}
return &models.RewindResponse{
Objects: rewindItems,
}, nil
}

View File

@@ -68,7 +68,11 @@ func registerObjectsHandlers(api *operations.ConsoleAPI) {
return user_api.NewDownloadObjectDefault(int(err.Code)).WithPayload(err)
}
return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
io.Copy(rw, resp)
x, err := io.Copy(rw, resp)
fmt.Println(x)
fmt.Println(err)
resp.Close()
})
})
@@ -229,7 +233,7 @@ func getDownloadObjectResponse(session *models.Principal, params user_api.Downlo
}
func downloadObject(ctx context.Context, client MCClient, versionID *string) (io.ReadCloser, error) {
// TODO: handle encripted files
// TODO: handle encrypted files
var reader io.ReadCloser
var version string
if versionID != nil {

View File

@@ -988,6 +988,7 @@ paths:
$ref: "#/definitions/error"
tags:
- UserAPI
/buckets/{bucket_name}/lifecycle/{lifecycle_id}:
put:
summary: Update Lifecycle rule
@@ -1015,6 +1016,35 @@ paths:
$ref: "#/definitions/error"
tags:
- UserAPI
/buckets/{bucket_name}/rewind/{date}:
get:
summary: Get objects in a bucket for a rewind date
operationId: GetBucketRewind
parameters:
- name: bucket_name
in: path
required: true
type: string
- name: date
in: path
required: true
type: string
- name: prefix
in: query
required: false
type: string
responses:
200:
description: A successful response.
schema:
$ref: "#/definitions/rewindResponse"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- UserAPI
/service-accounts:
get:
@@ -5485,3 +5515,28 @@ definitions:
type: string
message:
type: string
rewindItem:
type: object
properties:
last_modified:
type: string
size:
type: integer
format: int64
version_id:
type: string
delete_flag:
type: boolean
action:
type: string
name:
type: string
rewindResponse:
type: object
properties:
objects:
type: array
items:
$ref: "#/definitions/rewindItem"