Changed Object browser logic to work with websockets (#2419)
fixes https://github.com/minio/console/issues/943 ## What does this do? This allows us to start streaming results to the list instead of waiting to retrieve all the objects list before sending it to the client Included a couple of fixes: - Removed metadata for deleted items - Fixed multiple metadata requests - Fixed height grow for parent wrapper - Fixed object reload after restore version Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
@@ -40,6 +40,9 @@ type RewindItem struct {
|
||||
// delete flag
|
||||
DeleteFlag bool `json:"delete_flag,omitempty"`
|
||||
|
||||
// is latest
|
||||
IsLatest bool `json:"is_latest,omitempty"`
|
||||
|
||||
// last modified
|
||||
LastModified string `json:"last_modified,omitempty"`
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// 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, { Fragment, useEffect } from "react";
|
||||
import React, { Fragment, useCallback, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
@@ -37,8 +37,22 @@ import {
|
||||
} from "../../../../common/SecureComponent/permissions";
|
||||
import BackLink from "../../../../common/BackLink";
|
||||
import {
|
||||
newMessage,
|
||||
resetMessages,
|
||||
setIsVersioned,
|
||||
setLoadingLocking,
|
||||
setLoadingObjectInfo,
|
||||
setLoadingObjectsList,
|
||||
setLoadingRecords,
|
||||
setLoadingVersioning,
|
||||
setLoadingVersions,
|
||||
setLockingEnabled,
|
||||
setObjectDetailsView,
|
||||
setRecords,
|
||||
setSearchObjects,
|
||||
setSearchVersions,
|
||||
setSelectedObjectView,
|
||||
setSimplePathHandler,
|
||||
setVersionsModeEnabled,
|
||||
} from "../../ObjectBrowser/objectBrowserSlice";
|
||||
import SearchBox from "../../Common/SearchBox";
|
||||
@@ -47,18 +61,93 @@ import AutoColorIcon from "../../Common/Components/AutoColorIcon";
|
||||
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
|
||||
import { Button } from "mds";
|
||||
import hasPermission from "../../../../common/SecureComponent/accessControl";
|
||||
import { IMessageEvent } from "websocket";
|
||||
import { wsProtocol } from "../../../../utils/wsUtils";
|
||||
import {
|
||||
WebsocketRequest,
|
||||
WebsocketResponse,
|
||||
} from "../ListBuckets/Objects/ListObjects/types";
|
||||
import { decodeURLString, encodeURLString } from "../../../../common/utils";
|
||||
import { permissionItems } from "../ListBuckets/Objects/utils";
|
||||
import { setErrorSnackMessage } from "../../../../systemSlice";
|
||||
import api from "../../../../common/api";
|
||||
import { BucketObjectLocking, BucketVersioning } from "../types";
|
||||
import { ErrorResponseHandler } from "../../../../common/types";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
});
|
||||
|
||||
let objectsWS: WebSocket;
|
||||
let currentRequestID: number = 0;
|
||||
let errorCounter: number = 0;
|
||||
|
||||
const initWSConnection = (
|
||||
onMessageCallback: (message: IMessageEvent) => void,
|
||||
openCallback?: () => void,
|
||||
notAvailableCallback?: () => void
|
||||
) => {
|
||||
const url = new URL(window.location.toString());
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
const port = isDev ? "9090" : url.port;
|
||||
|
||||
// check if we are using base path, if not this always is `/`
|
||||
const baseLocation = new URL(document.baseURI);
|
||||
const baseUrl = baseLocation.pathname;
|
||||
|
||||
const wsProt = wsProtocol(url.protocol);
|
||||
|
||||
objectsWS = new WebSocket(
|
||||
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/objectManager`
|
||||
);
|
||||
|
||||
objectsWS.onopen = () => {
|
||||
if (openCallback) {
|
||||
openCallback();
|
||||
}
|
||||
errorCounter = 0;
|
||||
};
|
||||
|
||||
const reconnectFn = () => {
|
||||
if (errorCounter <= 5) {
|
||||
initWSConnection(onMessageCallback, openCallback);
|
||||
errorCounter += 1;
|
||||
} else {
|
||||
console.error("Websocket not available.");
|
||||
if (notAvailableCallback) {
|
||||
notAvailableCallback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
objectsWS.onclose = () => {
|
||||
console.warn("Websocket Disconnected. Attempting Reconnection...");
|
||||
|
||||
// We reconnect after 3 seconds
|
||||
setTimeout(reconnectFn, 3000);
|
||||
};
|
||||
|
||||
objectsWS.onerror = () => {
|
||||
console.error("Error in websocket connection. Attempting reconnection...");
|
||||
|
||||
// We reconnect after 3 seconds
|
||||
setTimeout(reconnectFn, 3000);
|
||||
};
|
||||
};
|
||||
|
||||
initWSConnection(() => {});
|
||||
|
||||
const BrowserHandler = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
|
||||
const loadingVersioning = useSelector(
|
||||
(state: AppState) => state.objectBrowser.loadingVersioning
|
||||
);
|
||||
|
||||
const versionsMode = useSelector(
|
||||
(state: AppState) => state.objectBrowser.versionsMode
|
||||
);
|
||||
@@ -71,6 +160,36 @@ const BrowserHandler = () => {
|
||||
const searchVersions = useSelector(
|
||||
(state: AppState) => state.objectBrowser.searchVersions
|
||||
);
|
||||
const rewindEnabled = useSelector(
|
||||
(state: AppState) => state.objectBrowser.rewind.rewindEnabled
|
||||
);
|
||||
const rewindDate = useSelector(
|
||||
(state: AppState) => state.objectBrowser.rewind.dateToRewind
|
||||
);
|
||||
const showDeleted = useSelector(
|
||||
(state: AppState) => state.objectBrowser.showDeleted
|
||||
);
|
||||
const allowResources = useSelector(
|
||||
(state: AppState) => state.console.session.allowResources
|
||||
);
|
||||
const loadingObjects = useSelector(
|
||||
(state: AppState) => state.objectBrowser.loadingObjects
|
||||
);
|
||||
const loadingLocking = useSelector(
|
||||
(state: AppState) => state.objectBrowser.loadingLocking
|
||||
);
|
||||
const bucketToRewind = useSelector(
|
||||
(state: AppState) => state.objectBrowser.rewind.bucketToRewind
|
||||
);
|
||||
const loadRecords = useSelector(
|
||||
(state: AppState) => state.objectBrowser.loadRecords
|
||||
);
|
||||
const detailsOpen = useSelector(
|
||||
(state: AppState) => state.objectBrowser.objectDetailsOpen
|
||||
);
|
||||
const selectedInternalPaths = useSelector(
|
||||
(state: AppState) => state.objectBrowser.selectedInternalPaths
|
||||
);
|
||||
|
||||
const features = useSelector(selFeatures);
|
||||
|
||||
@@ -81,10 +200,286 @@ const BrowserHandler = () => {
|
||||
|
||||
const obOnly = !!features?.includes("object-browser-only");
|
||||
|
||||
/*WS Request Handlers*/
|
||||
objectsWS.onmessage = useCallback(
|
||||
(message: IMessageEvent) => {
|
||||
// reset start status
|
||||
dispatch(setLoadingObjectsList(false));
|
||||
|
||||
const response: WebsocketResponse = JSON.parse(message.data.toString());
|
||||
if (currentRequestID === response.request_id) {
|
||||
// If response is not from current request, we can omit
|
||||
if (response.request_id !== currentRequestID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
response.error ===
|
||||
"The Access Key Id you provided does not exist in our records."
|
||||
) {
|
||||
// Session expired.
|
||||
window.location.reload();
|
||||
} else if (response.error === "Access Denied.") {
|
||||
let pathPrefix = "";
|
||||
if (internalPaths) {
|
||||
const decodedPath = decodeURLString(internalPaths);
|
||||
pathPrefix = decodedPath.endsWith("/")
|
||||
? decodedPath
|
||||
: decodedPath + "/";
|
||||
}
|
||||
|
||||
const permitItems = permissionItems(
|
||||
bucketName,
|
||||
pathPrefix,
|
||||
allowResources || []
|
||||
);
|
||||
|
||||
if (!permitItems || permitItems.length === 0) {
|
||||
dispatch(
|
||||
setErrorSnackMessage({
|
||||
errorMessage: response.error,
|
||||
detailedError: response.error,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(setRecords(permitItems));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This indicates final messages is received.
|
||||
if (response.request_end) {
|
||||
dispatch(setLoadingObjectsList(false));
|
||||
dispatch(setLoadingRecords(false));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
dispatch(newMessage(response.data));
|
||||
}
|
||||
}
|
||||
},
|
||||
[dispatch, internalPaths, allowResources, bucketName]
|
||||
);
|
||||
|
||||
const initWSRequest = useCallback(
|
||||
(path: string, date: Date) => {
|
||||
if (objectsWS && objectsWS.readyState === 1) {
|
||||
try {
|
||||
const newRequestID = currentRequestID + 1;
|
||||
dispatch(resetMessages());
|
||||
|
||||
const request: WebsocketRequest = {
|
||||
bucket_name: bucketName,
|
||||
prefix: encodeURLString(path),
|
||||
mode: rewindEnabled || showDeleted ? "rewind" : "objects",
|
||||
date: date.toISOString(),
|
||||
request_id: newRequestID,
|
||||
};
|
||||
|
||||
objectsWS.send(JSON.stringify(request));
|
||||
|
||||
// We store the new ID for the requestID
|
||||
currentRequestID = newRequestID;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
} else {
|
||||
// Socket is disconnected, we request reconnection but will need to recreate call
|
||||
const dupRequest = () => {
|
||||
initWSRequest(path, date);
|
||||
};
|
||||
|
||||
initWSConnection(dupRequest);
|
||||
}
|
||||
},
|
||||
[bucketName, rewindEnabled, showDeleted, dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const request: WebsocketRequest = {
|
||||
mode: "cancel",
|
||||
request_id: currentRequestID,
|
||||
};
|
||||
|
||||
if (objectsWS && objectsWS.readyState === 1) {
|
||||
objectsWS.send(JSON.stringify(request));
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (objectsWS?.readyState === 1) {
|
||||
const decodedIPaths = decodeURLString(internalPaths);
|
||||
|
||||
if (decodedIPaths.endsWith("/") || decodedIPaths === "") {
|
||||
dispatch(setObjectDetailsView(false));
|
||||
dispatch(setSelectedObjectView(null));
|
||||
dispatch(
|
||||
setSimplePathHandler(decodedIPaths === "" ? "/" : decodedIPaths)
|
||||
);
|
||||
} else {
|
||||
dispatch(setLoadingObjectInfo(true));
|
||||
dispatch(setObjectDetailsView(true));
|
||||
dispatch(setLoadingVersions(true));
|
||||
dispatch(
|
||||
setSelectedObjectView(
|
||||
`${decodedIPaths ? `${encodeURLString(decodedIPaths)}` : ``}`
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setSimplePathHandler(
|
||||
`${decodedIPaths.split("/").slice(0, -1).join("/")}/`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [internalPaths, rewindDate, rewindEnabled, dispatch]);
|
||||
|
||||
// Direct file access effect / prefix
|
||||
useEffect(() => {
|
||||
if (!loadingObjects && loadRecords && !rewindEnabled) {
|
||||
const parentPath = `${decodeURLString(internalPaths)
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/")}/`;
|
||||
|
||||
initWSRequest(parentPath, new Date());
|
||||
}
|
||||
}, [
|
||||
loadingObjects,
|
||||
loadRecords,
|
||||
bucketName,
|
||||
bucketToRewind,
|
||||
dispatch,
|
||||
internalPaths,
|
||||
rewindDate,
|
||||
rewindEnabled,
|
||||
initWSRequest,
|
||||
detailsOpen,
|
||||
]);
|
||||
|
||||
const displayListObjects = hasPermission(bucketName, [
|
||||
IAM_SCOPES.S3_LIST_BUCKET,
|
||||
]);
|
||||
|
||||
// Common objects list
|
||||
useEffect(() => {
|
||||
// begin watch if bucketName in bucketList and start pressed
|
||||
if (loadingObjects && displayListObjects) {
|
||||
let pathPrefix = "";
|
||||
if (internalPaths) {
|
||||
const decodedPath = decodeURLString(internalPaths);
|
||||
|
||||
// internalPaths are selected (file details), we split and get parent folder
|
||||
if (selectedInternalPaths === internalPaths) {
|
||||
pathPrefix = `${decodeURLString(internalPaths)
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/")}/`;
|
||||
} else {
|
||||
pathPrefix = decodedPath.endsWith("/")
|
||||
? decodedPath
|
||||
: decodedPath + "/";
|
||||
}
|
||||
}
|
||||
|
||||
let requestDate = new Date();
|
||||
|
||||
if (rewindEnabled && rewindDate) {
|
||||
requestDate = rewindDate;
|
||||
}
|
||||
|
||||
initWSRequest(pathPrefix, requestDate);
|
||||
} else {
|
||||
dispatch(setLoadingObjectsList(false));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
loadingObjects,
|
||||
internalPaths,
|
||||
dispatch,
|
||||
rewindDate,
|
||||
rewindEnabled,
|
||||
displayListObjects,
|
||||
initWSRequest,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setVersionsModeEnabled({ status: false }));
|
||||
}, [internalPaths, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingVersioning) {
|
||||
if (displayListObjects) {
|
||||
api
|
||||
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
|
||||
.then((res: BucketVersioning) => {
|
||||
dispatch(setIsVersioned(res.is_versioned));
|
||||
dispatch(setLoadingVersioning(false));
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
console.error(
|
||||
"Error Getting Object Versioning Status: ",
|
||||
err.detailedError
|
||||
);
|
||||
dispatch(setLoadingVersioning(false));
|
||||
});
|
||||
} else {
|
||||
dispatch(setLoadingVersioning(false));
|
||||
dispatch(resetMessages());
|
||||
}
|
||||
}
|
||||
}, [bucketName, loadingVersioning, dispatch, displayListObjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingLocking) {
|
||||
if (displayListObjects) {
|
||||
api
|
||||
.invoke("GET", `/api/v1/buckets/${bucketName}/object-locking`)
|
||||
.then((res: BucketObjectLocking) => {
|
||||
dispatch(setLockingEnabled(res.object_locking_enabled));
|
||||
dispatch(setLoadingLocking(false));
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
console.error(
|
||||
"Error Getting Object Locking Status: ",
|
||||
err.detailedError
|
||||
);
|
||||
dispatch(setLoadingLocking(false));
|
||||
});
|
||||
} else {
|
||||
dispatch(resetMessages());
|
||||
dispatch(setLoadingLocking(false));
|
||||
}
|
||||
}
|
||||
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingLocking) {
|
||||
if (displayListObjects) {
|
||||
api
|
||||
.invoke("GET", `/api/v1/buckets/${bucketName}/object-locking`)
|
||||
.then((res: BucketObjectLocking) => {
|
||||
dispatch(setLockingEnabled(res.object_locking_enabled));
|
||||
setLoadingLocking(false);
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
console.error(
|
||||
"Error Getting Object Locking Status: ",
|
||||
err.detailedError
|
||||
);
|
||||
setLoadingLocking(false);
|
||||
});
|
||||
} else {
|
||||
dispatch(resetMessages());
|
||||
setLoadingLocking(false);
|
||||
}
|
||||
}
|
||||
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
|
||||
|
||||
const openBucketConfiguration = () => {
|
||||
navigate(`/buckets/${bucketName}/admin`);
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ import { selFeatures } from "../../consoleSlice";
|
||||
import AutoColorIcon from "../../Common/Components/AutoColorIcon";
|
||||
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
|
||||
import AButton from "../../Common/AButton/AButton";
|
||||
import { setLoadingObjectsList } from "../../ObjectBrowser/objectBrowserSlice";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -123,6 +124,7 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
|
||||
.then((res: BucketList) => {
|
||||
setLoading(false);
|
||||
setRecords(res.buckets || []);
|
||||
dispatch(setLoadingObjectsList(true));
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
setLoading(false);
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
formFieldStyles,
|
||||
modalStyleUtils,
|
||||
} from "../../../../Common/FormComponents/common/styleLibrary";
|
||||
import { connect } from "react-redux";
|
||||
import { connect, useSelector } from "react-redux";
|
||||
import { encodeURLString } from "../../../../../../common/utils";
|
||||
|
||||
import { BucketObjectItem } from "./types";
|
||||
@@ -41,7 +41,6 @@ interface ICreatePath {
|
||||
bucketName: string;
|
||||
folderName: string;
|
||||
onClose: () => any;
|
||||
existingFiles: BucketObjectItem[];
|
||||
simplePath: string | null;
|
||||
}
|
||||
|
||||
@@ -57,7 +56,6 @@ const CreatePathModal = ({
|
||||
bucketName,
|
||||
onClose,
|
||||
classes,
|
||||
existingFiles,
|
||||
simplePath,
|
||||
}: ICreatePath) => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -67,6 +65,8 @@ const CreatePathModal = ({
|
||||
const [isFormValid, setIsFormValid] = useState<boolean>(false);
|
||||
const [currentPath, setCurrentPath] = useState(bucketName);
|
||||
|
||||
const records = useSelector((state: AppState) => state.objectBrowser.records);
|
||||
|
||||
useEffect(() => {
|
||||
if (simplePath) {
|
||||
const newPath = `${bucketName}${
|
||||
@@ -91,7 +91,7 @@ const CreatePathModal = ({
|
||||
const sharesName = (record: BucketObjectItem) =>
|
||||
record.name === folderPath + pathUrl;
|
||||
|
||||
if (existingFiles.findIndex(sharesName) !== -1) {
|
||||
if (records.findIndex(sharesName) !== -1) {
|
||||
dispatch(
|
||||
setModalErrorSnackMessage({
|
||||
errorMessage: "Folder cannot have the same name as an existing file",
|
||||
|
||||
@@ -30,15 +30,10 @@ import { Button } from "mds";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import get from "lodash/get";
|
||||
import { BucketObjectItem, BucketObjectItemsList } from "./types";
|
||||
import api from "../../../../../../common/api";
|
||||
import TableWrapper, {
|
||||
ItemActions,
|
||||
} from "../../../../Common/TableWrapper/TableWrapper";
|
||||
import {
|
||||
decodeURLString,
|
||||
encodeURLString,
|
||||
getClientOS,
|
||||
niceBytesInt,
|
||||
} from "../../../../../../common/utils";
|
||||
|
||||
@@ -50,27 +45,16 @@ import {
|
||||
searchField,
|
||||
tableStyles,
|
||||
} from "../../../../Common/FormComponents/common/styleLibrary";
|
||||
import { Badge, Typography } from "@mui/material";
|
||||
import { Badge } from "@mui/material";
|
||||
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
|
||||
import {
|
||||
download,
|
||||
extensionPreview,
|
||||
permissionItems,
|
||||
sortListObjects,
|
||||
} from "../utils";
|
||||
import {
|
||||
BucketInfo,
|
||||
BucketObjectLocking,
|
||||
BucketQuota,
|
||||
BucketVersioning,
|
||||
} from "../../../types";
|
||||
import { extensionPreview } from "../utils";
|
||||
import { BucketInfo, BucketQuota } from "../../../types";
|
||||
import { ErrorResponseHandler } from "../../../../../../common/types";
|
||||
|
||||
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
|
||||
|
||||
import { AppState, useAppDispatch } from "../../../../../../store";
|
||||
import PageLayout from "../../../../Common/Layout/PageLayout";
|
||||
|
||||
import {
|
||||
IAM_SCOPES,
|
||||
permissionTooltipHelper,
|
||||
@@ -79,7 +63,6 @@ import {
|
||||
hasPermission,
|
||||
SecureComponent,
|
||||
} from "../../../../../../common/SecureComponent";
|
||||
|
||||
import withSuspense from "../../../../Common/Components/withSuspense";
|
||||
import {
|
||||
BucketsIcon,
|
||||
@@ -91,7 +74,6 @@ import UploadFilesButton from "../../UploadFilesButton";
|
||||
import DetailsListPanel from "./DetailsListPanel";
|
||||
import ObjectDetailPanel from "./ObjectDetailPanel";
|
||||
import ActionsListSection from "./ActionsListSection";
|
||||
import { listModeColumns, rewindModeColumns } from "./ListObjectsHelpers";
|
||||
import VersionsNavigator from "../ObjectDetails/VersionsNavigator";
|
||||
import CheckboxWrapper from "../../../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
|
||||
|
||||
@@ -111,16 +93,21 @@ import {
|
||||
completeObject,
|
||||
failObject,
|
||||
openList,
|
||||
resetMessages,
|
||||
resetRewind,
|
||||
setLoadingObjectInfo,
|
||||
setDownloadRenameModal,
|
||||
setLoadingObjectsList,
|
||||
setLoadingRecords,
|
||||
setLoadingVersions,
|
||||
setNewObject,
|
||||
setObjectDetailsView,
|
||||
setPreviewOpen,
|
||||
setSearchObjects,
|
||||
setSelectedObjects,
|
||||
setSelectedObjectView,
|
||||
setSelectedPreview,
|
||||
setShareFileModalOpen,
|
||||
setShowDeletedObjects,
|
||||
setSimplePathHandler,
|
||||
setVersionsModeEnabled,
|
||||
updateProgress,
|
||||
} from "../../../../ObjectBrowser/objectBrowserSlice";
|
||||
@@ -132,8 +119,13 @@ import {
|
||||
setBucketInfo,
|
||||
} from "../../../BucketDetails/bucketDetailsSlice";
|
||||
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
|
||||
import { selFeatures } from "../../../../consoleSlice";
|
||||
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
|
||||
import ListObjectsTable from "./ListObjectsTable";
|
||||
import {
|
||||
downloadSelected,
|
||||
openPreview,
|
||||
openShare,
|
||||
} from "../../../../ObjectBrowser/objectBrowserThunks";
|
||||
|
||||
const HistoryIcon = React.lazy(
|
||||
() => import("../../../../../../icons/HistoryIcon")
|
||||
@@ -159,28 +151,6 @@ const PreviewFileModal = withSuspense(
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
browsePaper: {
|
||||
border: 0,
|
||||
height: "calc(100vh - 210px)",
|
||||
"&.isEmbedded": {
|
||||
height: "calc(100vh - 315px)",
|
||||
},
|
||||
"&.actionsPanelOpen": {
|
||||
minHeight: "100%",
|
||||
},
|
||||
"@media (max-width: 800px)": {
|
||||
width: 800,
|
||||
},
|
||||
},
|
||||
"@global": {
|
||||
".rowLine:hover .iconFileElm": {
|
||||
backgroundImage: "url(/images/ob_file_filled.svg)",
|
||||
},
|
||||
".rowLine:hover .iconFolderElm": {
|
||||
backgroundImage: "url(/images/ob_folder_filled.svg)",
|
||||
},
|
||||
},
|
||||
|
||||
badgeOverlap: {
|
||||
"& .MuiBadge-badge": {
|
||||
top: 10,
|
||||
@@ -215,12 +185,8 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
breadcrumbsContainer: {
|
||||
padding: "12px 14px 5px",
|
||||
},
|
||||
parentWrapper: {
|
||||
"@media (max-width: 800px)": {
|
||||
overflowX: "auto",
|
||||
},
|
||||
},
|
||||
fullContainer: {
|
||||
position: "relative",
|
||||
"@media (max-width: 799px)": {
|
||||
width: 0,
|
||||
},
|
||||
@@ -255,31 +221,6 @@ const acceptDnDStyle = {
|
||||
borderColor: "#00e676",
|
||||
};
|
||||
|
||||
function useInterval(callback: any, delay: number) {
|
||||
const savedCallback = useRef<Function | null>(null);
|
||||
|
||||
// Remember the latest callback.
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
if (savedCallback !== undefined && savedCallback.current) {
|
||||
savedCallback.current();
|
||||
}
|
||||
}
|
||||
|
||||
if (delay !== null) {
|
||||
let id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
}
|
||||
|
||||
const defLoading = <Typography component="h3">Loading...</Typography>;
|
||||
|
||||
const ListObjects = () => {
|
||||
const classes = useStyles();
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -290,9 +231,6 @@ const ListObjects = () => {
|
||||
const rewindEnabled = useSelector(
|
||||
(state: AppState) => state.objectBrowser.rewind.rewindEnabled
|
||||
);
|
||||
const rewindDate = useSelector(
|
||||
(state: AppState) => state.objectBrowser.rewind.dateToRewind
|
||||
);
|
||||
const bucketToRewind = useSelector(
|
||||
(state: AppState) => state.objectBrowser.rewind.bucketToRewind
|
||||
);
|
||||
@@ -300,9 +238,6 @@ const ListObjects = () => {
|
||||
(state: AppState) => state.objectBrowser.versionsMode
|
||||
);
|
||||
|
||||
const searchObjects = useSelector(
|
||||
(state: AppState) => state.objectBrowser.searchObjects
|
||||
);
|
||||
const showDeleted = useSelector(
|
||||
(state: AppState) => state.objectBrowser.showDeleted
|
||||
);
|
||||
@@ -312,47 +247,41 @@ const ListObjects = () => {
|
||||
const selectedInternalPaths = useSelector(
|
||||
(state: AppState) => state.objectBrowser.selectedInternalPaths
|
||||
);
|
||||
const loading = useSelector(
|
||||
const loadingObjects = useSelector(
|
||||
(state: AppState) => state.objectBrowser.loadingObjects
|
||||
);
|
||||
const simplePath = useSelector(
|
||||
(state: AppState) => state.objectBrowser.simplePath
|
||||
);
|
||||
|
||||
const loadingBucket = useSelector(selBucketDetailsLoading);
|
||||
const bucketInfo = useSelector(selBucketDetailsInfo);
|
||||
const allowResources = useSelector(
|
||||
(state: AppState) => state.console.session.allowResources
|
||||
const isVersioned = useSelector(
|
||||
(state: AppState) => state.objectBrowser.isVersioned
|
||||
);
|
||||
const lockingEnabled = useSelector(
|
||||
(state: AppState) => state.objectBrowser.lockingEnabled
|
||||
);
|
||||
const downloadRenameModal = useSelector(
|
||||
(state: AppState) => state.objectBrowser.downloadRenameModal
|
||||
);
|
||||
const selectedPreview = useSelector(
|
||||
(state: AppState) => state.objectBrowser.selectedPreview
|
||||
);
|
||||
const shareFileModalOpen = useSelector(
|
||||
(state: AppState) => state.objectBrowser.shareFileModalOpen
|
||||
);
|
||||
const previewOpen = useSelector(
|
||||
(state: AppState) => state.objectBrowser.previewOpen
|
||||
);
|
||||
|
||||
const features = useSelector(selFeatures);
|
||||
const obOnly = !!features?.includes("object-browser-only");
|
||||
const loadingBucket = useSelector(selBucketDetailsLoading);
|
||||
const bucketInfo = useSelector(selBucketDetailsInfo);
|
||||
|
||||
const [records, setRecords] = useState<BucketObjectItem[]>([]);
|
||||
const [deleteMultipleOpen, setDeleteMultipleOpen] = useState<boolean>(false);
|
||||
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 [loadingLocking, setLoadingLocking] = useState<boolean>(true);
|
||||
const [lockingEnabled, setLockingEnabled] = useState<boolean>(false);
|
||||
const [rewindSelect, setRewindSelect] = useState<boolean>(false);
|
||||
const [selectedObjects, setSelectedObjects] = useState<string[]>([]);
|
||||
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
|
||||
const [selectedPreview, setSelectedPreview] =
|
||||
useState<BucketObjectItem | null>(null);
|
||||
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
|
||||
const [sortDirection, setSortDirection] = useState<
|
||||
"ASC" | "DESC" | undefined
|
||||
>("ASC");
|
||||
const [currentSortField, setCurrentSortField] = useState<string>("name");
|
||||
const [iniLoad, setIniLoad] = useState<boolean>(false);
|
||||
const [canShareFile, setCanShareFile] = useState<boolean>(false);
|
||||
const [canPreviewFile, setCanPreviewFile] = useState<boolean>(false);
|
||||
const [quota, setQuota] = useState<BucketQuota | null>(null);
|
||||
const [downloadRenameModal, setDownloadRenameModal] =
|
||||
useState<BucketObjectItem | null>(null);
|
||||
|
||||
const pathSegment = location.pathname.split("/browse/");
|
||||
|
||||
@@ -379,6 +308,30 @@ const ListObjects = () => {
|
||||
true
|
||||
);
|
||||
|
||||
const displayDeleteObject = hasPermission(bucketName, [
|
||||
IAM_SCOPES.S3_DELETE_OBJECT,
|
||||
]);
|
||||
const selectedObjects = useSelector(
|
||||
(state: AppState) => state.objectBrowser.selectedObjects
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setSearchObjects(""));
|
||||
dispatch(setLoadingObjectsList(true));
|
||||
dispatch(setSelectedObjects([]));
|
||||
}, [simplePath, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rewindEnabled) {
|
||||
if (bucketToRewind !== bucketName) {
|
||||
dispatch(resetRewind());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [rewindEnabled, bucketToRewind, bucketName, dispatch]);
|
||||
|
||||
// END OF WS HANDLERS
|
||||
|
||||
useEffect(() => {
|
||||
if (folderUpload.current !== null) {
|
||||
folderUpload.current.setAttribute("directory", "");
|
||||
@@ -433,39 +386,14 @@ const ListObjects = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedObjects.length === 0 && selectedInternalPaths === null) {
|
||||
if (
|
||||
selectedObjects.length === 0 &&
|
||||
selectedInternalPaths === null &&
|
||||
!loadingObjects
|
||||
) {
|
||||
dispatch(setObjectDetailsView(false));
|
||||
}
|
||||
}, [selectedObjects, selectedInternalPaths, dispatch]);
|
||||
|
||||
const displayDeleteObject = hasPermission(bucketName, [
|
||||
IAM_SCOPES.S3_DELETE_OBJECT,
|
||||
]);
|
||||
|
||||
const displayListObjects = hasPermission(bucketName, [
|
||||
IAM_SCOPES.S3_LIST_BUCKET,
|
||||
]);
|
||||
|
||||
const updateMessage = () => {
|
||||
let timeDelta = Date.now() - loadingStartTime;
|
||||
|
||||
if (timeDelta / 1000 >= 6) {
|
||||
setLoadingMessage(
|
||||
<Fragment>
|
||||
<Typography component="h3">
|
||||
This operation is taking longer than expected... (
|
||||
{Math.ceil(timeDelta / 1000)}s)
|
||||
</Typography>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (timeDelta / 1000 >= 3) {
|
||||
setLoadingMessage(
|
||||
<Typography component="h3">
|
||||
This operation is taking longer than expected...
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [selectedObjects, selectedInternalPaths, dispatch, loadingObjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!iniLoad) {
|
||||
@@ -474,280 +402,6 @@ const ListObjects = () => {
|
||||
}
|
||||
}, [iniLoad, dispatch, setIniLoad]);
|
||||
|
||||
useInterval(() => {
|
||||
// Your custom logic here
|
||||
if (loading) {
|
||||
updateMessage();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingVersioning) {
|
||||
if (displayListObjects) {
|
||||
api
|
||||
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
|
||||
.then((res: BucketVersioning) => {
|
||||
setIsVersioned(res.is_versioned);
|
||||
setLoadingVersioning(false);
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
console.error(
|
||||
"Error Getting Object Versioning Status: ",
|
||||
err.detailedError
|
||||
);
|
||||
setLoadingVersioning(false);
|
||||
});
|
||||
} else {
|
||||
setLoadingVersioning(false);
|
||||
setRecords([]);
|
||||
}
|
||||
}
|
||||
}, [bucketName, loadingVersioning, dispatch, displayListObjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingLocking) {
|
||||
if (displayListObjects) {
|
||||
api
|
||||
.invoke("GET", `/api/v1/buckets/${bucketName}/object-locking`)
|
||||
.then((res: BucketObjectLocking) => {
|
||||
setLockingEnabled(res.object_locking_enabled);
|
||||
setLoadingLocking(false);
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
console.error(
|
||||
"Error Getting Object Locking Status: ",
|
||||
err.detailedError
|
||||
);
|
||||
setLoadingLocking(false);
|
||||
});
|
||||
} else {
|
||||
setRecords([]);
|
||||
setLoadingLocking(false);
|
||||
}
|
||||
}
|
||||
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
|
||||
|
||||
useEffect(() => {
|
||||
const decodedIPaths = decodeURLString(internalPaths);
|
||||
|
||||
if (decodedIPaths.endsWith("/") || decodedIPaths === "") {
|
||||
dispatch(setObjectDetailsView(false));
|
||||
dispatch(setSelectedObjectView(null));
|
||||
dispatch(
|
||||
setSimplePathHandler(decodedIPaths === "" ? "/" : decodedIPaths)
|
||||
);
|
||||
} else {
|
||||
dispatch(setLoadingObjectInfo(true));
|
||||
dispatch(setObjectDetailsView(true));
|
||||
dispatch(setLoadingVersions(true));
|
||||
dispatch(
|
||||
setSelectedObjectView(
|
||||
`${decodedIPaths ? `${encodeURLString(decodedIPaths)}` : ``}`
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setSimplePathHandler(
|
||||
`${decodedIPaths.split("/").slice(0, -1).join("/")}/`
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [internalPaths, rewindDate, rewindEnabled, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setSearchObjects(""));
|
||||
dispatch(setLoadingObjectsList(true));
|
||||
setSelectedObjects([]);
|
||||
}, [simplePath, dispatch, setSelectedObjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
if (displayListObjects) {
|
||||
let pathPrefix = "";
|
||||
if (internalPaths) {
|
||||
const decodedPath = decodeURLString(internalPaths);
|
||||
pathPrefix = decodedPath.endsWith("/")
|
||||
? decodedPath
|
||||
: decodedPath + "/";
|
||||
}
|
||||
|
||||
let currentTimestamp = Date.now();
|
||||
setLoadingStartTime(currentTimestamp);
|
||||
setLoadingMessage(defLoading);
|
||||
|
||||
// We get URL to look into
|
||||
let urlTake = `/api/v1/buckets/${bucketName}/objects`;
|
||||
|
||||
// Is rewind enabled?, we use Rewind API
|
||||
if (rewindEnabled) {
|
||||
if (bucketToRewind !== bucketName) {
|
||||
dispatch(resetRewind());
|
||||
return;
|
||||
}
|
||||
|
||||
if (rewindDate) {
|
||||
const rewindParsed = rewindDate.toISOString();
|
||||
|
||||
urlTake = `/api/v1/buckets/${bucketName}/rewind/${rewindParsed}`;
|
||||
}
|
||||
} else if (showDeleted) {
|
||||
// Do we want to display deleted items too?, we use rewind to current time to show everything
|
||||
const currDate = new Date();
|
||||
const currDateISO = currDate.toISOString();
|
||||
|
||||
urlTake = `/api/v1/buckets/${bucketName}/rewind/${currDateISO}`;
|
||||
}
|
||||
|
||||
api
|
||||
.invoke(
|
||||
"GET",
|
||||
`${urlTake}${
|
||||
pathPrefix ? `?prefix=${encodeURLString(pathPrefix)}` : ``
|
||||
}`
|
||||
)
|
||||
.then((res: BucketObjectItemsList) => {
|
||||
const records: BucketObjectItem[] = res.objects || [];
|
||||
const folders: BucketObjectItem[] = [];
|
||||
const files: BucketObjectItem[] = [];
|
||||
|
||||
// We separate items between folders or files to display folders at the beginning always.
|
||||
records.forEach((record) => {
|
||||
// We omit files from the same path
|
||||
if (record.name !== decodeURLString(internalPaths)) {
|
||||
// this is a folder
|
||||
if (record.name.endsWith("/")) {
|
||||
folders.push(record);
|
||||
} else {
|
||||
// this is a file
|
||||
files.push(record);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const recordsInElement = [...folders, ...files];
|
||||
|
||||
if (recordsInElement.length === 0 && pathPrefix !== "") {
|
||||
let pathTest = `/api/v1/buckets/${bucketName}/objects${
|
||||
internalPaths ? `?prefix=${internalPaths}` : ""
|
||||
}`;
|
||||
|
||||
if (rewindEnabled) {
|
||||
const rewindParsed = rewindDate.toISOString();
|
||||
|
||||
let pathPrefix = "";
|
||||
if (internalPaths) {
|
||||
const decodedPath = decodeURLString(internalPaths);
|
||||
pathPrefix = decodedPath.endsWith("/")
|
||||
? decodedPath
|
||||
: decodedPath + "/";
|
||||
}
|
||||
|
||||
pathTest = `/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${
|
||||
pathPrefix ? `?prefix=${encodeURLString(pathPrefix)}` : ``
|
||||
}`;
|
||||
}
|
||||
|
||||
api
|
||||
.invoke("GET", pathTest)
|
||||
.then((res: BucketObjectItemsList) => {
|
||||
//It is a file since it has elements in the object, setting file flag and waiting for component mount
|
||||
if (!res.objects) {
|
||||
// It is a folder, we remove loader & set original results list
|
||||
dispatch(setLoadingObjectsList(false));
|
||||
setRecords(recordsInElement);
|
||||
} else {
|
||||
// This code prevents the program from opening a file when a substring of that file is entered as a new folder.
|
||||
// Previously, if there was a file test1.txt and the folder test was created with the same prefix, the program
|
||||
// would open test1.txt instead
|
||||
let found = false;
|
||||
let pathPrefixChopped = pathPrefix.slice(
|
||||
0,
|
||||
pathPrefix.length - 1
|
||||
);
|
||||
for (let i = 0; i < res.objects.length; i++) {
|
||||
if (res.objects[i].name === pathPrefixChopped) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (
|
||||
(res.objects.length === 1 &&
|
||||
res.objects[0].name.endsWith("/")) ||
|
||||
!found
|
||||
) {
|
||||
// This is a folder, we set the original results list
|
||||
setRecords(recordsInElement);
|
||||
} else {
|
||||
// This is a file. We change URL & Open file details view.
|
||||
dispatch(setObjectDetailsView(true));
|
||||
dispatch(setSelectedObjectView(internalPaths));
|
||||
|
||||
// We split the selected object URL & remove the last item to fetch the files list for the parent folder
|
||||
const parentPath = `${decodeURLString(internalPaths)
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/")}/`;
|
||||
|
||||
api
|
||||
.invoke(
|
||||
"GET",
|
||||
`${urlTake}${
|
||||
pathPrefix
|
||||
? `?prefix=${encodeURLString(parentPath)}`
|
||||
: ``
|
||||
}`
|
||||
)
|
||||
.then((res: BucketObjectItemsList) => {
|
||||
const records: BucketObjectItem[] = res.objects || [];
|
||||
|
||||
setRecords(records);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
dispatch(setLoadingObjectsList(false));
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
dispatch(setLoadingObjectsList(false));
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
});
|
||||
} else {
|
||||
setRecords(recordsInElement);
|
||||
dispatch(setLoadingObjectsList(false));
|
||||
}
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
const permitItems = permissionItems(
|
||||
bucketName,
|
||||
pathPrefix,
|
||||
allowResources || []
|
||||
);
|
||||
|
||||
if (!permitItems || permitItems.length === 0) {
|
||||
dispatch(setErrorSnackMessage(err));
|
||||
} else {
|
||||
setRecords(permitItems);
|
||||
}
|
||||
|
||||
dispatch(setLoadingObjectsList(false));
|
||||
});
|
||||
} else {
|
||||
dispatch(setLoadingObjectsList(false));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
loading,
|
||||
dispatch,
|
||||
bucketName,
|
||||
rewindEnabled,
|
||||
rewindDate,
|
||||
internalPaths,
|
||||
bucketInfo,
|
||||
showDeleted,
|
||||
displayListObjects,
|
||||
bucketToRewind,
|
||||
allowResources,
|
||||
]);
|
||||
|
||||
// bucket info
|
||||
useEffect(() => {
|
||||
if (loadingBucket) {
|
||||
@@ -769,7 +423,7 @@ const ListObjects = () => {
|
||||
|
||||
if (refresh) {
|
||||
dispatch(setSnackBarMessage(`Objects deleted successfully.`));
|
||||
setSelectedObjects([]);
|
||||
dispatch(setSelectedObjects([]));
|
||||
dispatch(setLoadingObjectsList(true));
|
||||
}
|
||||
};
|
||||
@@ -794,73 +448,6 @@ const ListObjects = () => {
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const downloadObject = (object: BucketObjectItem) => {
|
||||
const identityDownload = encodeURLString(
|
||||
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
|
||||
);
|
||||
|
||||
const ID = makeid(8);
|
||||
|
||||
const downloadCall = download(
|
||||
bucketName,
|
||||
encodeURLString(object.name),
|
||||
object.version_id,
|
||||
object.size,
|
||||
null,
|
||||
ID,
|
||||
(progress) => {
|
||||
dispatch(
|
||||
updateProgress({
|
||||
instanceID: identityDownload,
|
||||
progress: progress,
|
||||
})
|
||||
);
|
||||
},
|
||||
() => {
|
||||
dispatch(completeObject(identityDownload));
|
||||
},
|
||||
(msg: string) => {
|
||||
dispatch(failObject({ instanceID: identityDownload, msg }));
|
||||
},
|
||||
() => {
|
||||
dispatch(cancelObjectInList(identityDownload));
|
||||
}
|
||||
);
|
||||
storeCallForObjectWithID(ID, downloadCall);
|
||||
dispatch(
|
||||
setNewObject({
|
||||
ID,
|
||||
bucketName,
|
||||
done: false,
|
||||
instanceID: identityDownload,
|
||||
percentage: 0,
|
||||
prefix: object.name,
|
||||
type: "download",
|
||||
waitingForFile: true,
|
||||
failed: false,
|
||||
cancelled: false,
|
||||
errorMessage: "",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const openPath = (idElement: string) => {
|
||||
setSelectedObjects([]);
|
||||
|
||||
const newPath = `/buckets/${bucketName}/browse${
|
||||
idElement ? `/${encodeURLString(idElement)}` : ``
|
||||
}`;
|
||||
navigate(newPath);
|
||||
|
||||
dispatch(setObjectDetailsView(true));
|
||||
dispatch(setLoadingVersions(true));
|
||||
dispatch(
|
||||
setSelectedObjectView(
|
||||
`${idElement ? `${encodeURLString(idElement)}` : ``}`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const uploadObject = useCallback(
|
||||
(files: File[], folderPath: string): void => {
|
||||
let pathPrefix = "";
|
||||
@@ -1064,7 +651,7 @@ const ListObjects = () => {
|
||||
}
|
||||
// We force objects list reload after all promises were handled
|
||||
dispatch(setLoadingObjectsList(true));
|
||||
setSelectedObjects([]);
|
||||
dispatch(setSelectedObjects([]));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1110,140 +697,18 @@ const ListObjects = () => {
|
||||
[isDragActive, isDragAccept]
|
||||
);
|
||||
|
||||
const openPreview = () => {
|
||||
if (selectedObjects.length === 1) {
|
||||
let fileObject: BucketObjectItem | undefined;
|
||||
|
||||
const findFunction = (currValue: BucketObjectItem) =>
|
||||
selectedObjects.includes(currValue.name);
|
||||
|
||||
fileObject = filteredRecords.find(findFunction);
|
||||
|
||||
if (fileObject) {
|
||||
setSelectedPreview(fileObject);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openShare = () => {
|
||||
if (selectedObjects.length === 1) {
|
||||
let fileObject: BucketObjectItem | undefined;
|
||||
|
||||
const findFunction = (currValue: BucketObjectItem) =>
|
||||
selectedObjects.includes(currValue.name);
|
||||
|
||||
fileObject = filteredRecords.find(findFunction);
|
||||
|
||||
if (fileObject) {
|
||||
setSelectedPreview(fileObject);
|
||||
setShareFileModalOpen(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeShareModal = () => {
|
||||
setShareFileModalOpen(false);
|
||||
setSelectedPreview(null);
|
||||
dispatch(setShareFileModalOpen(false));
|
||||
dispatch(setSelectedPreview(null));
|
||||
};
|
||||
|
||||
const filteredRecords = records.filter((b: BucketObjectItem) => {
|
||||
if (searchObjects === "") {
|
||||
return true;
|
||||
} else {
|
||||
const objectName = b.name.toLowerCase();
|
||||
if (objectName.indexOf(searchObjects.toLowerCase()) >= 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const rewindCloseModal = () => {
|
||||
setRewindSelect(false);
|
||||
};
|
||||
|
||||
const closePreviewWindow = () => {
|
||||
setPreviewOpen(false);
|
||||
setSelectedPreview(null);
|
||||
};
|
||||
|
||||
const selectListObjects = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const targetD = e.target;
|
||||
const value = targetD.value;
|
||||
const checked = targetD.checked;
|
||||
|
||||
let elements: string[] = [...selectedObjects]; // We clone the selectedBuckets array
|
||||
|
||||
if (checked) {
|
||||
// If the user has checked this field we need to push this to selectedBucketsList
|
||||
elements.push(value);
|
||||
} else {
|
||||
// User has unchecked this field, we need to remove it from the list
|
||||
elements = elements.filter((element) => element !== value);
|
||||
}
|
||||
setSelectedObjects(elements);
|
||||
dispatch(setSelectedObjectView(null));
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
const sortChange = (sortData: any) => {
|
||||
const newSortDirection = get(sortData, "sortDirection", "DESC");
|
||||
setCurrentSortField(sortData.sortBy);
|
||||
setSortDirection(newSortDirection);
|
||||
dispatch(setLoadingObjectsList(true));
|
||||
};
|
||||
|
||||
const plSelect = filteredRecords;
|
||||
const sortASC = plSelect.sort(sortListObjects(currentSortField));
|
||||
|
||||
let payload: BucketObjectItem[] = [];
|
||||
|
||||
if (sortDirection === "ASC") {
|
||||
payload = sortASC;
|
||||
} else {
|
||||
payload = sortASC.reverse();
|
||||
}
|
||||
|
||||
const selectAllItems = () => {
|
||||
dispatch(setSelectedObjectView(null));
|
||||
|
||||
if (selectedObjects.length === payload.length) {
|
||||
setSelectedObjects([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = payload.map((item) => item.name);
|
||||
setSelectedObjects(elements);
|
||||
};
|
||||
|
||||
const downloadSelected = () => {
|
||||
if (selectedObjects.length !== 0) {
|
||||
let itemsToDownload: BucketObjectItem[] = [];
|
||||
|
||||
const filterFunction = (currValue: BucketObjectItem) =>
|
||||
selectedObjects.includes(currValue.name);
|
||||
|
||||
itemsToDownload = filteredRecords.filter(filterFunction);
|
||||
|
||||
// I case just one element is selected, then we trigger download modal validation.
|
||||
// We are going to enforce zip download when multiple files are selected
|
||||
if (itemsToDownload.length === 1) {
|
||||
if (
|
||||
itemsToDownload[0].name.length > 200 &&
|
||||
getClientOS().toLowerCase().includes("win")
|
||||
) {
|
||||
setDownloadRenameModal(itemsToDownload[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
itemsToDownload.forEach((filteredItem) => {
|
||||
downloadObject(filteredItem);
|
||||
});
|
||||
}
|
||||
dispatch(setPreviewOpen(false));
|
||||
dispatch(setSelectedPreview(null));
|
||||
};
|
||||
|
||||
const onClosePanel = (forceRefresh: boolean) => {
|
||||
@@ -1268,7 +733,7 @@ const ListObjects = () => {
|
||||
}
|
||||
|
||||
dispatch(setObjectDetailsView(false));
|
||||
setSelectedObjects([]);
|
||||
dispatch(setSelectedObjects([]));
|
||||
|
||||
if (forceRefresh) {
|
||||
dispatch(setLoadingObjectsList(true));
|
||||
@@ -1276,28 +741,22 @@ const ListObjects = () => {
|
||||
};
|
||||
|
||||
const setDeletedAction = () => {
|
||||
dispatch(resetMessages());
|
||||
dispatch(setShowDeletedObjects(!showDeleted));
|
||||
onClosePanel(true);
|
||||
};
|
||||
|
||||
const closeRenameModal = () => {
|
||||
setDownloadRenameModal(null);
|
||||
dispatch(setDownloadRenameModal(null));
|
||||
};
|
||||
|
||||
const tableActions: ItemActions[] = [
|
||||
{
|
||||
type: "view",
|
||||
label: "View",
|
||||
onClick: openPath,
|
||||
sendOnlyId: true,
|
||||
},
|
||||
];
|
||||
|
||||
const multiActionButtons = [
|
||||
{
|
||||
action: downloadSelected,
|
||||
action: () => {
|
||||
dispatch(downloadSelected(bucketName));
|
||||
},
|
||||
label: "Download",
|
||||
disabled: !canDownload || selectedObjects.length === 0,
|
||||
disabled: !canDownload || selectedObjects?.length === 0,
|
||||
icon: <DownloadIcon />,
|
||||
tooltip: canDownload
|
||||
? "Download Selected"
|
||||
@@ -1307,14 +766,18 @@ const ListObjects = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
action: openShare,
|
||||
action: () => {
|
||||
dispatch(openShare());
|
||||
},
|
||||
label: "Share",
|
||||
disabled: selectedObjects.length !== 1 || !canShareFile,
|
||||
icon: <ShareIcon />,
|
||||
tooltip: canShareFile ? "Share Selected File" : "Sharing unavailable",
|
||||
},
|
||||
{
|
||||
action: openPreview,
|
||||
action: () => {
|
||||
dispatch(openPreview());
|
||||
},
|
||||
label: "Preview",
|
||||
disabled: selectedObjects.length !== 1 || !canPreviewFile,
|
||||
icon: <PreviewIcon />,
|
||||
@@ -1484,6 +947,8 @@ const ListObjects = () => {
|
||||
if (versionsMode) {
|
||||
dispatch(setLoadingVersions(true));
|
||||
} else {
|
||||
dispatch(resetMessages());
|
||||
dispatch(setLoadingRecords(true));
|
||||
dispatch(setLoadingObjectsList(true));
|
||||
}
|
||||
}}
|
||||
@@ -1560,7 +1025,6 @@ const ListObjects = () => {
|
||||
<BrowserBreadcrumbs
|
||||
bucketName={bucketName}
|
||||
internalPaths={pageTitle}
|
||||
existingFiles={records || []}
|
||||
additionalOptions={
|
||||
!isVersioned || rewindEnabled ? null : (
|
||||
<div>
|
||||
@@ -1581,48 +1045,7 @@ const ListObjects = () => {
|
||||
hidePathButton={false}
|
||||
/>
|
||||
</Grid>
|
||||
<TableWrapper
|
||||
itemActions={tableActions}
|
||||
columns={
|
||||
rewindEnabled ? rewindModeColumns : listModeColumns
|
||||
}
|
||||
isLoading={loading}
|
||||
loadingMessage={loadingMessage}
|
||||
entityName="Objects"
|
||||
idField="name"
|
||||
records={payload}
|
||||
customPaperHeight={`${classes.browsePaper} ${
|
||||
obOnly ? "isEmbedded" : ""
|
||||
} ${detailsOpen ? "actionsPanelOpen" : ""}`}
|
||||
selectedItems={selectedObjects}
|
||||
onSelect={selectListObjects}
|
||||
customEmptyMessage={
|
||||
!displayListObjects
|
||||
? permissionTooltipHelper(
|
||||
[IAM_SCOPES.S3_LIST_BUCKET],
|
||||
"view Objects in this bucket"
|
||||
)
|
||||
: `This location is empty${
|
||||
!rewindEnabled
|
||||
? ", please try uploading a new file"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
sortConfig={{
|
||||
currentSort: currentSortField,
|
||||
currentDirection: sortDirection,
|
||||
triggerSort: sortChange,
|
||||
}}
|
||||
onSelectAll={selectAllItems}
|
||||
rowStyle={({ index }) => {
|
||||
if (payload[index]?.delete_flag) {
|
||||
return "deleted";
|
||||
}
|
||||
|
||||
return "";
|
||||
}}
|
||||
parentClassName={classes.parentWrapper}
|
||||
/>
|
||||
<ListObjectsTable />
|
||||
</Grid>
|
||||
</SecureComponent>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 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 { listModeColumns, rewindModeColumns } from "./ListObjectsHelpers";
|
||||
import TableWrapper, {
|
||||
ItemActions,
|
||||
} from "../../../../Common/TableWrapper/TableWrapper";
|
||||
import React, { useState } from "react";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState, useAppDispatch } from "../../../../../../store";
|
||||
import { selFeatures } from "../../../../consoleSlice";
|
||||
import { encodeURLString } from "../../../../../../common/utils";
|
||||
import {
|
||||
setLoadingObjectsList,
|
||||
setLoadingVersions,
|
||||
setObjectDetailsView,
|
||||
setSelectedObjects,
|
||||
setSelectedObjectView,
|
||||
} from "../../../../ObjectBrowser/objectBrowserSlice";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import get from "lodash/get";
|
||||
import { sortListObjects } from "../utils";
|
||||
import { BucketObjectItem } from "./types";
|
||||
import {
|
||||
IAM_SCOPES,
|
||||
permissionTooltipHelper,
|
||||
} from "../../../../../../common/SecureComponent/permissions";
|
||||
import { hasPermission } from "../../../../../../common/SecureComponent";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
browsePaper: {
|
||||
border: 0,
|
||||
height: "calc(100vh - 290px)",
|
||||
"&.isEmbedded": {
|
||||
height: "calc(100vh - 315px)",
|
||||
},
|
||||
"&.actionsPanelOpen": {
|
||||
minHeight: "100%",
|
||||
},
|
||||
"@media (max-width: 800px)": {
|
||||
width: 800,
|
||||
},
|
||||
},
|
||||
parentWrapper: {
|
||||
position: "relative",
|
||||
height: "calc(100% - 60px)",
|
||||
"@media (max-width: 800px)": {
|
||||
overflowX: "auto",
|
||||
},
|
||||
},
|
||||
"@global": {
|
||||
".rowLine:hover .iconFileElm": {
|
||||
backgroundImage: "url(/images/ob_file_filled.svg)",
|
||||
},
|
||||
".rowLine:hover .iconFolderElm": {
|
||||
backgroundImage: "url(/images/ob_folder_filled.svg)",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const ListObjectsTable = () => {
|
||||
const classes = useStyles();
|
||||
const dispatch = useAppDispatch();
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [sortDirection, setSortDirection] = useState<
|
||||
"ASC" | "DESC" | undefined
|
||||
>("ASC");
|
||||
const [currentSortField, setCurrentSortField] = useState<string>("name");
|
||||
|
||||
const bucketName = params.bucketName || "";
|
||||
|
||||
const detailsOpen = useSelector(
|
||||
(state: AppState) => state.objectBrowser.objectDetailsOpen
|
||||
);
|
||||
|
||||
const loadingObjects = useSelector(
|
||||
(state: AppState) => state.objectBrowser.loadingObjects
|
||||
);
|
||||
|
||||
const features = useSelector(selFeatures);
|
||||
const obOnly = !!features?.includes("object-browser-only");
|
||||
|
||||
const rewindEnabled = useSelector(
|
||||
(state: AppState) => state.objectBrowser.rewind.rewindEnabled
|
||||
);
|
||||
const records = useSelector((state: AppState) => state.objectBrowser.records);
|
||||
const searchObjects = useSelector(
|
||||
(state: AppState) => state.objectBrowser.searchObjects
|
||||
);
|
||||
const selectedObjects = useSelector(
|
||||
(state: AppState) => state.objectBrowser.selectedObjects
|
||||
);
|
||||
|
||||
const displayListObjects = hasPermission(bucketName, [
|
||||
IAM_SCOPES.S3_LIST_BUCKET,
|
||||
]);
|
||||
|
||||
const filteredRecords = records.filter((b: BucketObjectItem) => {
|
||||
if (searchObjects === "") {
|
||||
return true;
|
||||
} else {
|
||||
const objectName = b.name.toLowerCase();
|
||||
if (objectName.indexOf(searchObjects.toLowerCase()) >= 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const plSelect = filteredRecords;
|
||||
const sortASC = plSelect.sort(sortListObjects(currentSortField));
|
||||
|
||||
let payload: BucketObjectItem[] = [];
|
||||
|
||||
if (sortDirection === "ASC") {
|
||||
payload = sortASC;
|
||||
} else {
|
||||
payload = sortASC.reverse();
|
||||
}
|
||||
|
||||
const openPath = (idElement: string) => {
|
||||
dispatch(setSelectedObjects([]));
|
||||
|
||||
const newPath = `/buckets/${bucketName}/browse${
|
||||
idElement ? `/${encodeURLString(idElement)}` : ``
|
||||
}`;
|
||||
navigate(newPath);
|
||||
|
||||
dispatch(setObjectDetailsView(true));
|
||||
dispatch(setLoadingVersions(true));
|
||||
dispatch(
|
||||
setSelectedObjectView(
|
||||
`${idElement ? `${encodeURLString(idElement)}` : ``}`
|
||||
)
|
||||
);
|
||||
};
|
||||
const tableActions: ItemActions[] = [
|
||||
{
|
||||
type: "view",
|
||||
label: "View",
|
||||
onClick: openPath,
|
||||
sendOnlyId: true,
|
||||
},
|
||||
];
|
||||
|
||||
const sortChange = (sortData: any) => {
|
||||
const newSortDirection = get(sortData, "sortDirection", "DESC");
|
||||
setCurrentSortField(sortData.sortBy);
|
||||
setSortDirection(newSortDirection);
|
||||
dispatch(setLoadingObjectsList(true));
|
||||
};
|
||||
|
||||
const selectAllItems = () => {
|
||||
dispatch(setSelectedObjectView(null));
|
||||
|
||||
if (selectedObjects.length === payload.length) {
|
||||
dispatch(setSelectedObjects([]));
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = payload.map((item) => item.name);
|
||||
dispatch(setSelectedObjects(elements));
|
||||
};
|
||||
|
||||
const selectListObjects = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const targetD = e.target;
|
||||
const value = targetD.value;
|
||||
const checked = targetD.checked;
|
||||
|
||||
let elements: string[] = [...selectedObjects]; // We clone the selectedBuckets array
|
||||
|
||||
if (checked) {
|
||||
// If the user has checked this field we need to push this to selectedBucketsList
|
||||
elements.push(value);
|
||||
} else {
|
||||
// User has unchecked this field, we need to remove it from the list
|
||||
elements = elements.filter((element) => element !== value);
|
||||
}
|
||||
dispatch(setSelectedObjects(elements));
|
||||
dispatch(setSelectedObjectView(null));
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
return (
|
||||
<TableWrapper
|
||||
itemActions={tableActions}
|
||||
columns={rewindEnabled ? rewindModeColumns : listModeColumns}
|
||||
isLoading={loadingObjects}
|
||||
entityName="Objects"
|
||||
idField="name"
|
||||
records={payload}
|
||||
customPaperHeight={`${classes.browsePaper} ${
|
||||
obOnly ? "isEmbedded" : ""
|
||||
} ${detailsOpen ? "actionsPanelOpen" : ""}`}
|
||||
selectedItems={selectedObjects}
|
||||
onSelect={selectListObjects}
|
||||
customEmptyMessage={
|
||||
!displayListObjects
|
||||
? permissionTooltipHelper(
|
||||
[IAM_SCOPES.S3_LIST_BUCKET],
|
||||
"view Objects in this bucket"
|
||||
)
|
||||
: `This location is empty${
|
||||
!rewindEnabled ? ", please try uploading a new file" : ""
|
||||
}`
|
||||
}
|
||||
sortConfig={{
|
||||
currentSort: currentSortField,
|
||||
currentDirection: sortDirection,
|
||||
triggerSort: sortChange,
|
||||
}}
|
||||
onSelectAll={selectAllItems}
|
||||
rowStyle={({ index }) => {
|
||||
if (payload[index]?.delete_flag) {
|
||||
return "deleted";
|
||||
}
|
||||
|
||||
return "";
|
||||
}}
|
||||
parentClassName={classes.parentWrapper}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ListObjectsTable;
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
spacingUtils,
|
||||
textStyleUtils,
|
||||
} from "../../../../Common/FormComponents/common/styleLibrary";
|
||||
import { IFileInfo } from "../ObjectDetails/types";
|
||||
import { IFileInfo, MetadataResponse } from "../ObjectDetails/types";
|
||||
import { download, extensionPreview } from "../utils";
|
||||
import { ErrorResponseHandler } from "../../../../../../common/types";
|
||||
|
||||
@@ -189,6 +189,8 @@ const ObjectDetailPanel = ({
|
||||
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
|
||||
const [totalVersionsSize, setTotalVersionsSize] = useState<number>(0);
|
||||
const [longFileOpen, setLongFileOpen] = useState<boolean>(false);
|
||||
const [metaData, setMetaData] = useState<any | null>(null);
|
||||
const [loadMetadata, setLoadingMetadata] = useState<boolean>(false);
|
||||
|
||||
const internalPathsDecoded = decodeURLString(internalPaths) || "";
|
||||
const allPathData = internalPathsDecoded.split("/");
|
||||
@@ -212,6 +214,10 @@ const ObjectDetailPanel = ({
|
||||
) || emptyFile;
|
||||
}
|
||||
|
||||
if (!infoElement.is_delete_marker) {
|
||||
setLoadingMetadata(true);
|
||||
}
|
||||
|
||||
setActualInfo(infoElement);
|
||||
}
|
||||
}, [selectedVersion, distributedSetup, allInfoElements]);
|
||||
@@ -242,8 +248,14 @@ const ObjectDetailPanel = ({
|
||||
|
||||
setTotalVersionsSize(tVersionSize);
|
||||
} else {
|
||||
setActualInfo(result[0]);
|
||||
const resInfo = result[0];
|
||||
|
||||
setActualInfo(resInfo);
|
||||
setVersions([]);
|
||||
|
||||
if (!resInfo.is_delete_marker) {
|
||||
setLoadingMetadata(true);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(setLoadingObjectInfo(false));
|
||||
@@ -262,6 +274,26 @@ const ObjectDetailPanel = ({
|
||||
selectedVersion,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadMetadata && internalPaths !== "") {
|
||||
api
|
||||
.invoke(
|
||||
"GET",
|
||||
`/api/v1/buckets/${bucketName}/objects/metadata?prefix=${internalPaths}`
|
||||
)
|
||||
.then((res: MetadataResponse) => {
|
||||
let metadata = get(res, "objectMetadata", {});
|
||||
|
||||
setMetaData(metadata);
|
||||
setLoadingMetadata(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error Getting Metadata Status: ", err.detailedError);
|
||||
setLoadingMetadata(false);
|
||||
});
|
||||
}
|
||||
}, [bucketName, internalPaths, loadMetadata]);
|
||||
|
||||
let tagKeys: string[] = [];
|
||||
|
||||
if (actualInfo && actualInfo.tags) {
|
||||
@@ -649,7 +681,7 @@ const ObjectDetailPanel = ({
|
||||
version_id: actualInfo.version_id || "null",
|
||||
size: parseInt(actualInfo.size || "0"),
|
||||
content_type: "",
|
||||
last_modified: new Date(actualInfo.last_modified),
|
||||
last_modified: actualInfo.last_modified,
|
||||
}}
|
||||
onClosePreview={() => {
|
||||
setPreviewOpen(false);
|
||||
@@ -838,20 +870,19 @@ const ObjectDetailPanel = ({
|
||||
</Fragment>
|
||||
</SecureComponent>
|
||||
</Box>
|
||||
<Grid item xs={12} className={classes.headerForSection}>
|
||||
<span>Metadata</span>
|
||||
<MetadataIcon />
|
||||
</Grid>
|
||||
<Box className={classes.detailContainer}>
|
||||
{actualInfo ? (
|
||||
<ObjectMetaData
|
||||
bucketName={bucketName}
|
||||
internalPaths={internalPaths}
|
||||
actualInfo={actualInfo}
|
||||
linear
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
{!actualInfo.is_delete_marker && (
|
||||
<Fragment>
|
||||
<Grid item xs={12} className={classes.headerForSection}>
|
||||
<span>Metadata</span>
|
||||
<MetadataIcon />
|
||||
</Grid>
|
||||
<Box className={classes.detailContainer}>
|
||||
{actualInfo && metaData ? (
|
||||
<ObjectMetaData metaData={metaData} linear />
|
||||
) : null}
|
||||
</Box>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
@@ -14,17 +14,48 @@
|
||||
// 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 { IFileInfo } from "../ObjectDetails/types";
|
||||
|
||||
export interface BucketObjectItem {
|
||||
name: string;
|
||||
size: number;
|
||||
etag?: string;
|
||||
last_modified: Date;
|
||||
last_modified: string;
|
||||
content_type?: string;
|
||||
version_id: string;
|
||||
delete_flag?: boolean;
|
||||
is_latest?: boolean;
|
||||
}
|
||||
|
||||
export interface BucketObjectItemsList {
|
||||
objects: BucketObjectItem[];
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export interface WebsocketRequest {
|
||||
mode: "objects" | "rewind" | "close" | "cancel";
|
||||
bucket_name?: string;
|
||||
prefix?: string;
|
||||
date?: string;
|
||||
request_id: number;
|
||||
}
|
||||
|
||||
export interface WebsocketResponse {
|
||||
request_id: number;
|
||||
error?: string;
|
||||
request_end?: boolean;
|
||||
data?: ObjectResponse[];
|
||||
}
|
||||
export interface ObjectResponse {
|
||||
name: string;
|
||||
last_modified: string;
|
||||
size: number;
|
||||
version_id: string;
|
||||
delete_flag: boolean;
|
||||
is_latest: boolean;
|
||||
}
|
||||
|
||||
export interface IRestoreLocalObjectList {
|
||||
prefix: string;
|
||||
objectInfo: IFileInfo;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import React, { Fragment, useCallback, useEffect, useState } from "react";
|
||||
import useApi from "../../../../Common/Hooks/useApi";
|
||||
import { ErrorResponseHandler } from "../../../../../../common/types";
|
||||
import { MetadataResponse } from "./types";
|
||||
import get from "lodash/get";
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 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, { Fragment } from "react";
|
||||
import { withStyles } from "@mui/styles";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { Box, Table, TableBody, TableCell, TableRow } from "@mui/material";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
@@ -11,13 +24,10 @@ import {
|
||||
detailsPanel,
|
||||
spacingUtils,
|
||||
} from "../../../../Common/FormComponents/common/styleLibrary";
|
||||
import { withStyles } from "@mui/styles";
|
||||
|
||||
interface IObjectMetadata {
|
||||
bucketName: string;
|
||||
internalPaths: string;
|
||||
metaData: any;
|
||||
classes?: any;
|
||||
actualInfo: any;
|
||||
linear?: boolean;
|
||||
}
|
||||
|
||||
@@ -45,38 +55,11 @@ const styles = (theme: Theme) =>
|
||||
});
|
||||
|
||||
const ObjectMetaData = ({
|
||||
bucketName,
|
||||
internalPaths,
|
||||
metaData,
|
||||
classes,
|
||||
actualInfo,
|
||||
linear = false,
|
||||
}: IObjectMetadata) => {
|
||||
const [metaData, setMetaData] = useState<any>({});
|
||||
|
||||
const onMetaDataSuccess = (res: MetadataResponse) => {
|
||||
let metadata = get(res, "objectMetadata", {});
|
||||
|
||||
setMetaData(metadata);
|
||||
};
|
||||
const onMetaDataError = (err: ErrorResponseHandler) => false;
|
||||
|
||||
const [, invokeMetaDataApi] = useApi(onMetaDataSuccess, onMetaDataError);
|
||||
|
||||
const metaKeys = Object.keys(metaData);
|
||||
const loadMetaData = useCallback(() => {
|
||||
invokeMetaDataApi(
|
||||
"GET",
|
||||
`/api/v1/buckets/${bucketName}/objects/metadata?prefix=${internalPaths}`
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bucketName, internalPaths, actualInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (actualInfo) {
|
||||
loadMetaData();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actualInfo, loadMetaData]);
|
||||
|
||||
if (linear) {
|
||||
return (
|
||||
|
||||
@@ -29,12 +29,14 @@ import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
|
||||
import RecoverIcon from "../../../../../../icons/RecoverIcon";
|
||||
import { setErrorSnackMessage } from "../../../../../../systemSlice";
|
||||
import { useAppDispatch } from "../../../../../../store";
|
||||
import { IFileInfo } from "./types";
|
||||
import { restoreLocalObjectList } from "../../../../ObjectBrowser/objectBrowserSlice";
|
||||
|
||||
interface IRestoreFileVersion {
|
||||
classes: any;
|
||||
restoreOpen: boolean;
|
||||
bucketName: string;
|
||||
versionID: string;
|
||||
versionToRestore: IFileInfo;
|
||||
objectPath: string;
|
||||
onCloseAndUpdate: (refresh: boolean) => void;
|
||||
}
|
||||
@@ -46,7 +48,7 @@ const styles = (theme: Theme) =>
|
||||
|
||||
const RestoreFileVersion = ({
|
||||
classes,
|
||||
versionID,
|
||||
versionToRestore,
|
||||
bucketName,
|
||||
objectPath,
|
||||
restoreOpen,
|
||||
@@ -63,11 +65,18 @@ const RestoreFileVersion = ({
|
||||
"PUT",
|
||||
`/api/v1/buckets/${bucketName}/objects/restore?prefix=${encodeURLString(
|
||||
objectPath
|
||||
)}&version_id=${versionID}`
|
||||
)}&version_id=${versionToRestore.version_id}`
|
||||
)
|
||||
.then((res: any) => {
|
||||
console.log("REStORE", res);
|
||||
setRestoreLoading(false);
|
||||
onCloseAndUpdate(true);
|
||||
dispatch(
|
||||
restoreLocalObjectList({
|
||||
prefix: objectPath,
|
||||
objectInfo: versionToRestore,
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((error: ErrorResponseHandler) => {
|
||||
dispatch(setErrorSnackMessage(error));
|
||||
@@ -95,7 +104,7 @@ const RestoreFileVersion = ({
|
||||
Are you sure you want to restore <br />
|
||||
<b>{objectPath}</b> <br /> with Version ID:
|
||||
<br />
|
||||
<b className={classes.wrapText}>{versionID}</b>?
|
||||
<b className={classes.wrapText}>{versionToRestore.version_id}</b>?
|
||||
</DialogContentText>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -181,7 +181,7 @@ const VersionsNavigator = ({
|
||||
const [objectToShare, setObjectToShare] = useState<IFileInfo | null>(null);
|
||||
const [versions, setVersions] = useState<IFileInfo[]>([]);
|
||||
const [restoreVersionOpen, setRestoreVersionOpen] = useState<boolean>(false);
|
||||
const [restoreVersion, setRestoreVersion] = useState<string>("");
|
||||
const [restoreVersion, setRestoreVersion] = useState<IFileInfo | null>(null);
|
||||
const [sortValue, setSortValue] = useState<string>("date");
|
||||
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
|
||||
const [deleteNonCurrentOpen, setDeleteNonCurrentOpen] =
|
||||
@@ -313,7 +313,7 @@ const VersionsNavigator = ({
|
||||
};
|
||||
|
||||
const onRestoreItem = (item: IFileInfo) => {
|
||||
setRestoreVersion(item.version_id || "");
|
||||
setRestoreVersion(item);
|
||||
setRestoreVersionOpen(true);
|
||||
};
|
||||
|
||||
@@ -334,7 +334,7 @@ const VersionsNavigator = ({
|
||||
|
||||
const closeRestoreModal = (reloadObjectData: boolean) => {
|
||||
setRestoreVersionOpen(false);
|
||||
setRestoreVersion("");
|
||||
setRestoreVersion(null);
|
||||
|
||||
if (reloadObjectData) {
|
||||
dispatch(setLoadingVersions(true));
|
||||
@@ -454,11 +454,11 @@ const VersionsNavigator = ({
|
||||
dataObject={objectToShare || actualInfo}
|
||||
/>
|
||||
)}
|
||||
{restoreVersionOpen && actualInfo && (
|
||||
{restoreVersionOpen && actualInfo && restoreVersion && (
|
||||
<RestoreFileVersion
|
||||
restoreOpen={restoreVersionOpen}
|
||||
bucketName={bucketName}
|
||||
versionID={restoreVersion}
|
||||
versionToRestore={restoreVersion}
|
||||
objectPath={actualInfo.name}
|
||||
onCloseAndUpdate={closeRestoreModal}
|
||||
/>
|
||||
@@ -477,7 +477,7 @@ const VersionsNavigator = ({
|
||||
objectToShare && objectToShare.size ? objectToShare.size : "0"
|
||||
),
|
||||
content_type: "",
|
||||
last_modified: new Date(actualInfo.last_modified),
|
||||
last_modified: actualInfo.last_modified,
|
||||
}}
|
||||
onClosePreview={() => {
|
||||
setPreviewOpen(false);
|
||||
@@ -514,7 +514,6 @@ const VersionsNavigator = ({
|
||||
<BrowserBreadcrumbs
|
||||
bucketName={bucketName}
|
||||
internalPaths={decodeURLString(internalPaths)}
|
||||
existingFiles={[]}
|
||||
hidePathButton={true}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -247,7 +247,7 @@ export const permissionItems = (
|
||||
returnElements.push({
|
||||
name: `${currentElementInPath}/`,
|
||||
size: 0,
|
||||
last_modified: new Date(),
|
||||
last_modified: "",
|
||||
version_id: "",
|
||||
});
|
||||
}
|
||||
@@ -276,7 +276,7 @@ export const permissionItems = (
|
||||
pathToRouteElements.length > 0 ? "/" : ""
|
||||
}${splitElement}/`,
|
||||
size: 0,
|
||||
last_modified: new Date(),
|
||||
last_modified: "",
|
||||
version_id: "",
|
||||
});
|
||||
return false;
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
IAM_SCOPES,
|
||||
permissionTooltipHelper,
|
||||
} from "../../../common/SecureComponent/permissions";
|
||||
import { BucketObjectItem } from "../Buckets/ListBuckets/Objects/ListObjects/types";
|
||||
import withSuspense from "../Common/Components/withSuspense";
|
||||
import { setSnackBarMessage } from "../../../systemSlice";
|
||||
import { AppState, useAppDispatch } from "../../../store";
|
||||
@@ -58,7 +57,6 @@ interface IObjectBrowser {
|
||||
bucketName: string;
|
||||
internalPaths: string;
|
||||
hidePathButton?: boolean;
|
||||
existingFiles: BucketObjectItem[];
|
||||
additionalOptions?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -66,7 +64,6 @@ const BrowserBreadcrumbs = ({
|
||||
classes,
|
||||
bucketName,
|
||||
internalPaths,
|
||||
existingFiles,
|
||||
hidePathButton,
|
||||
additionalOptions,
|
||||
}: IObjectBrowser) => {
|
||||
@@ -176,7 +173,6 @@ const BrowserBreadcrumbs = ({
|
||||
bucketName={bucketName}
|
||||
folderName={internalPaths}
|
||||
onClose={closeAddFolderModal}
|
||||
existingFiles={existingFiles}
|
||||
/>
|
||||
)}
|
||||
<Grid item xs={12} className={`${classes.breadcrumbs}`}>
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { IFileItem, ObjectBrowserState } from "./types";
|
||||
import {
|
||||
BucketObjectItem,
|
||||
IRestoreLocalObjectList,
|
||||
} from "../Buckets/ListBuckets/Objects/ListObjects/types";
|
||||
|
||||
const defaultRewind = {
|
||||
rewindEnabled: false,
|
||||
@@ -47,6 +51,18 @@ const initialState: ObjectBrowserState = {
|
||||
showDeleted: false,
|
||||
selectedInternalPaths: null,
|
||||
simplePath: null,
|
||||
// object browser
|
||||
records: [],
|
||||
loadRecords: true,
|
||||
loadingVersioning: true,
|
||||
isVersioned: false,
|
||||
lockingEnabled: false,
|
||||
loadingLocking: false,
|
||||
selectedObjects: [],
|
||||
downloadRenameModal: null,
|
||||
selectedPreview: null,
|
||||
previewOpen: false,
|
||||
shareFileModalOpen: false,
|
||||
};
|
||||
|
||||
export const objectBrowserSlice = createSlice({
|
||||
@@ -259,6 +275,67 @@ export const objectBrowserSlice = createSlice({
|
||||
action.payload,
|
||||
];
|
||||
},
|
||||
setRecords: (state, action: PayloadAction<BucketObjectItem[]>) => {
|
||||
state.records = action.payload;
|
||||
},
|
||||
setLoadingVersioning: (state, action: PayloadAction<boolean>) => {
|
||||
state.loadingVersioning = action.payload;
|
||||
},
|
||||
setIsVersioned: (state, action: PayloadAction<boolean>) => {
|
||||
state.isVersioned = action.payload;
|
||||
},
|
||||
setLockingEnabled: (state, action: PayloadAction<boolean>) => {
|
||||
state.lockingEnabled = action.payload;
|
||||
},
|
||||
setLoadingLocking: (state, action: PayloadAction<boolean>) => {
|
||||
state.loadingLocking = action.payload;
|
||||
},
|
||||
newMessage: (state, action: PayloadAction<BucketObjectItem[]>) => {
|
||||
state.records = [...state.records, ...action.payload];
|
||||
},
|
||||
resetMessages: (state) => {
|
||||
state.records = [];
|
||||
},
|
||||
setLoadingRecords: (state, action: PayloadAction<boolean>) => {
|
||||
state.loadRecords = action.payload;
|
||||
},
|
||||
setSelectedObjects: (state, action: PayloadAction<string[]>) => {
|
||||
state.selectedObjects = action.payload;
|
||||
},
|
||||
setDownloadRenameModal: (
|
||||
state,
|
||||
action: PayloadAction<BucketObjectItem | null>
|
||||
) => {
|
||||
state.downloadRenameModal = action.payload;
|
||||
},
|
||||
setSelectedPreview: (
|
||||
state,
|
||||
action: PayloadAction<BucketObjectItem | null>
|
||||
) => {
|
||||
state.selectedPreview = action.payload;
|
||||
},
|
||||
setPreviewOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.previewOpen = action.payload;
|
||||
},
|
||||
setShareFileModalOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.shareFileModalOpen = action.payload;
|
||||
},
|
||||
restoreLocalObjectList: (
|
||||
state,
|
||||
action: PayloadAction<IRestoreLocalObjectList>
|
||||
) => {
|
||||
const indexToReplace = state.records.findIndex(
|
||||
(element) => element.name === action.payload.prefix
|
||||
);
|
||||
|
||||
if (indexToReplace >= 0) {
|
||||
state.records[indexToReplace].delete_flag =
|
||||
action.payload.objectInfo.is_delete_marker;
|
||||
state.records[indexToReplace].size = parseInt(
|
||||
action.payload.objectInfo.size || "0"
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
export const {
|
||||
@@ -287,6 +364,20 @@ export const {
|
||||
setSimplePathHandler,
|
||||
newDownloadInit,
|
||||
newUploadInit,
|
||||
setRecords,
|
||||
resetMessages,
|
||||
setLoadingVersioning,
|
||||
setIsVersioned,
|
||||
setLoadingLocking,
|
||||
setLockingEnabled,
|
||||
newMessage,
|
||||
setSelectedObjects,
|
||||
setDownloadRenameModal,
|
||||
setSelectedPreview,
|
||||
setPreviewOpen,
|
||||
setShareFileModalOpen,
|
||||
setLoadingRecords,
|
||||
restoreLocalObjectList,
|
||||
} = objectBrowserSlice.actions;
|
||||
|
||||
export default objectBrowserSlice.reducer;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 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 { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { AppState } from "../../../store";
|
||||
import { encodeURLString, getClientOS } from "../../../common/utils";
|
||||
import { BucketObjectItem } from "../Buckets/ListBuckets/Objects/ListObjects/types";
|
||||
import { makeid, storeCallForObjectWithID } from "./transferManager";
|
||||
import { download } from "../Buckets/ListBuckets/Objects/utils";
|
||||
import {
|
||||
cancelObjectInList,
|
||||
completeObject,
|
||||
failObject,
|
||||
setDownloadRenameModal,
|
||||
setNewObject,
|
||||
setPreviewOpen,
|
||||
setSelectedPreview,
|
||||
setShareFileModalOpen,
|
||||
updateProgress,
|
||||
} from "./objectBrowserSlice";
|
||||
|
||||
export const downloadSelected = createAsyncThunk(
|
||||
"objectBrowser/downloadSelected",
|
||||
async (bucketName: string, { getState, rejectWithValue, dispatch }) => {
|
||||
const state = getState() as AppState;
|
||||
|
||||
const downloadObject = (object: BucketObjectItem) => {
|
||||
const identityDownload = encodeURLString(
|
||||
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
|
||||
);
|
||||
|
||||
const ID = makeid(8);
|
||||
|
||||
const downloadCall = download(
|
||||
bucketName,
|
||||
encodeURLString(object.name),
|
||||
object.version_id,
|
||||
object.size,
|
||||
null,
|
||||
ID,
|
||||
(progress) => {
|
||||
dispatch(
|
||||
updateProgress({
|
||||
instanceID: identityDownload,
|
||||
progress: progress,
|
||||
})
|
||||
);
|
||||
},
|
||||
() => {
|
||||
dispatch(completeObject(identityDownload));
|
||||
},
|
||||
(msg: string) => {
|
||||
dispatch(failObject({ instanceID: identityDownload, msg }));
|
||||
},
|
||||
() => {
|
||||
dispatch(cancelObjectInList(identityDownload));
|
||||
}
|
||||
);
|
||||
storeCallForObjectWithID(ID, downloadCall);
|
||||
dispatch(
|
||||
setNewObject({
|
||||
ID,
|
||||
bucketName,
|
||||
done: false,
|
||||
instanceID: identityDownload,
|
||||
percentage: 0,
|
||||
prefix: object.name,
|
||||
type: "download",
|
||||
waitingForFile: true,
|
||||
failed: false,
|
||||
cancelled: false,
|
||||
errorMessage: "",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
if (state.objectBrowser.selectedObjects.length !== 0) {
|
||||
let itemsToDownload: BucketObjectItem[] = [];
|
||||
|
||||
const filterFunction = (currValue: BucketObjectItem) =>
|
||||
state.objectBrowser.selectedObjects.includes(currValue.name);
|
||||
|
||||
itemsToDownload = state.objectBrowser.records.filter(filterFunction);
|
||||
|
||||
// I case just one element is selected, then we trigger download modal validation.
|
||||
// We are going to enforce zip download when multiple files are selected
|
||||
if (itemsToDownload.length === 1) {
|
||||
if (
|
||||
itemsToDownload[0].name.length > 200 &&
|
||||
getClientOS().toLowerCase().includes("win")
|
||||
) {
|
||||
dispatch(setDownloadRenameModal(itemsToDownload[0]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
itemsToDownload.forEach((filteredItem) => {
|
||||
downloadObject(filteredItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const openPreview = createAsyncThunk(
|
||||
"objectBrowser/openPreview",
|
||||
async (_, { getState, rejectWithValue, dispatch }) => {
|
||||
const state = getState() as AppState;
|
||||
|
||||
if (state.objectBrowser.selectedObjects.length === 1) {
|
||||
let fileObject: BucketObjectItem | undefined;
|
||||
|
||||
const findFunction = (currValue: BucketObjectItem) =>
|
||||
state.objectBrowser.selectedObjects.includes(currValue.name);
|
||||
|
||||
fileObject = state.objectBrowser.records.find(findFunction);
|
||||
|
||||
if (fileObject) {
|
||||
dispatch(setSelectedPreview(fileObject));
|
||||
dispatch(setPreviewOpen(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const openShare = createAsyncThunk(
|
||||
"objectBrowser/openShare",
|
||||
async (_, { getState, rejectWithValue, dispatch }) => {
|
||||
const state = getState() as AppState;
|
||||
|
||||
if (state.objectBrowser.selectedObjects.length === 1) {
|
||||
let fileObject: BucketObjectItem | undefined;
|
||||
|
||||
const findFunction = (currValue: BucketObjectItem) =>
|
||||
state.objectBrowser.selectedObjects.includes(currValue.name);
|
||||
|
||||
fileObject = state.objectBrowser.records.find(findFunction);
|
||||
|
||||
if (fileObject) {
|
||||
dispatch(setSelectedPreview(fileObject));
|
||||
dispatch(setShareFileModalOpen(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -14,6 +14,8 @@
|
||||
// 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 { BucketObjectItem } from "../Buckets/ListBuckets/Objects/ListObjects/types";
|
||||
|
||||
export const REWIND_SET_ENABLE = "REWIND/SET_ENABLE";
|
||||
export const REWIND_RESET_REWIND = "REWIND/RESET_REWIND";
|
||||
|
||||
@@ -76,6 +78,17 @@ export interface ObjectBrowserState {
|
||||
objectDetailsOpen: boolean;
|
||||
selectedInternalPaths: string | null;
|
||||
simplePath: string | null;
|
||||
records: BucketObjectItem[];
|
||||
loadRecords: boolean;
|
||||
loadingVersioning: boolean;
|
||||
isVersioned: boolean;
|
||||
lockingEnabled: boolean;
|
||||
loadingLocking: boolean;
|
||||
selectedObjects: string[];
|
||||
downloadRenameModal: BucketObjectItem | null;
|
||||
selectedPreview: BucketObjectItem | null;
|
||||
previewOpen: boolean;
|
||||
shareFileModalOpen: boolean;
|
||||
}
|
||||
|
||||
export interface ObjectManager {
|
||||
|
||||
102
restapi/admin_objects.go
Normal file
102
restapi/admin_objects.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 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 restapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/minio/mc/cmd"
|
||||
"github.com/minio/minio-go/v7"
|
||||
)
|
||||
|
||||
type objectsListOpts struct {
|
||||
BucketName string
|
||||
Prefix string
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
type ObjectsRequest struct {
|
||||
Mode string `json:"mode,nonempty"`
|
||||
BucketName string `json:"bucket_name"`
|
||||
Prefix string `json:"prefix"`
|
||||
Date string `json:"date"`
|
||||
RequestID int64 `json:"request_id"`
|
||||
}
|
||||
|
||||
type WSResponse struct {
|
||||
RequestID int64 `json:"request_id,nonempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
RequestEnd bool `json:"request_end,omitempty"`
|
||||
Data []ObjectResponse `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type ObjectResponse struct {
|
||||
Name string `json:"name,nonempty"`
|
||||
LastModified string `json:"last_modified,nonempty"`
|
||||
Size int64 `json:"size,nonempty"`
|
||||
VersionID string `json:"version_id,nonempty"`
|
||||
DeleteMarker bool `json:"delete_flag,omitempty"`
|
||||
IsLatest bool `json:"is_latest,omitempty"`
|
||||
}
|
||||
|
||||
func getObjectsOptionsFromReq(request ObjectsRequest) (*objectsListOpts, error) {
|
||||
pOptions := objectsListOpts{
|
||||
BucketName: request.BucketName,
|
||||
Prefix: "",
|
||||
}
|
||||
|
||||
prefix := request.Prefix
|
||||
|
||||
if prefix != "" {
|
||||
encodedPrefix := SanitizeEncodedPrefix(prefix)
|
||||
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
|
||||
if err != nil {
|
||||
LogError("error decoding prefix: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pOptions.Prefix = string(decodedPrefix)
|
||||
}
|
||||
|
||||
if request.Mode == "rewind" {
|
||||
parsedDate, errDate := time.Parse(time.RFC3339, request.Date)
|
||||
|
||||
if errDate != nil {
|
||||
return nil, errDate
|
||||
}
|
||||
|
||||
pOptions.Date = parsedDate
|
||||
}
|
||||
|
||||
return &pOptions, nil
|
||||
}
|
||||
|
||||
func startObjectsListing(ctx context.Context, client MinioClient, objOpts *objectsListOpts) <-chan minio.ObjectInfo {
|
||||
opts := minio.ListObjectsOptions{
|
||||
Prefix: objOpts.Prefix,
|
||||
}
|
||||
|
||||
return client.listObjects(ctx, objOpts.BucketName, opts)
|
||||
}
|
||||
|
||||
func startRewindListing(ctx context.Context, client MCClient, objOpts *objectsListOpts) <-chan *cmd.ClientContent {
|
||||
lsRewind := client.list(ctx, cmd.ListOptions{TimeRef: objOpts.Date, WithDeleteMarkers: true})
|
||||
|
||||
return lsRewind
|
||||
}
|
||||
236
restapi/admin_objects_test.go
Normal file
236
restapi/admin_objects_test.go
Normal file
@@ -0,0 +1,236 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 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 restapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mc "github.com/minio/mc/cmd"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWSRewindObjects(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := s3ClientMock{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
testOptions objectsListOpts
|
||||
testMessages []*mc.ClientContent
|
||||
}{
|
||||
{
|
||||
name: "Get list with multiple elements",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{
|
||||
{
|
||||
BucketName: "buckettest",
|
||||
URL: mc.ClientURL{Path: "/file1.txt"},
|
||||
},
|
||||
{
|
||||
BucketName: "buckettest",
|
||||
URL: mc.ClientURL{Path: "/file2.txt"},
|
||||
},
|
||||
{
|
||||
BucketName: "buckettest",
|
||||
URL: mc.ClientURL{Path: "/path1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty list of elements",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "emptybucket",
|
||||
Prefix: "/",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{},
|
||||
},
|
||||
{
|
||||
name: "Get list with one element",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{
|
||||
{
|
||||
BucketName: "buckettestsingle",
|
||||
URL: mc.ClientURL{Path: "/file12.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get data from subpaths",
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/path1/path2",
|
||||
Date: time.Now(),
|
||||
},
|
||||
testMessages: []*mc.ClientContent{
|
||||
{
|
||||
BucketName: "buckettestsingle",
|
||||
URL: mc.ClientURL{Path: "/path1/path2/file12.txt"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
mcListMock = func(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent {
|
||||
ch := make(chan *mc.ClientContent)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for _, m := range tt.testMessages {
|
||||
ch <- m
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
rewindList := startRewindListing(ctx, client, &tt.testOptions)
|
||||
|
||||
// check that the rewindList got the same number of data from Console.
|
||||
|
||||
totalItems := 0
|
||||
for data := range rewindList {
|
||||
// Compare elements as we are defining the channel responses
|
||||
assert.Equal(tt.testMessages[totalItems].URL.Path, data.URL.Path)
|
||||
totalItems++
|
||||
}
|
||||
assert.Equal(len(tt.testMessages), totalItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWSListObjects(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := minioClientMock{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
testOptions objectsListOpts
|
||||
testMessages []minio.ObjectInfo
|
||||
}{
|
||||
{
|
||||
name: "Get list with multiple elements",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{
|
||||
{
|
||||
Key: "/file1.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
{
|
||||
Key: "/file2.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
{
|
||||
Key: "/path1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty list of elements",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "emptybucket",
|
||||
Prefix: "/",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{},
|
||||
},
|
||||
{
|
||||
name: "Get list with one element",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{
|
||||
{
|
||||
Key: "/file2.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get data from subpaths",
|
||||
wantErr: false,
|
||||
testOptions: objectsListOpts{
|
||||
BucketName: "buckettest",
|
||||
Prefix: "/path1/path2",
|
||||
},
|
||||
testMessages: []minio.ObjectInfo{
|
||||
{
|
||||
Key: "/path1/path2/file1.txt",
|
||||
Size: 500,
|
||||
IsLatest: true,
|
||||
LastModified: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
minioListObjectsMock = func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
|
||||
ch := make(chan minio.ObjectInfo)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for _, m := range tt.testMessages {
|
||||
ch <- m
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
objectsListing := startObjectsListing(ctx, client, &tt.testOptions)
|
||||
|
||||
// check that the TestReceiver got the same number of data from Console
|
||||
totalItems := 0
|
||||
for data := range objectsListing {
|
||||
// Compare elements as we are defining the channel responses
|
||||
assert.Equal(tt.testMessages[totalItems].Key, data.Key)
|
||||
totalItems++
|
||||
}
|
||||
assert.Equal(len(tt.testMessages), totalItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7159,6 +7159,9 @@ func init() {
|
||||
"delete_flag": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_latest": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"last_modified": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -15464,6 +15467,9 @@ func init() {
|
||||
"delete_flag": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_latest": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"last_modified": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -225,6 +225,7 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin
|
||||
Recursive: recursive,
|
||||
WithVersions: withVersions,
|
||||
WithMetadata: withMetadata,
|
||||
MaxKeys: 100,
|
||||
}
|
||||
if withMetadata {
|
||||
opts.MaxKeys = 1
|
||||
|
||||
@@ -18,20 +18,23 @@ package restapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
|
||||
"github.com/minio/console/pkg/utils"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/minio/console/models"
|
||||
"github.com/minio/console/pkg/auth"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
@@ -70,6 +73,18 @@ type wsS3Client struct {
|
||||
client MCClient
|
||||
}
|
||||
|
||||
// ConsoleWebSocketMClient interface of a Websocket Client
|
||||
type ConsoleWebsocketMClient interface {
|
||||
objectManager(options objectsListOpts)
|
||||
}
|
||||
|
||||
type wsMinioClient struct {
|
||||
// websocket connection.
|
||||
conn wsConn
|
||||
// MinIO admin Client
|
||||
client minioClient
|
||||
}
|
||||
|
||||
// WSConn interface with all functions to be implemented
|
||||
// by mock when testing, it should include all websocket.Conn
|
||||
// respective api calls that are used within this project.
|
||||
@@ -135,9 +150,9 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
// Un-comment for development so websockets work on port 5005
|
||||
// upgrader.CheckOrigin = func(r *http.Request) bool {
|
||||
// return true
|
||||
// }
|
||||
/*upgrader.CheckOrigin = func(r *http.Request) bool {
|
||||
return true
|
||||
}*/
|
||||
|
||||
// upgrades the HTTP server connection to the WebSocket protocol.
|
||||
conn, err := upgrader.Upgrade(w, req, nil)
|
||||
@@ -236,7 +251,7 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
|
||||
closeWsConn(conn)
|
||||
return
|
||||
}
|
||||
wsS3Client, err := newWebSocketS3Client(conn, session, wOptions.BucketName)
|
||||
wsS3Client, err := newWebSocketS3Client(conn, session, wOptions.BucketName, "")
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, err)
|
||||
closeWsConn(conn)
|
||||
@@ -272,6 +287,15 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
go wsAdminClient.profile(ctx, pOptions)
|
||||
|
||||
case strings.HasPrefix(wsPath, `/objectManager`):
|
||||
wsMinioClient, err := newWebSocketMinioClient(conn, session)
|
||||
if err != nil {
|
||||
ErrorWithContext(ctx, err)
|
||||
closeWsConn(conn)
|
||||
return
|
||||
}
|
||||
|
||||
go wsMinioClient.objectManager(session)
|
||||
default:
|
||||
// path not found
|
||||
closeWsConn(conn)
|
||||
@@ -299,10 +323,10 @@ func newWebSocketAdminClient(conn *websocket.Conn, autClaims *models.Principal)
|
||||
}
|
||||
|
||||
// newWebSocketS3Client returns a wsAdminClient authenticated as Console admin
|
||||
func newWebSocketS3Client(conn *websocket.Conn, claims *models.Principal, bucketName string) (*wsS3Client, error) {
|
||||
func newWebSocketS3Client(conn *websocket.Conn, claims *models.Principal, bucketName, prefix string) (*wsS3Client, error) {
|
||||
// Only start Websocket Interaction after user has been
|
||||
// authenticated with MinIO
|
||||
s3Client, err := newS3BucketClient(claims, bucketName, "")
|
||||
s3Client, err := newS3BucketClient(claims, bucketName, prefix)
|
||||
if err != nil {
|
||||
LogError("error creating S3Client:", err)
|
||||
return nil, err
|
||||
@@ -318,6 +342,28 @@ func newWebSocketS3Client(conn *websocket.Conn, claims *models.Principal, bucket
|
||||
return wsS3Client, nil
|
||||
}
|
||||
|
||||
func newWebSocketMinioClient(conn *websocket.Conn, claims *models.Principal) (*wsMinioClient, error) {
|
||||
// Only start Websocket Interaction after user has been
|
||||
// authenticated with MinIO
|
||||
|
||||
mClient, err := newMinioClient(claims)
|
||||
if err != nil {
|
||||
LogError("error creating MinioClient:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a websocket connection interface implementation
|
||||
// defining the connection to be used
|
||||
wsConnection := wsConn{conn: conn}
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
minioClient := minioClient{client: mClient}
|
||||
|
||||
// create websocket client and handle request
|
||||
wsMinioClient := &wsMinioClient{conn: wsConnection, client: minioClient}
|
||||
return wsMinioClient, nil
|
||||
}
|
||||
|
||||
// wsReadClientCtx reads the messages that come from the client
|
||||
// if the client sends a Close Message the context will be
|
||||
// canceled. If the connection is closed the goroutine inside
|
||||
@@ -492,6 +538,228 @@ func (wsc *wsAdminClient) profile(ctx context.Context, opts *profileOptions) {
|
||||
sendWsCloseMessage(wsc.conn, err)
|
||||
}
|
||||
|
||||
func (wsc *wsMinioClient) objectManager(session *models.Principal) {
|
||||
// Storage of Cancel Contexts for this connection
|
||||
cancelContexts := make(map[int64]context.CancelFunc)
|
||||
|
||||
// Initial goroutine
|
||||
defer func() {
|
||||
// We close socket at the end of requests
|
||||
wsc.conn.close()
|
||||
for _, c := range cancelContexts {
|
||||
// invoke cancel
|
||||
c()
|
||||
}
|
||||
}()
|
||||
|
||||
writeChannel := make(chan WSResponse)
|
||||
done := make(chan interface{})
|
||||
|
||||
// Read goroutine
|
||||
go func() {
|
||||
for {
|
||||
mType, message, err := wsc.conn.readMessage()
|
||||
if err != nil {
|
||||
LogInfo("Error while reading objectManager message", err)
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
|
||||
if mType == websocket.TextMessage {
|
||||
// We get request data & review information
|
||||
var messageRequest ObjectsRequest
|
||||
|
||||
err := json.Unmarshal(message, &messageRequest)
|
||||
if err != nil {
|
||||
LogInfo("Error on message request unmarshal")
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
|
||||
// new message, new context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// We store the cancel func associated with this request
|
||||
cancelContexts[messageRequest.RequestID] = cancel
|
||||
|
||||
const itemsPerBatch = 1000
|
||||
switch messageRequest.Mode {
|
||||
case "close":
|
||||
close(done)
|
||||
return
|
||||
case "cancel":
|
||||
// if we have that request id, cancel it
|
||||
if cancelFunc, ok := cancelContexts[messageRequest.RequestID]; ok {
|
||||
cancelFunc()
|
||||
delete(cancelContexts, messageRequest.RequestID)
|
||||
}
|
||||
case "objects":
|
||||
// cancel all previous open objects requests for listing
|
||||
for rid, c := range cancelContexts {
|
||||
if rid < messageRequest.RequestID {
|
||||
// invoke cancel
|
||||
c()
|
||||
}
|
||||
}
|
||||
|
||||
// start listing and writing to web socket
|
||||
go func() {
|
||||
defer func() {
|
||||
log.Println("Closing listing goroutine:", messageRequest.RequestID)
|
||||
}()
|
||||
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
|
||||
if err != nil {
|
||||
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
|
||||
return
|
||||
}
|
||||
var buffer []ObjectResponse
|
||||
for lsObj := range startObjectsListing(ctx, wsc.client, objectRqConfigs) {
|
||||
if cancelContexts[messageRequest.RequestID] == nil {
|
||||
return
|
||||
}
|
||||
if lsObj.Err != nil {
|
||||
writeChannel <- WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Error: lsObj.Err.Error(),
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
objItem := ObjectResponse{
|
||||
Name: lsObj.Key,
|
||||
Size: lsObj.Size,
|
||||
LastModified: lsObj.LastModified.Format(time.RFC3339),
|
||||
VersionID: lsObj.VersionID,
|
||||
IsLatest: lsObj.IsLatest,
|
||||
DeleteMarker: lsObj.IsDeleteMarker,
|
||||
}
|
||||
buffer = append(buffer, objItem)
|
||||
|
||||
if len(buffer) >= itemsPerBatch {
|
||||
writeChannel <- WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Data: buffer,
|
||||
}
|
||||
buffer = nil
|
||||
}
|
||||
}
|
||||
if len(buffer) > 0 {
|
||||
writeChannel <- WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Data: buffer,
|
||||
}
|
||||
}
|
||||
|
||||
writeChannel <- WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
RequestEnd: true,
|
||||
}
|
||||
|
||||
// remove the cancellation context
|
||||
delete(cancelContexts, messageRequest.RequestID)
|
||||
}()
|
||||
case "rewind":
|
||||
// cancel all previous open objects requests for listing
|
||||
for rid, c := range cancelContexts {
|
||||
if rid < messageRequest.RequestID {
|
||||
// invoke cancel
|
||||
c()
|
||||
}
|
||||
}
|
||||
|
||||
// start listing and writing to web socket
|
||||
go func() {
|
||||
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
|
||||
if err != nil {
|
||||
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
s3Client, err := newS3BucketClient(session, objectRqConfigs.BucketName, objectRqConfigs.Prefix)
|
||||
if err != nil {
|
||||
LogError("error creating S3Client:", err)
|
||||
close(done)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
mcS3C := mcClient{client: s3Client}
|
||||
|
||||
var buffer []ObjectResponse
|
||||
|
||||
for lsObj := range startRewindListing(ctx, mcS3C, objectRqConfigs) {
|
||||
if lsObj.Err != nil {
|
||||
writeChannel <- WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Error: lsObj.Err.String(),
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.ReplaceAll(lsObj.URL.Path, fmt.Sprintf("/%s/", objectRqConfigs.BucketName), "")
|
||||
|
||||
objItem := ObjectResponse{
|
||||
Name: name,
|
||||
Size: lsObj.Size,
|
||||
LastModified: lsObj.Time.Format(time.RFC3339),
|
||||
VersionID: lsObj.VersionID,
|
||||
IsLatest: lsObj.IsLatest,
|
||||
DeleteMarker: lsObj.IsDeleteMarker,
|
||||
}
|
||||
buffer = append(buffer, objItem)
|
||||
|
||||
if len(buffer) >= itemsPerBatch {
|
||||
writeChannel <- WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Data: buffer,
|
||||
}
|
||||
buffer = nil
|
||||
}
|
||||
|
||||
}
|
||||
if len(buffer) > 0 {
|
||||
writeChannel <- WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
Data: buffer,
|
||||
}
|
||||
}
|
||||
|
||||
writeChannel <- WSResponse{
|
||||
RequestID: messageRequest.RequestID,
|
||||
RequestEnd: true,
|
||||
}
|
||||
|
||||
// remove the cancellation context
|
||||
delete(cancelContexts, messageRequest.RequestID)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Write goroutine
|
||||
go func() {
|
||||
for writeM := range writeChannel {
|
||||
jsonData, err := json.Marshal(writeM)
|
||||
if err != nil {
|
||||
LogInfo("Error while parsing the response", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = wsc.conn.writeMessage(websocket.TextMessage, jsonData)
|
||||
|
||||
if err != nil {
|
||||
LogInfo("Error while writing the message", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-done
|
||||
}
|
||||
|
||||
// sendWsCloseMessage sends Websocket Connection Close Message indicating the Status Code
|
||||
// see https://tools.ietf.org/html/rfc6455#page-45
|
||||
func sendWsCloseMessage(conn WSConn, err error) {
|
||||
@@ -507,7 +775,7 @@ func sendWsCloseMessage(conn WSConn, err error) {
|
||||
return
|
||||
}
|
||||
// else, internal server error
|
||||
conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, ErrDefault.Error()))
|
||||
conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()))
|
||||
return
|
||||
}
|
||||
// normal closure
|
||||
|
||||
@@ -5203,6 +5203,8 @@ definitions:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
is_latest:
|
||||
type: boolean
|
||||
|
||||
rewindResponse:
|
||||
type: object
|
||||
|
||||
Reference in New Issue
Block a user