mirror of
https://github.com/OpenMaxIO/openmaxio-object-browser
synced 2026-07-01 07:41:18 -07:00
Anonymous Access (#2600)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
@@ -45,8 +45,8 @@ import (
|
|||||||
// Session token errors
|
// Session token errors
|
||||||
var (
|
var (
|
||||||
ErrNoAuthToken = errors.New("session token missing")
|
ErrNoAuthToken = errors.New("session token missing")
|
||||||
errTokenExpired = errors.New("session token has expired")
|
ErrTokenExpired = errors.New("session token has expired")
|
||||||
errReadingToken = errors.New("session token internal data is malformed")
|
ErrReadingToken = errors.New("session token internal data is malformed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// derivedKey is the key used to encrypt the session token claims, its derived using pbkdf on CONSOLE_PBKDF_PASSPHRASE with CONSOLE_PBKDF_SALT
|
// derivedKey is the key used to encrypt the session token claims, its derived using pbkdf on CONSOLE_PBKDF_PASSPHRASE with CONSOLE_PBKDF_SALT
|
||||||
@@ -101,12 +101,12 @@ func SessionTokenAuthenticate(token string) (*TokenClaims, error) {
|
|||||||
decryptedToken, err := DecryptToken(token)
|
decryptedToken, err := DecryptToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// fail decrypting token
|
// fail decrypting token
|
||||||
return nil, errReadingToken
|
return nil, ErrReadingToken
|
||||||
}
|
}
|
||||||
claimTokens, err := ParseClaimsFromToken(string(decryptedToken))
|
claimTokens, err := ParseClaimsFromToken(string(decryptedToken))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// fail unmarshalling token into data structure
|
// fail unmarshalling token into data structure
|
||||||
return nil, errReadingToken
|
return nil, ErrReadingToken
|
||||||
}
|
}
|
||||||
// claimsTokens contains the decrypted JWT for Console
|
// claimsTokens contains the decrypted JWT for Console
|
||||||
return claimTokens, nil
|
return claimTokens, nil
|
||||||
@@ -321,7 +321,7 @@ func GetTokenFromRequest(r *http.Request) (string, error) {
|
|||||||
}
|
}
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
if tokenCookie.Expires.After(currentTime) {
|
if tokenCookie.Expires.After(currentTime) {
|
||||||
return "", errTokenExpired
|
return "", ErrTokenExpired
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(tokenCookie.Value), nil
|
return strings.TrimSpace(tokenCookie.Value), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
globalSetDistributedSetup,
|
globalSetDistributedSetup,
|
||||||
operatorMode,
|
operatorMode,
|
||||||
selOpMode,
|
selOpMode,
|
||||||
|
setAnonymousMode,
|
||||||
setOverrideStyles,
|
setOverrideStyles,
|
||||||
setSiteReplicationInfo,
|
setSiteReplicationInfo,
|
||||||
userLogged,
|
userLogged,
|
||||||
@@ -48,7 +49,9 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
|
|||||||
|
|
||||||
const [sessionLoading, setSessionLoading] = useState<boolean>(true);
|
const [sessionLoading, setSessionLoading] = useState<boolean>(true);
|
||||||
const userLoggedIn = useSelector((state: AppState) => state.system.loggedIn);
|
const userLoggedIn = useSelector((state: AppState) => state.system.loggedIn);
|
||||||
|
const anonymousMode = useSelector(
|
||||||
|
(state: AppState) => state.system.anonymousMode
|
||||||
|
);
|
||||||
const { pathname = "" } = useLocation();
|
const { pathname = "" } = useLocation();
|
||||||
|
|
||||||
const StorePathAndRedirect = () => {
|
const StorePathAndRedirect = () => {
|
||||||
@@ -56,6 +59,9 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
|
|||||||
return <Navigate to={{ pathname: `login` }} />;
|
return <Navigate to={{ pathname: `login` }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pathnameParts = pathname.split("/");
|
||||||
|
const screen = pathnameParts.length > 2 ? pathnameParts[1] : "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api
|
api
|
||||||
.invoke("GET", `/api/v1/session`)
|
.invoke("GET", `/api/v1/session`)
|
||||||
@@ -81,8 +87,37 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => setSessionLoading(false));
|
.catch(() => {
|
||||||
}, [dispatch]);
|
// if we are trying to browse, probe access to the requested prefix
|
||||||
|
if (screen === "browser") {
|
||||||
|
const bucket = pathnameParts.length >= 3 ? pathnameParts[2] : "";
|
||||||
|
// no bucket, no business
|
||||||
|
if (bucket === "") {
|
||||||
|
setSessionLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// before marking the session as done, let's check if the bucket is publicly accessible
|
||||||
|
api
|
||||||
|
.invoke(
|
||||||
|
"GET",
|
||||||
|
`/api/v1/buckets/${bucket}/objects?limit=1`,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
"X-Anonymous": "1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((value) => {
|
||||||
|
dispatch(setAnonymousMode());
|
||||||
|
setSessionLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setSessionLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSessionLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [dispatch, screen, pathnameParts]);
|
||||||
|
|
||||||
const [, invokeSRInfoApi] = useApi(
|
const [, invokeSRInfoApi] = useApi(
|
||||||
(res: any) => {
|
(res: any) => {
|
||||||
@@ -112,7 +147,7 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userLoggedIn && !sessionLoading && !isOperatorMode) {
|
if (userLoggedIn && !sessionLoading && !isOperatorMode && !anonymousMode) {
|
||||||
invokeSRInfoApi("GET", `api/v1/admin/site-replication`);
|
invokeSRInfoApi("GET", `api/v1/admin/site-replication`);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ const hasPermission = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const sessionGrants = state.console.session.permissions || {};
|
const sessionGrants = state.console.session
|
||||||
|
? state.console.session.permissions || {}
|
||||||
|
: {};
|
||||||
|
|
||||||
const globalGrants = sessionGrants["arn:aws:s3:::*"] || [];
|
const globalGrants = sessionGrants["arn:aws:s3:::*"] || [];
|
||||||
let resources: string[] = [];
|
let resources: string[] = [];
|
||||||
|
|||||||
@@ -20,13 +20,23 @@ import { clearSession } from "../utils";
|
|||||||
import { ErrorResponseHandler } from "../types";
|
import { ErrorResponseHandler } from "../types";
|
||||||
import { baseUrl } from "../../history";
|
import { baseUrl } from "../../history";
|
||||||
|
|
||||||
|
type RequestHeaders = { [name: string]: string };
|
||||||
|
|
||||||
export class API {
|
export class API {
|
||||||
invoke(method: string, url: string, data?: object) {
|
invoke(method: string, url: string, data?: object, headers?: RequestHeaders) {
|
||||||
let targetURL = url;
|
let targetURL = url;
|
||||||
if (targetURL[0] === "/") {
|
if (targetURL[0] === "/") {
|
||||||
targetURL = targetURL.slice(1);
|
targetURL = targetURL.slice(1);
|
||||||
}
|
}
|
||||||
return request(method, targetURL)
|
let req = request(method, targetURL);
|
||||||
|
|
||||||
|
if (headers) {
|
||||||
|
for (let k in headers) {
|
||||||
|
req.set(k, headers[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return req
|
||||||
.send(data)
|
.send(data)
|
||||||
.then((res) => res.body)
|
.then((res) => res.body)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|||||||
88
portal-ui/src/screens/AnonymousAccess/AnonymousAccess.tsx
Normal file
88
portal-ui/src/screens/AnonymousAccess/AnonymousAccess.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2023 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, Suspense } from "react";
|
||||||
|
import ObjectBrowser from "../Console/ObjectBrowser/ObjectBrowser";
|
||||||
|
import LoadingComponent from "../../common/LoadingComponent";
|
||||||
|
import ObjectManager from "../Console/Common/ObjectManager/ObjectManager";
|
||||||
|
import { ApplicationLogo } from "mds";
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
import { IAM_PAGES } from "../../common/SecureComponent/permissions";
|
||||||
|
import { resetSession } from "../Console/consoleSlice";
|
||||||
|
import { useAppDispatch } from "../../store";
|
||||||
|
import { resetSystem } from "../../systemSlice";
|
||||||
|
import { getLogoVar } from "../../config";
|
||||||
|
import ObjectManagerButton from "../Console/Common/ObjectManager/ObjectManagerButton";
|
||||||
|
import { Button } from "@mui/material";
|
||||||
|
|
||||||
|
const AnonymousAccess = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(90deg, rgba(16,47,81,1) 0%, rgba(13,28,64,1) 100%)",
|
||||||
|
height: 100,
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
paddingLeft: 16,
|
||||||
|
paddingRight: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: 200, flexShrink: 1 }}>
|
||||||
|
<ApplicationLogo
|
||||||
|
applicationName={"console"}
|
||||||
|
subVariant={getLogoVar()}
|
||||||
|
inverse={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ flexGrow: 1 }}></div>
|
||||||
|
<div style={{ flexShrink: 1, display: "flex", flexDirection: "row" }}>
|
||||||
|
<Button
|
||||||
|
id={"go-to-login"}
|
||||||
|
variant={"text"}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(resetSession());
|
||||||
|
dispatch(resetSystem());
|
||||||
|
}}
|
||||||
|
style={{ color: "white" }}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
<ObjectManagerButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Suspense fallback={<LoadingComponent />}>
|
||||||
|
<ObjectManager />
|
||||||
|
</Suspense>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path={`${IAM_PAGES.OBJECT_BROWSER_VIEW}/*`}
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<LoadingComponent />}>
|
||||||
|
<ObjectBrowser />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default AnonymousAccess;
|
||||||
@@ -16,26 +16,15 @@
|
|||||||
|
|
||||||
import React, { Fragment, useCallback, useEffect } from "react";
|
import React, { Fragment, useCallback, useEffect } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useParams } from "react-router-dom";
|
||||||
import { Theme } from "@mui/material/styles";
|
import { Theme } from "@mui/material/styles";
|
||||||
import createStyles from "@mui/styles/createStyles";
|
import createStyles from "@mui/styles/createStyles";
|
||||||
import withStyles from "@mui/styles/withStyles";
|
import withStyles from "@mui/styles/withStyles";
|
||||||
import { Grid } from "@mui/material";
|
|
||||||
import { AppState, useAppDispatch } from "../../../../store";
|
import { AppState, useAppDispatch } from "../../../../store";
|
||||||
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
|
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
|
||||||
|
|
||||||
import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects";
|
import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects";
|
||||||
import PageHeader from "../../Common/PageHeader/PageHeader";
|
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
|
||||||
import { SettingsIcon } from "mds";
|
|
||||||
|
|
||||||
import { SecureComponent } from "../../../../common/SecureComponent";
|
|
||||||
import {
|
|
||||||
IAM_PAGES,
|
|
||||||
IAM_PERMISSIONS,
|
|
||||||
IAM_ROLES,
|
|
||||||
IAM_SCOPES,
|
|
||||||
} from "../../../../common/SecureComponent/permissions";
|
|
||||||
import BackLink from "../../../../common/BackLink";
|
|
||||||
import {
|
import {
|
||||||
newMessage,
|
newMessage,
|
||||||
resetMessages,
|
resetMessages,
|
||||||
@@ -50,17 +39,10 @@ import {
|
|||||||
setLockingEnabled,
|
setLockingEnabled,
|
||||||
setObjectDetailsView,
|
setObjectDetailsView,
|
||||||
setRecords,
|
setRecords,
|
||||||
setSearchObjects,
|
|
||||||
setSearchVersions,
|
|
||||||
setSelectedObjectView,
|
setSelectedObjectView,
|
||||||
setSimplePathHandler,
|
setSimplePathHandler,
|
||||||
setVersionsModeEnabled,
|
setVersionsModeEnabled,
|
||||||
} from "../../ObjectBrowser/objectBrowserSlice";
|
} from "../../ObjectBrowser/objectBrowserSlice";
|
||||||
import SearchBox from "../../Common/SearchBox";
|
|
||||||
import { selFeatures } from "../../consoleSlice";
|
|
||||||
import AutoColorIcon from "../../Common/Components/AutoColorIcon";
|
|
||||||
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
|
|
||||||
import { Button } from "mds";
|
|
||||||
import hasPermission from "../../../../common/SecureComponent/accessControl";
|
import hasPermission from "../../../../common/SecureComponent/accessControl";
|
||||||
import { IMessageEvent } from "websocket";
|
import { IMessageEvent } from "websocket";
|
||||||
import { wsProtocol } from "../../../../utils/wsUtils";
|
import { wsProtocol } from "../../../../utils/wsUtils";
|
||||||
@@ -74,6 +56,7 @@ import { setErrorSnackMessage } from "../../../../systemSlice";
|
|||||||
import api from "../../../../common/api";
|
import api from "../../../../common/api";
|
||||||
import { BucketObjectLocking, BucketVersioning } from "../types";
|
import { BucketObjectLocking, BucketVersioning } from "../types";
|
||||||
import { ErrorResponseHandler } from "../../../../common/types";
|
import { ErrorResponseHandler } from "../../../../common/types";
|
||||||
|
import OBHeader from "../../ObjectBrowser/OBHeader";
|
||||||
|
|
||||||
const styles = (theme: Theme) =>
|
const styles = (theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@@ -145,7 +128,6 @@ const initWSConnection = (
|
|||||||
|
|
||||||
const BrowserHandler = () => {
|
const BrowserHandler = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@@ -153,18 +135,6 @@ const BrowserHandler = () => {
|
|||||||
(state: AppState) => state.objectBrowser.loadingVersioning
|
(state: AppState) => state.objectBrowser.loadingVersioning
|
||||||
);
|
);
|
||||||
|
|
||||||
const versionsMode = useSelector(
|
|
||||||
(state: AppState) => state.objectBrowser.versionsMode
|
|
||||||
);
|
|
||||||
const searchObjects = useSelector(
|
|
||||||
(state: AppState) => state.objectBrowser.searchObjects
|
|
||||||
);
|
|
||||||
const versionedFile = useSelector(
|
|
||||||
(state: AppState) => state.objectBrowser.versionedFile
|
|
||||||
);
|
|
||||||
const searchVersions = useSelector(
|
|
||||||
(state: AppState) => state.objectBrowser.searchVersions
|
|
||||||
);
|
|
||||||
const rewindEnabled = useSelector(
|
const rewindEnabled = useSelector(
|
||||||
(state: AppState) => state.objectBrowser.rewind.rewindEnabled
|
(state: AppState) => state.objectBrowser.rewind.rewindEnabled
|
||||||
);
|
);
|
||||||
@@ -195,15 +165,14 @@ const BrowserHandler = () => {
|
|||||||
const isOpeningOD = useSelector(
|
const isOpeningOD = useSelector(
|
||||||
(state: AppState) => state.objectBrowser.isOpeningObjectDetail
|
(state: AppState) => state.objectBrowser.isOpeningObjectDetail
|
||||||
);
|
);
|
||||||
|
const anonymousMode = useSelector(
|
||||||
const features = useSelector(selFeatures);
|
(state: AppState) => state.system.anonymousMode
|
||||||
|
);
|
||||||
|
|
||||||
const bucketName = params.bucketName || "";
|
const bucketName = params.bucketName || "";
|
||||||
const pathSegment = location.pathname.split(`/browser/${bucketName}/`);
|
const pathSegment = location.pathname.split(`/browser/${bucketName}/`);
|
||||||
const internalPaths = pathSegment.length === 2 ? pathSegment[1] : "";
|
const internalPaths = pathSegment.length === 2 ? pathSegment[1] : "";
|
||||||
|
|
||||||
const obOnly = !!features?.includes("object-browser-only");
|
|
||||||
|
|
||||||
/*WS Request Handlers*/
|
/*WS Request Handlers*/
|
||||||
const onMessageCallBack = useCallback(
|
const onMessageCallBack = useCallback(
|
||||||
(message: IMessageEvent) => {
|
(message: IMessageEvent) => {
|
||||||
@@ -297,7 +266,6 @@ const BrowserHandler = () => {
|
|||||||
const dupRequest = () => {
|
const dupRequest = () => {
|
||||||
initWSRequest(path, date);
|
initWSRequest(path, date);
|
||||||
};
|
};
|
||||||
|
|
||||||
initWSConnection(dupRequest, onMessageCallBack);
|
initWSConnection(dupRequest, onMessageCallBack);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -377,10 +345,11 @@ const BrowserHandler = () => {
|
|||||||
simplePath,
|
simplePath,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const displayListObjects = hasPermission(bucketName, [
|
const displayListObjects =
|
||||||
IAM_SCOPES.S3_LIST_BUCKET,
|
hasPermission(bucketName, [
|
||||||
IAM_SCOPES.S3_ALL_LIST_BUCKET,
|
IAM_SCOPES.S3_LIST_BUCKET,
|
||||||
]);
|
IAM_SCOPES.S3_ALL_LIST_BUCKET,
|
||||||
|
]) || anonymousMode;
|
||||||
|
|
||||||
// Common objects list
|
// Common objects list
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -408,7 +377,6 @@ const BrowserHandler = () => {
|
|||||||
if (rewindEnabled && rewindDate) {
|
if (rewindEnabled && rewindDate) {
|
||||||
requestDate = rewindDate;
|
requestDate = rewindDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
initWSRequest(pathPrefix, requestDate);
|
initWSRequest(pathPrefix, requestDate);
|
||||||
} else {
|
} else {
|
||||||
dispatch(setLoadingObjects(false));
|
dispatch(setLoadingObjects(false));
|
||||||
@@ -429,7 +397,7 @@ const BrowserHandler = () => {
|
|||||||
}, [internalPaths, dispatch]);
|
}, [internalPaths, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingVersioning) {
|
if (loadingVersioning && !anonymousMode) {
|
||||||
if (displayListObjects) {
|
if (displayListObjects) {
|
||||||
api
|
api
|
||||||
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
|
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
|
||||||
@@ -449,7 +417,13 @@ const BrowserHandler = () => {
|
|||||||
dispatch(resetMessages());
|
dispatch(resetMessages());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [bucketName, loadingVersioning, dispatch, displayListObjects]);
|
}, [
|
||||||
|
bucketName,
|
||||||
|
loadingVersioning,
|
||||||
|
dispatch,
|
||||||
|
displayListObjects,
|
||||||
|
anonymousMode,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingLocking) {
|
if (loadingLocking) {
|
||||||
@@ -497,127 +471,10 @@ const BrowserHandler = () => {
|
|||||||
}
|
}
|
||||||
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
|
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
|
||||||
|
|
||||||
const openBucketConfiguration = () => {
|
|
||||||
navigate(`/buckets/${bucketName}/admin`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const configureBucketAllowed = hasPermission(bucketName, [
|
|
||||||
IAM_SCOPES.S3_GET_BUCKET_POLICY,
|
|
||||||
IAM_SCOPES.S3_PUT_BUCKET_POLICY,
|
|
||||||
IAM_SCOPES.S3_GET_BUCKET_VERSIONING,
|
|
||||||
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
|
|
||||||
IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION,
|
|
||||||
IAM_SCOPES.S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION,
|
|
||||||
IAM_SCOPES.S3_DELETE_BUCKET,
|
|
||||||
IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS,
|
|
||||||
IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS,
|
|
||||||
IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
|
|
||||||
IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION,
|
|
||||||
IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION,
|
|
||||||
IAM_SCOPES.S3_PUT_LIFECYCLE_CONFIGURATION,
|
|
||||||
IAM_SCOPES.ADMIN_GET_BUCKET_QUOTA,
|
|
||||||
IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA,
|
|
||||||
IAM_SCOPES.S3_PUT_BUCKET_TAGGING,
|
|
||||||
IAM_SCOPES.S3_GET_BUCKET_TAGGING,
|
|
||||||
IAM_SCOPES.S3_LIST_BUCKET_VERSIONS,
|
|
||||||
IAM_SCOPES.S3_GET_BUCKET_POLICY_STATUS,
|
|
||||||
IAM_SCOPES.S3_DELETE_BUCKET_POLICY,
|
|
||||||
IAM_SCOPES.S3_GET_ACTIONS,
|
|
||||||
IAM_SCOPES.S3_PUT_ACTIONS,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const searchBar = (
|
|
||||||
<Fragment>
|
|
||||||
{!versionsMode ? (
|
|
||||||
<SecureComponent
|
|
||||||
scopes={[IAM_SCOPES.S3_LIST_BUCKET, IAM_SCOPES.S3_ALL_LIST_BUCKET]}
|
|
||||||
resource={bucketName}
|
|
||||||
errorProps={{ disabled: true }}
|
|
||||||
>
|
|
||||||
<SearchBox
|
|
||||||
placeholder={"Start typing to filter objects in the bucket"}
|
|
||||||
onChange={(value) => {
|
|
||||||
dispatch(setSearchObjects(value));
|
|
||||||
}}
|
|
||||||
value={searchObjects}
|
|
||||||
/>
|
|
||||||
</SecureComponent>
|
|
||||||
) : (
|
|
||||||
<Fragment>
|
|
||||||
<SearchBox
|
|
||||||
placeholder={`Start typing to filter versions of ${versionedFile}`}
|
|
||||||
onChange={(value) => {
|
|
||||||
dispatch(setSearchVersions(value));
|
|
||||||
}}
|
|
||||||
value={searchVersions}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{!obOnly ? (
|
{!anonymousMode && <OBHeader bucketName={bucketName} />}
|
||||||
<PageHeader
|
<ListObjects />
|
||||||
label={
|
|
||||||
<BackLink
|
|
||||||
label={"Object Browser"}
|
|
||||||
to={IAM_PAGES.OBJECT_BROWSER_VIEW}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<SecureComponent
|
|
||||||
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
|
|
||||||
resource={bucketName}
|
|
||||||
errorProps={{ disabled: true }}
|
|
||||||
>
|
|
||||||
<TooltipWrapper
|
|
||||||
tooltip={
|
|
||||||
configureBucketAllowed
|
|
||||||
? "Configure Bucket"
|
|
||||||
: "You do not have the required permissions to configure this bucket. Please contact your MinIO administrator to request " +
|
|
||||||
IAM_ROLES.BUCKET_ADMIN +
|
|
||||||
" permisions."
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
id={"configure-bucket-main"}
|
|
||||||
color="primary"
|
|
||||||
aria-label="Configure Bucket"
|
|
||||||
onClick={openBucketConfiguration}
|
|
||||||
icon={
|
|
||||||
<SettingsIcon
|
|
||||||
style={{ width: 20, height: 20, marginTop: -3 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
padding: "0 10px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TooltipWrapper>
|
|
||||||
</SecureComponent>
|
|
||||||
}
|
|
||||||
middleComponent={searchBar}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
sx={{
|
|
||||||
padding: "20px 32px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Grid>
|
|
||||||
<AutoColorIcon marginRight={30} marginTop={10} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs>
|
|
||||||
{searchBar}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
<Grid>
|
|
||||||
<ListObjects />
|
|
||||||
</Grid>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -241,9 +241,6 @@ const BucketDetails = ({ classes }: IBucketDetailsProps) => {
|
|||||||
<PageLayout className={classes.pageContainer}>
|
<PageLayout className={classes.pageContainer}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<ScreenTitle
|
<ScreenTitle
|
||||||
classes={{
|
|
||||||
screenTitle: classes.screenTitle,
|
|
||||||
}}
|
|
||||||
icon={
|
icon={
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<BucketsIcon width={40} />
|
<BucketsIcon width={40} />
|
||||||
|
|||||||
@@ -26,7 +26,16 @@ import { useSelector } from "react-redux";
|
|||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { Theme } from "@mui/material/styles";
|
import { Theme } from "@mui/material/styles";
|
||||||
import { Button } from "mds";
|
import {
|
||||||
|
BucketsIcon,
|
||||||
|
Button,
|
||||||
|
DeleteIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
HistoryIcon,
|
||||||
|
PreviewIcon,
|
||||||
|
RefreshIcon,
|
||||||
|
ShareIcon,
|
||||||
|
} from "mds";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import createStyles from "@mui/styles/createStyles";
|
import createStyles from "@mui/styles/createStyles";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
@@ -68,15 +77,6 @@ import {
|
|||||||
SecureComponent,
|
SecureComponent,
|
||||||
} from "../../../../../../common/SecureComponent";
|
} from "../../../../../../common/SecureComponent";
|
||||||
import withSuspense from "../../../../Common/Components/withSuspense";
|
import withSuspense from "../../../../Common/Components/withSuspense";
|
||||||
import {
|
|
||||||
BucketsIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
PreviewIcon,
|
|
||||||
ShareIcon,
|
|
||||||
HistoryIcon,
|
|
||||||
RefreshIcon,
|
|
||||||
DeleteIcon,
|
|
||||||
} from "mds";
|
|
||||||
import UploadFilesButton from "../../UploadFilesButton";
|
import UploadFilesButton from "../../UploadFilesButton";
|
||||||
import DetailsListPanel from "./DetailsListPanel";
|
import DetailsListPanel from "./DetailsListPanel";
|
||||||
import ObjectDetailPanel from "./ObjectDetailPanel";
|
import ObjectDetailPanel from "./ObjectDetailPanel";
|
||||||
@@ -136,6 +136,8 @@ import {
|
|||||||
openShare,
|
openShare,
|
||||||
} from "../../../../ObjectBrowser/objectBrowserThunks";
|
} from "../../../../ObjectBrowser/objectBrowserThunks";
|
||||||
|
|
||||||
|
import FilterObjectsSB from "../../../../ObjectBrowser/FilterObjectsSB";
|
||||||
|
|
||||||
const DeleteMultipleObjects = withSuspense(
|
const DeleteMultipleObjects = withSuspense(
|
||||||
React.lazy(() => import("./DeleteMultipleObjects"))
|
React.lazy(() => import("./DeleteMultipleObjects"))
|
||||||
);
|
);
|
||||||
@@ -158,12 +160,6 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
minWidth: 5,
|
minWidth: 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
screenTitle: {
|
|
||||||
borderBottom: 0,
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingLeft: 0,
|
|
||||||
paddingRight: 0,
|
|
||||||
},
|
|
||||||
...tableStyles,
|
...tableStyles,
|
||||||
...actionsTray,
|
...actionsTray,
|
||||||
...searchField,
|
...searchField,
|
||||||
@@ -174,7 +170,6 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
},
|
},
|
||||||
screenTitleContainer: {
|
screenTitleContainer: {
|
||||||
border: "#EAEDEE 1px solid",
|
border: "#EAEDEE 1px solid",
|
||||||
padding: "0.8rem 15px 0",
|
|
||||||
},
|
},
|
||||||
labelStyle: {
|
labelStyle: {
|
||||||
color: "#969FA8",
|
color: "#969FA8",
|
||||||
@@ -194,6 +189,11 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
display: "none",
|
display: "none",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
actionsSection: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
...objectBrowserExtras,
|
...objectBrowserExtras,
|
||||||
...objectBrowserCommon,
|
...objectBrowserCommon,
|
||||||
...containerForHeader(theme.spacing(4)),
|
...containerForHeader(theme.spacing(4)),
|
||||||
@@ -273,6 +273,9 @@ const ListObjects = () => {
|
|||||||
const selectedBucket = useSelector(
|
const selectedBucket = useSelector(
|
||||||
(state: AppState) => state.objectBrowser.selectedBucket
|
(state: AppState) => state.objectBrowser.selectedBucket
|
||||||
);
|
);
|
||||||
|
const anonymousMode = useSelector(
|
||||||
|
(state: AppState) => state.system.anonymousMode
|
||||||
|
);
|
||||||
|
|
||||||
const loadingBucket = useSelector(selBucketDetailsLoading);
|
const loadingBucket = useSelector(selBucketDetailsLoading);
|
||||||
const bucketInfo = useSelector(selBucketDetailsInfo);
|
const bucketInfo = useSelector(selBucketDetailsInfo);
|
||||||
@@ -305,12 +308,13 @@ const ListObjects = () => {
|
|||||||
IAM_SCOPES.S3_GET_ACTIONS,
|
IAM_SCOPES.S3_GET_ACTIONS,
|
||||||
]);
|
]);
|
||||||
const canDelete = hasPermission(bucketName, [IAM_SCOPES.S3_DELETE_OBJECT]);
|
const canDelete = hasPermission(bucketName, [IAM_SCOPES.S3_DELETE_OBJECT]);
|
||||||
const canUpload = hasPermission(
|
const canUpload =
|
||||||
uploadPath,
|
hasPermission(
|
||||||
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
|
uploadPath,
|
||||||
true,
|
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
|
||||||
true
|
true,
|
||||||
);
|
true
|
||||||
|
) || anonymousMode;
|
||||||
|
|
||||||
const displayDeleteObject = hasPermission(bucketName, [
|
const displayDeleteObject = hasPermission(bucketName, [
|
||||||
IAM_SCOPES.S3_DELETE_OBJECT,
|
IAM_SCOPES.S3_DELETE_OBJECT,
|
||||||
@@ -365,7 +369,7 @@ const ListObjects = () => {
|
|||||||
}, [selectedObjects]);
|
}, [selectedObjects]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!quota) {
|
if (!quota && !anonymousMode) {
|
||||||
api
|
api
|
||||||
.invoke("GET", `/api/v1/buckets/${bucketName}/quota`)
|
.invoke("GET", `/api/v1/buckets/${bucketName}/quota`)
|
||||||
.then((res: BucketQuota) => {
|
.then((res: BucketQuota) => {
|
||||||
@@ -382,7 +386,7 @@ const ListObjects = () => {
|
|||||||
setQuota(null);
|
setQuota(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [quota, bucketName]);
|
}, [quota, bucketName, anonymousMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedObjects.length > 0) {
|
if (selectedObjects.length > 0) {
|
||||||
@@ -408,7 +412,7 @@ const ListObjects = () => {
|
|||||||
|
|
||||||
// bucket info
|
// bucket info
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingBucket) {
|
if (loadingBucket && !anonymousMode) {
|
||||||
api
|
api
|
||||||
.invoke("GET", `/api/v1/buckets/${bucketName}`)
|
.invoke("GET", `/api/v1/buckets/${bucketName}`)
|
||||||
.then((res: BucketInfo) => {
|
.then((res: BucketInfo) => {
|
||||||
@@ -421,7 +425,7 @@ const ListObjects = () => {
|
|||||||
dispatch(setErrorSnackMessage(err));
|
dispatch(setErrorSnackMessage(err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [bucketName, loadingBucket, dispatch]);
|
}, [bucketName, loadingBucket, dispatch, anonymousMode]);
|
||||||
|
|
||||||
// Load retention Config
|
// Load retention Config
|
||||||
|
|
||||||
@@ -538,6 +542,10 @@ const ListObjects = () => {
|
|||||||
|
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", uploadUrl, true);
|
xhr.open("POST", uploadUrl, true);
|
||||||
|
if (anonymousMode) {
|
||||||
|
xhr.setRequestHeader("X-Anonymous", "1");
|
||||||
|
}
|
||||||
|
// xhr.setRequestHeader("X-Anonymous", "1");
|
||||||
|
|
||||||
const areMultipleFiles = files.length > 1;
|
const areMultipleFiles = files.length > 1;
|
||||||
let errorMessage = `An error occurred while uploading the file${
|
let errorMessage = `An error occurred while uploading the file${
|
||||||
@@ -677,7 +685,7 @@ const ListObjects = () => {
|
|||||||
|
|
||||||
upload(files, bucketName, pathPrefix, folderPath);
|
upload(files, bucketName, pathPrefix, folderPath);
|
||||||
},
|
},
|
||||||
[bucketName, dispatch, simplePath]
|
[bucketName, dispatch, simplePath, anonymousMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
@@ -879,60 +887,67 @@ const ListObjects = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PageLayout variant={"full"}>
|
<PageLayout variant={"full"}>
|
||||||
|
{anonymousMode && (
|
||||||
|
<div style={{ paddingBottom: 16 }}>
|
||||||
|
<FilterObjectsSB />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Grid item xs={12} className={classes.screenTitleContainer}>
|
<Grid item xs={12} className={classes.screenTitleContainer}>
|
||||||
<ScreenTitle
|
<ScreenTitle
|
||||||
className={classes.screenTitle}
|
|
||||||
icon={
|
icon={
|
||||||
<span className={classes.listIcon}>
|
<span>
|
||||||
<BucketsIcon />
|
<BucketsIcon style={{ width: 30 }} />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
title={<span className={classes.titleSpacer}>{bucketName}</span>}
|
title={<span className={classes.titleSpacer}>{bucketName}</span>}
|
||||||
subTitle={
|
subTitle={
|
||||||
<Fragment>
|
!anonymousMode ? (
|
||||||
<Grid item xs={12} className={classes.bucketDetails}>
|
<Fragment>
|
||||||
<span className={classes.detailsSpacer}>
|
<Grid item xs={12} className={classes.bucketDetails}>
|
||||||
Created on:
|
<span className={classes.detailsSpacer}>
|
||||||
<strong>
|
Created on:
|
||||||
{bucketInfo?.creation_date
|
<strong>
|
||||||
? createdTime.toFormat(
|
{bucketInfo?.creation_date
|
||||||
"ccc, LLL dd yyyy HH:mm:ss (ZZZZ)"
|
? createdTime.toFormat(
|
||||||
)
|
"ccc, LLL dd yyyy HH:mm:ss (ZZZZ)"
|
||||||
: ""}
|
)
|
||||||
</strong>
|
: ""}
|
||||||
</span>
|
</strong>
|
||||||
<span className={classes.detailsSpacer}>
|
</span>
|
||||||
Access:
|
<span className={classes.detailsSpacer}>
|
||||||
<strong>{bucketInfo?.access || ""}</strong>
|
Access:
|
||||||
</span>
|
<strong>{bucketInfo?.access || ""}</strong>
|
||||||
{bucketInfo && (
|
</span>
|
||||||
<Fragment>
|
{bucketInfo && (
|
||||||
<span className={classes.detailsSpacer}>
|
<Fragment>
|
||||||
{bucketInfo.size && (
|
<span className={classes.detailsSpacer}>
|
||||||
<Fragment>{niceBytesInt(bucketInfo.size)}</Fragment>
|
{bucketInfo.size && (
|
||||||
)}
|
<Fragment>{niceBytesInt(bucketInfo.size)}</Fragment>
|
||||||
{bucketInfo.size && quota && (
|
)}
|
||||||
<Fragment> / {niceBytesInt(quota.quota)}</Fragment>
|
{bucketInfo.size && quota && (
|
||||||
)}
|
<Fragment> / {niceBytesInt(quota.quota)}</Fragment>
|
||||||
{bucketInfo.size && bucketInfo.objects ? " - " : ""}
|
)}
|
||||||
{bucketInfo.objects && (
|
{bucketInfo.size && bucketInfo.objects ? " - " : ""}
|
||||||
<Fragment>
|
{bucketInfo.objects && (
|
||||||
{bucketInfo.objects} Object
|
<Fragment>
|
||||||
{bucketInfo.objects && bucketInfo.objects !== 1
|
{bucketInfo.objects} Object
|
||||||
? "s"
|
{bucketInfo.objects && bucketInfo.objects !== 1
|
||||||
: ""}
|
? "s"
|
||||||
</Fragment>
|
: ""}
|
||||||
)}
|
</Fragment>
|
||||||
</span>
|
)}
|
||||||
</Fragment>
|
</span>
|
||||||
)}
|
</Fragment>
|
||||||
</Grid>
|
)}
|
||||||
</Fragment>
|
</Grid>
|
||||||
|
</Fragment>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<Fragment>
|
<div className={classes.actionsSection}>
|
||||||
<div className={classes.actionsSection}>
|
{!anonymousMode && (
|
||||||
<TooltipWrapper tooltip={"Rewind Bucket"}>
|
<TooltipWrapper tooltip={"Rewind Bucket"}>
|
||||||
<Button
|
<Button
|
||||||
id={"rewind-objects-list"}
|
id={"rewind-objects-list"}
|
||||||
@@ -970,61 +985,63 @@ const ListObjects = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
<TooltipWrapper tooltip={"Reload List"}>
|
)}
|
||||||
<Button
|
<TooltipWrapper tooltip={"Reload List"}>
|
||||||
id={"refresh-objects-list"}
|
<Button
|
||||||
label={"Refresh"}
|
id={"refresh-objects-list"}
|
||||||
icon={<RefreshIcon />}
|
label={"Refresh"}
|
||||||
variant={"regular"}
|
icon={<RefreshIcon />}
|
||||||
onClick={() => {
|
variant={"regular"}
|
||||||
if (versionsMode) {
|
onClick={() => {
|
||||||
dispatch(setLoadingVersions(true));
|
if (versionsMode) {
|
||||||
} else {
|
dispatch(setLoadingVersions(true));
|
||||||
dispatch(resetMessages());
|
} else {
|
||||||
dispatch(setLoadingRecords(true));
|
dispatch(resetMessages());
|
||||||
dispatch(setLoadingObjects(true));
|
dispatch(setLoadingRecords(true));
|
||||||
}
|
dispatch(setLoadingObjects(true));
|
||||||
}}
|
|
||||||
disabled={
|
|
||||||
!hasPermission(bucketName, [
|
|
||||||
IAM_SCOPES.S3_LIST_BUCKET,
|
|
||||||
IAM_SCOPES.S3_ALL_LIST_BUCKET,
|
|
||||||
]) || rewindEnabled
|
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
</TooltipWrapper>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
onChange={handleUploadButton}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
ref={fileUpload}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
onChange={handleUploadButton}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
ref={folderUpload}
|
|
||||||
/>
|
|
||||||
<UploadFilesButton
|
|
||||||
bucketName={bucketName}
|
|
||||||
uploadPath={uploadPath.join("/")}
|
|
||||||
uploadFileFunction={(closeMenu) => {
|
|
||||||
if (fileUpload && fileUpload.current) {
|
|
||||||
fileUpload.current.click();
|
|
||||||
}
|
|
||||||
closeMenu();
|
|
||||||
}}
|
|
||||||
uploadFolderFunction={(closeMenu) => {
|
|
||||||
if (folderUpload && folderUpload.current) {
|
|
||||||
folderUpload.current.click();
|
|
||||||
}
|
|
||||||
closeMenu();
|
|
||||||
}}
|
}}
|
||||||
|
disabled={
|
||||||
|
anonymousMode
|
||||||
|
? false
|
||||||
|
: !hasPermission(bucketName, [
|
||||||
|
IAM_SCOPES.S3_LIST_BUCKET,
|
||||||
|
IAM_SCOPES.S3_ALL_LIST_BUCKET,
|
||||||
|
]) || rewindEnabled
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</TooltipWrapper>
|
||||||
</Fragment>
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
onChange={handleUploadButton}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
ref={fileUpload}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
onChange={handleUploadButton}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
ref={folderUpload}
|
||||||
|
/>
|
||||||
|
<UploadFilesButton
|
||||||
|
bucketName={bucketName}
|
||||||
|
uploadPath={uploadPath.join("/")}
|
||||||
|
uploadFileFunction={(closeMenu) => {
|
||||||
|
if (fileUpload && fileUpload.current) {
|
||||||
|
fileUpload.current.click();
|
||||||
|
}
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
uploadFolderFunction={(closeMenu) => {
|
||||||
|
if (folderUpload && folderUpload.current) {
|
||||||
|
folderUpload.current.click();
|
||||||
|
}
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -1058,66 +1075,70 @@ const ListObjects = () => {
|
|||||||
errorProps={{ disabled: true }}
|
errorProps={{ disabled: true }}
|
||||||
>
|
>
|
||||||
<Grid item xs={12} className={classes.fullContainer}>
|
<Grid item xs={12} className={classes.fullContainer}>
|
||||||
<Grid item xs={12} className={classes.breadcrumbsContainer}>
|
{!anonymousMode && (
|
||||||
<BrowserBreadcrumbs
|
<Grid item xs={12} className={classes.breadcrumbsContainer}>
|
||||||
bucketName={bucketName}
|
<BrowserBreadcrumbs
|
||||||
internalPaths={pageTitle}
|
bucketName={bucketName}
|
||||||
additionalOptions={
|
internalPaths={pageTitle}
|
||||||
!isVersioned || rewindEnabled ? null : (
|
additionalOptions={
|
||||||
<div>
|
!isVersioned || rewindEnabled ? null : (
|
||||||
<CheckboxWrapper
|
<div>
|
||||||
name={"deleted_objects"}
|
<CheckboxWrapper
|
||||||
id={"showDeletedObjects"}
|
name={"deleted_objects"}
|
||||||
value={"deleted_on"}
|
id={"showDeletedObjects"}
|
||||||
label={"Show deleted objects"}
|
value={"deleted_on"}
|
||||||
onChange={setDeletedAction}
|
label={"Show deleted objects"}
|
||||||
checked={showDeleted}
|
onChange={setDeletedAction}
|
||||||
overrideLabelClasses={classes.labelStyle}
|
checked={showDeleted}
|
||||||
className={classes.overrideShowDeleted}
|
overrideLabelClasses={classes.labelStyle}
|
||||||
noTopMargin
|
className={classes.overrideShowDeleted}
|
||||||
/>
|
noTopMargin
|
||||||
</div>
|
/>
|
||||||
)
|
</div>
|
||||||
}
|
)
|
||||||
hidePathButton={false}
|
}
|
||||||
/>
|
hidePathButton={false}
|
||||||
</Grid>
|
/>
|
||||||
<ListObjectsTable />
|
</Grid>
|
||||||
|
)}
|
||||||
|
<ListObjectsTable internalPaths={selectedInternalPaths} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</SecureComponent>
|
</SecureComponent>
|
||||||
)}
|
)}
|
||||||
<SecureComponent
|
{!anonymousMode && (
|
||||||
scopes={[
|
<SecureComponent
|
||||||
IAM_SCOPES.S3_LIST_BUCKET,
|
scopes={[
|
||||||
IAM_SCOPES.S3_ALL_LIST_BUCKET,
|
IAM_SCOPES.S3_LIST_BUCKET,
|
||||||
]}
|
IAM_SCOPES.S3_ALL_LIST_BUCKET,
|
||||||
resource={bucketName}
|
]}
|
||||||
errorProps={{ disabled: true }}
|
resource={bucketName}
|
||||||
>
|
errorProps={{ disabled: true }}
|
||||||
<DetailsListPanel
|
|
||||||
open={detailsOpen}
|
|
||||||
closePanel={() => {
|
|
||||||
onClosePanel(false);
|
|
||||||
}}
|
|
||||||
className={`${versionsMode ? classes.hideListOnSmall : ""}`}
|
|
||||||
>
|
>
|
||||||
{selectedObjects.length > 0 && (
|
<DetailsListPanel
|
||||||
<ActionsListSection
|
open={detailsOpen}
|
||||||
items={multiActionButtons}
|
closePanel={() => {
|
||||||
title={"Selected Objects:"}
|
onClosePanel(false);
|
||||||
/>
|
}}
|
||||||
)}
|
className={`${versionsMode ? classes.hideListOnSmall : ""}`}
|
||||||
{selectedInternalPaths !== null && (
|
>
|
||||||
<ObjectDetailPanel
|
{selectedObjects.length > 0 && (
|
||||||
internalPaths={selectedInternalPaths}
|
<ActionsListSection
|
||||||
bucketName={bucketName}
|
items={multiActionButtons}
|
||||||
onClosePanel={onClosePanel}
|
title={"Selected Objects:"}
|
||||||
versioning={isVersioned}
|
/>
|
||||||
locking={lockingEnabled}
|
)}
|
||||||
/>
|
{selectedInternalPaths !== null && (
|
||||||
)}
|
<ObjectDetailPanel
|
||||||
</DetailsListPanel>
|
internalPaths={selectedInternalPaths}
|
||||||
</SecureComponent>
|
bucketName={bucketName}
|
||||||
|
onClosePanel={onClosePanel}
|
||||||
|
versioning={isVersioned}
|
||||||
|
locking={lockingEnabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DetailsListPanel>
|
||||||
|
</SecureComponent>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ import {
|
|||||||
permissionTooltipHelper,
|
permissionTooltipHelper,
|
||||||
} from "../../../../../../common/SecureComponent/permissions";
|
} from "../../../../../../common/SecureComponent/permissions";
|
||||||
import { hasPermission } from "../../../../../../common/SecureComponent";
|
import { hasPermission } from "../../../../../../common/SecureComponent";
|
||||||
|
import { downloadObject } from "../../../../ObjectBrowser/utils";
|
||||||
|
import { IFileInfo } from "../ObjectDetails/types";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@@ -77,7 +79,11 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ListObjectsTable = () => {
|
interface IListObjectTable {
|
||||||
|
internalPaths: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListObjectsTable = ({ internalPaths }: IListObjectTable) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -111,7 +117,9 @@ const ListObjectsTable = () => {
|
|||||||
const selectedObjects = useSelector(
|
const selectedObjects = useSelector(
|
||||||
(state: AppState) => state.objectBrowser.selectedObjects
|
(state: AppState) => state.objectBrowser.selectedObjects
|
||||||
);
|
);
|
||||||
|
const anonymousMode = useSelector(
|
||||||
|
(state: AppState) => state.system.anonymousMode
|
||||||
|
);
|
||||||
const displayListObjects = hasPermission(bucketName, [
|
const displayListObjects = hasPermission(bucketName, [
|
||||||
IAM_SCOPES.S3_LIST_BUCKET,
|
IAM_SCOPES.S3_LIST_BUCKET,
|
||||||
IAM_SCOPES.S3_ALL_LIST_BUCKET,
|
IAM_SCOPES.S3_ALL_LIST_BUCKET,
|
||||||
@@ -141,29 +149,43 @@ const ListObjectsTable = () => {
|
|||||||
payload = sortASC.reverse();
|
payload = sortASC.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
const openPath = (idElement: string) => {
|
const openPath = (object: IFileInfo) => {
|
||||||
dispatch(setSelectedObjects([]));
|
const idElement = object.name;
|
||||||
|
|
||||||
const newPath = `/browser/${bucketName}${
|
const newPath = `/browser/${bucketName}${
|
||||||
idElement ? `/${encodeURLString(idElement)}` : ``
|
idElement ? `/${encodeURLString(idElement)}` : ``
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
// for anonymous start download
|
||||||
|
if (anonymousMode && internalPaths !== null && !object.name.endsWith("/")) {
|
||||||
|
downloadObject(
|
||||||
|
dispatch,
|
||||||
|
bucketName,
|
||||||
|
`${encodeURLString(idElement)}`,
|
||||||
|
object
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(setSelectedObjects([]));
|
||||||
|
|
||||||
navigate(newPath);
|
navigate(newPath);
|
||||||
|
|
||||||
dispatch(setObjectDetailsView(true));
|
if (!anonymousMode) {
|
||||||
dispatch(setLoadingVersions(true));
|
dispatch(setObjectDetailsView(true));
|
||||||
|
dispatch(setLoadingVersions(true));
|
||||||
|
dispatch(setIsOpeningOD(true));
|
||||||
|
}
|
||||||
dispatch(
|
dispatch(
|
||||||
setSelectedObjectView(
|
setSelectedObjectView(
|
||||||
`${idElement ? `${encodeURLString(idElement)}` : ``}`
|
`${idElement ? `${encodeURLString(idElement)}` : ``}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
dispatch(setIsOpeningOD(true));
|
|
||||||
};
|
};
|
||||||
const tableActions: ItemActions[] = [
|
const tableActions: ItemActions[] = [
|
||||||
{
|
{
|
||||||
type: "view",
|
type: "view",
|
||||||
label: "View",
|
label: "View",
|
||||||
onClick: openPath,
|
onClick: openPath,
|
||||||
sendOnlyId: true,
|
sendOnlyId: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -218,9 +240,9 @@ const ListObjectsTable = () => {
|
|||||||
obOnly ? "isEmbedded" : ""
|
obOnly ? "isEmbedded" : ""
|
||||||
} ${detailsOpen ? "actionsPanelOpen" : ""}`}
|
} ${detailsOpen ? "actionsPanelOpen" : ""}`}
|
||||||
selectedItems={selectedObjects}
|
selectedItems={selectedObjects}
|
||||||
onSelect={selectListObjects}
|
onSelect={!anonymousMode ? selectListObjects : undefined}
|
||||||
customEmptyMessage={
|
customEmptyMessage={
|
||||||
!displayListObjects
|
!displayListObjects && !anonymousMode
|
||||||
? permissionTooltipHelper(
|
? permissionTooltipHelper(
|
||||||
[IAM_SCOPES.S3_LIST_BUCKET, IAM_SCOPES.S3_ALL_LIST_BUCKET],
|
[IAM_SCOPES.S3_LIST_BUCKET, IAM_SCOPES.S3_ALL_LIST_BUCKET],
|
||||||
"view Objects in this bucket"
|
"view Objects in this bucket"
|
||||||
|
|||||||
@@ -18,7 +18,21 @@ import React, { Fragment, useEffect, useState } from "react";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { withStyles } from "@mui/styles";
|
import { withStyles } from "@mui/styles";
|
||||||
import { Button } from "mds";
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
InspectMenuIcon,
|
||||||
|
LegalHoldIcon,
|
||||||
|
Loader,
|
||||||
|
MetadataIcon,
|
||||||
|
ObjectInfoIcon,
|
||||||
|
PreviewIcon,
|
||||||
|
RetentionIcon,
|
||||||
|
ShareIcon,
|
||||||
|
TagsIcon,
|
||||||
|
VersionsIcon,
|
||||||
|
} from "mds";
|
||||||
import createStyles from "@mui/styles/createStyles";
|
import createStyles from "@mui/styles/createStyles";
|
||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
@@ -30,13 +44,11 @@ import {
|
|||||||
textStyleUtils,
|
textStyleUtils,
|
||||||
} from "../../../../Common/FormComponents/common/styleLibrary";
|
} from "../../../../Common/FormComponents/common/styleLibrary";
|
||||||
import { IFileInfo, MetadataResponse } from "../ObjectDetails/types";
|
import { IFileInfo, MetadataResponse } from "../ObjectDetails/types";
|
||||||
import { download, extensionPreview } from "../utils";
|
import { extensionPreview } from "../utils";
|
||||||
import { ErrorResponseHandler } from "../../../../../../common/types";
|
import { ErrorResponseHandler } from "../../../../../../common/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
decodeURLString,
|
decodeURLString,
|
||||||
encodeURLString,
|
|
||||||
getClientOS,
|
|
||||||
niceBytes,
|
niceBytes,
|
||||||
niceBytesInt,
|
niceBytesInt,
|
||||||
niceDaysInt,
|
niceDaysInt,
|
||||||
@@ -46,19 +58,6 @@ import {
|
|||||||
permissionTooltipHelper,
|
permissionTooltipHelper,
|
||||||
} from "../../../../../../common/SecureComponent/permissions";
|
} from "../../../../../../common/SecureComponent/permissions";
|
||||||
import { AppState, useAppDispatch } from "../../../../../../store";
|
import { AppState, useAppDispatch } from "../../../../../../store";
|
||||||
import {
|
|
||||||
DeleteIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
LegalHoldIcon,
|
|
||||||
MetadataIcon,
|
|
||||||
ObjectInfoIcon,
|
|
||||||
PreviewIcon,
|
|
||||||
RetentionIcon,
|
|
||||||
ShareIcon,
|
|
||||||
TagsIcon,
|
|
||||||
VersionsIcon,
|
|
||||||
} from "mds";
|
|
||||||
import { InspectMenuIcon } from "mds";
|
|
||||||
import api from "../../../../../../common/api";
|
import api from "../../../../../../common/api";
|
||||||
import ShareFile from "../ObjectDetails/ShareFile";
|
import ShareFile from "../ObjectDetails/ShareFile";
|
||||||
import SetRetention from "../ObjectDetails/SetRetention";
|
import SetRetention from "../ObjectDetails/SetRetention";
|
||||||
@@ -74,25 +73,16 @@ import ActionsListSection from "./ActionsListSection";
|
|||||||
import { displayFileIconName } from "./utils";
|
import { displayFileIconName } from "./utils";
|
||||||
import TagsModal from "../ObjectDetails/TagsModal";
|
import TagsModal from "../ObjectDetails/TagsModal";
|
||||||
import InspectObject from "./InspectObject";
|
import InspectObject from "./InspectObject";
|
||||||
import { Loader } from "mds";
|
|
||||||
import { selDistSet } from "../../../../../../systemSlice";
|
import { selDistSet } from "../../../../../../systemSlice";
|
||||||
import {
|
import {
|
||||||
makeid,
|
|
||||||
storeCallForObjectWithID,
|
|
||||||
} from "../../../../ObjectBrowser/transferManager";
|
|
||||||
import {
|
|
||||||
cancelObjectInList,
|
|
||||||
completeObject,
|
|
||||||
failObject,
|
|
||||||
setLoadingObjectInfo,
|
setLoadingObjectInfo,
|
||||||
setLoadingVersions,
|
setLoadingVersions,
|
||||||
setNewObject,
|
|
||||||
setSelectedVersion,
|
setSelectedVersion,
|
||||||
setVersionsModeEnabled,
|
setVersionsModeEnabled,
|
||||||
updateProgress,
|
|
||||||
} from "../../../../ObjectBrowser/objectBrowserSlice";
|
} from "../../../../ObjectBrowser/objectBrowserSlice";
|
||||||
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
|
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
|
||||||
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
|
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
|
||||||
|
import { downloadObject } from "../../../../ObjectBrowser/utils";
|
||||||
|
|
||||||
const styles = () =>
|
const styles = () =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@@ -325,65 +315,6 @@ const ObjectDetailPanel = ({
|
|||||||
setLongFileOpen(false);
|
setLongFileOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadObject = (object: IFileInfo) => {
|
|
||||||
const identityDownload = encodeURLString(
|
|
||||||
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
object.name.length > 200 &&
|
|
||||||
getClientOS().toLowerCase().includes("win")
|
|
||||||
) {
|
|
||||||
setLongFileOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ID = makeid(8);
|
|
||||||
|
|
||||||
const downloadCall = download(
|
|
||||||
bucketName,
|
|
||||||
internalPaths,
|
|
||||||
object.version_id,
|
|
||||||
parseInt(object.size || "0"),
|
|
||||||
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 closeDeleteModal = (closeAndReload: boolean) => {
|
const closeDeleteModal = (closeAndReload: boolean) => {
|
||||||
setDeleteOpen(false);
|
setDeleteOpen(false);
|
||||||
|
|
||||||
@@ -482,7 +413,7 @@ const ObjectDetailPanel = ({
|
|||||||
const multiActionButtons = [
|
const multiActionButtons = [
|
||||||
{
|
{
|
||||||
action: () => {
|
action: () => {
|
||||||
downloadObject(actualInfo);
|
downloadObject(dispatch, bucketName, internalPaths, actualInfo);
|
||||||
},
|
},
|
||||||
label: "Download",
|
label: "Download",
|
||||||
disabled: !!actualInfo.is_delete_marker || !canGetObject,
|
disabled: !!actualInfo.is_delete_marker || !canGetObject,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { BucketObjectItem } from "./ListObjects/types";
|
|||||||
import { IAllowResources } from "../../../types";
|
import { IAllowResources } from "../../../types";
|
||||||
import { encodeURLString } from "../../../../../common/utils";
|
import { encodeURLString } from "../../../../../common/utils";
|
||||||
import { removeTrace } from "../../../ObjectBrowser/transferManager";
|
import { removeTrace } from "../../../ObjectBrowser/transferManager";
|
||||||
|
import store from "../../../../../store";
|
||||||
|
|
||||||
export const download = (
|
export const download = (
|
||||||
bucketName: string,
|
bucketName: string,
|
||||||
@@ -34,6 +35,8 @@ export const download = (
|
|||||||
const anchor = document.createElement("a");
|
const anchor = document.createElement("a");
|
||||||
document.body.appendChild(anchor);
|
document.body.appendChild(anchor);
|
||||||
let basename = document.baseURI.replace(window.location.origin, "");
|
let basename = document.baseURI.replace(window.location.origin, "");
|
||||||
|
const state = store.getState();
|
||||||
|
const anonymousMode = state.system.anonymousMode;
|
||||||
|
|
||||||
let path = `${
|
let path = `${
|
||||||
window.location.origin
|
window.location.origin
|
||||||
@@ -48,6 +51,9 @@ export const download = (
|
|||||||
|
|
||||||
var req = new XMLHttpRequest();
|
var req = new XMLHttpRequest();
|
||||||
req.open("GET", path, true);
|
req.open("GET", path, true);
|
||||||
|
if (anonymousMode) {
|
||||||
|
req.setRequestHeader("X-Anonymous", "1");
|
||||||
|
}
|
||||||
req.addEventListener(
|
req.addEventListener(
|
||||||
"progress",
|
"progress",
|
||||||
function (evt) {
|
function (evt) {
|
||||||
|
|||||||
@@ -14,21 +14,22 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
import { Theme } from "@mui/material/styles";
|
import { Theme } from "@mui/material/styles";
|
||||||
import { Menu, MenuItem } from "@mui/material";
|
import { Menu, MenuItem } from "@mui/material";
|
||||||
import createStyles from "@mui/styles/createStyles";
|
import createStyles from "@mui/styles/createStyles";
|
||||||
import withStyles from "@mui/styles/withStyles";
|
import withStyles from "@mui/styles/withStyles";
|
||||||
import ListItemText from "@mui/material/ListItemText";
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import { UploadFolderIcon, UploadIcon } from "mds";
|
import { Button, UploadFolderIcon, UploadIcon } from "mds";
|
||||||
import {
|
import {
|
||||||
IAM_SCOPES,
|
IAM_SCOPES,
|
||||||
permissionTooltipHelper,
|
permissionTooltipHelper,
|
||||||
} from "../../../../common/SecureComponent/permissions";
|
} from "../../../../common/SecureComponent/permissions";
|
||||||
import { hasPermission } from "../../../../common/SecureComponent";
|
import { hasPermission } from "../../../../common/SecureComponent";
|
||||||
import { Button } from "mds";
|
|
||||||
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
|
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { AppState } from "../../../../store";
|
||||||
|
|
||||||
interface IUploadFilesButton {
|
interface IUploadFilesButton {
|
||||||
uploadPath: string;
|
uploadPath: string;
|
||||||
@@ -58,7 +59,10 @@ const UploadFilesButton = ({
|
|||||||
uploadFolderFunction,
|
uploadFolderFunction,
|
||||||
classes,
|
classes,
|
||||||
}: IUploadFilesButton) => {
|
}: IUploadFilesButton) => {
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const anonymousMode = useSelector(
|
||||||
|
(state: AppState) => state.system.anonymousMode
|
||||||
|
);
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const openUploadMenu = Boolean(anchorEl);
|
const openUploadMenu = Boolean(anchorEl);
|
||||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@@ -67,10 +71,11 @@ const UploadFilesButton = ({
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadObjectAllowed = hasPermission(uploadPath, [
|
const uploadObjectAllowed =
|
||||||
IAM_SCOPES.S3_PUT_OBJECT,
|
hasPermission(uploadPath, [
|
||||||
IAM_SCOPES.S3_PUT_ACTIONS,
|
IAM_SCOPES.S3_PUT_OBJECT,
|
||||||
]);
|
IAM_SCOPES.S3_PUT_ACTIONS,
|
||||||
|
]) || anonymousMode;
|
||||||
const uploadFolderAllowed = hasPermission(
|
const uploadFolderAllowed = hasPermission(
|
||||||
bucketName,
|
bucketName,
|
||||||
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
|
[IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
|
||||||
|
|||||||
@@ -456,12 +456,6 @@ export const objectBrowserCommon = {
|
|||||||
flexDirection: "row" as const,
|
flexDirection: "row" as const,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actionsSection: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
width: "100%",
|
|
||||||
marginTop: 15,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ** According to W3 spec, default minimum values for flex width flex-grow is "auto" (https://drafts.csswg.org/css-flexbox/#min-size-auto). So in this case we need to enforce the use of an absolute width.
|
// ** According to W3 spec, default minimum values for flex width flex-grow is "auto" (https://drafts.csswg.org/css-flexbox/#min-size-auto). So in this case we need to enforce the use of an absolute width.
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
cleanList,
|
cleanList,
|
||||||
deleteFromList,
|
deleteFromList,
|
||||||
} from "../../ObjectBrowser/objectBrowserSlice";
|
} from "../../ObjectBrowser/objectBrowserSlice";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
const styles = (theme: Theme) =>
|
const styles = (theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@@ -36,7 +37,7 @@ const styles = (theme: Theme) =>
|
|||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: 20,
|
right: 20,
|
||||||
top: 60,
|
top: 62,
|
||||||
width: 400,
|
width: 400,
|
||||||
overflowY: "hidden",
|
overflowY: "hidden",
|
||||||
overflowX: "hidden",
|
overflowX: "hidden",
|
||||||
@@ -51,6 +52,9 @@ const styles = (theme: Theme) =>
|
|||||||
minHeight: 400,
|
minHeight: 400,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
downloadContainerAnonymous: {
|
||||||
|
top: 70,
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
@@ -94,13 +98,17 @@ const ObjectManager = ({ classes }: IObjectManager) => {
|
|||||||
const managerOpen = useSelector(
|
const managerOpen = useSelector(
|
||||||
(state: AppState) => state.objectBrowser.objectManager.managerOpen
|
(state: AppState) => state.objectBrowser.objectManager.managerOpen
|
||||||
);
|
);
|
||||||
|
const anonymousMode = useSelector(
|
||||||
|
(state: AppState) => state.system.anonymousMode
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{managerOpen && (
|
{managerOpen && (
|
||||||
<div
|
<div
|
||||||
className={`${classes.downloadContainer} ${
|
className={clsx(classes.downloadContainer, {
|
||||||
managerOpen ? "open" : ""
|
[classes.downloadContainerAnonymous]: anonymousMode,
|
||||||
}`}
|
open: managerOpen,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<div className={classes.cleanIcon}>
|
<div className={classes.cleanIcon}>
|
||||||
<Tooltip title={"Clean Completed Objects"} placement="bottom-start">
|
<Tooltip title={"Clean Completed Objects"} placement="bottom-start">
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2023 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, useEffect, useState } from "react";
|
||||||
|
import { Button, CircleIcon, ObjectManagerIcon } from "mds";
|
||||||
|
import { toggleList } from "../../ObjectBrowser/objectBrowserSlice";
|
||||||
|
import { AppState, useAppDispatch } from "../../../../store";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
indicator: {
|
||||||
|
position: "absolute",
|
||||||
|
display: "block",
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
top: 0,
|
||||||
|
right: 4,
|
||||||
|
marginTop: 4,
|
||||||
|
transitionDuration: "0.2s",
|
||||||
|
color: "#32C787",
|
||||||
|
"& svg": {
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transitionDuration: "0.2s",
|
||||||
|
},
|
||||||
|
"&.newItem": {
|
||||||
|
color: "#2781B0",
|
||||||
|
"& svg": {
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ObjectManagerButton = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const classes = useStyles();
|
||||||
|
const managerObjects = useSelector(
|
||||||
|
(state: AppState) => state.objectBrowser.objectManager.objectsToManage
|
||||||
|
);
|
||||||
|
const newItems = useSelector(
|
||||||
|
(state: AppState) => state.objectBrowser.objectManager.newItems
|
||||||
|
);
|
||||||
|
const managerOpen = useSelector(
|
||||||
|
(state: AppState) => state.objectBrowser.objectManager.managerOpen
|
||||||
|
);
|
||||||
|
|
||||||
|
const [newObject, setNewObject] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (managerObjects.length > 0 && !managerOpen) {
|
||||||
|
setNewObject(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setNewObject(false);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}, [managerObjects.length, managerOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{managerObjects && managerObjects.length > 0 && (
|
||||||
|
<Button
|
||||||
|
aria-label="Refresh List"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(toggleList());
|
||||||
|
}}
|
||||||
|
icon={
|
||||||
|
<Fragment>
|
||||||
|
<div
|
||||||
|
className={`${classes.indicator} ${newObject ? "newItem" : ""}`}
|
||||||
|
style={{
|
||||||
|
opacity: managerObjects.length > 0 && newItems ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircleIcon />
|
||||||
|
</div>
|
||||||
|
<ObjectManagerIcon
|
||||||
|
style={{ width: 20, height: 20, marginTop: -2 }}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
id="object-manager-toggle"
|
||||||
|
style={{ position: "relative", padding: "0 10px" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ObjectManagerButton;
|
||||||
@@ -33,8 +33,8 @@ const TrafficMonitor = () => {
|
|||||||
(state: AppState) => state.objectBrowser.objectManager.objectsToManage
|
(state: AppState) => state.objectBrowser.objectManager.objectsToManage
|
||||||
);
|
);
|
||||||
|
|
||||||
const limitVars = useSelector(
|
const limitVars = useSelector((state: AppState) =>
|
||||||
(state: AppState) => state.console.session.envConstants
|
state.console.session ? state.console.session.envConstants : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentDIP = useSelector(
|
const currentDIP = useSelector(
|
||||||
|
|||||||
@@ -14,20 +14,20 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Fragment, useEffect, useState } from "react";
|
import React, { Fragment } from "react";
|
||||||
import { Theme } from "@mui/material/styles";
|
import { Theme } from "@mui/material/styles";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import createStyles from "@mui/styles/createStyles";
|
import createStyles from "@mui/styles/createStyles";
|
||||||
import withStyles from "@mui/styles/withStyles";
|
import withStyles from "@mui/styles/withStyles";
|
||||||
import { AppState, useAppDispatch } from "../../../../store";
|
import { AppState } from "../../../../store";
|
||||||
|
|
||||||
import { CircleIcon, ObjectManagerIcon } from "mds";
|
import { ApplicationLogo } from "mds";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { toggleList } from "../../ObjectBrowser/objectBrowserSlice";
|
|
||||||
import { selFeatures } from "../../consoleSlice";
|
import { selFeatures } from "../../consoleSlice";
|
||||||
import { selDirectPVMode, selOpMode } from "../../../../systemSlice";
|
import { selDirectPVMode, selOpMode } from "../../../../systemSlice";
|
||||||
import { ApplicationLogo, Button } from "mds";
|
import ObjectManagerButton from "../ObjectManager/ObjectManagerButton";
|
||||||
|
import { getLogoVar } from "../../../../config";
|
||||||
|
|
||||||
const styles = (theme: Theme) =>
|
const styles = (theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@@ -63,31 +63,6 @@ const styles = (theme: Theme) =>
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
indicator: {
|
|
||||||
position: "absolute",
|
|
||||||
display: "block",
|
|
||||||
width: 15,
|
|
||||||
height: 15,
|
|
||||||
top: 0,
|
|
||||||
right: 4,
|
|
||||||
marginTop: 4,
|
|
||||||
transitionDuration: "0.2s",
|
|
||||||
color: "#32C787",
|
|
||||||
"& svg": {
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transitionDuration: "0.2s",
|
|
||||||
},
|
|
||||||
"&.newItem": {
|
|
||||||
color: "#2781B0",
|
|
||||||
"& svg": {
|
|
||||||
width: 15,
|
|
||||||
height: 15,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IPageHeader {
|
interface IPageHeader {
|
||||||
@@ -103,45 +78,12 @@ const PageHeader = ({
|
|||||||
actions,
|
actions,
|
||||||
middleComponent,
|
middleComponent,
|
||||||
}: IPageHeader) => {
|
}: IPageHeader) => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const sidebarOpen = useSelector(
|
const sidebarOpen = useSelector(
|
||||||
(state: AppState) => state.system.sidebarOpen
|
(state: AppState) => state.system.sidebarOpen
|
||||||
);
|
);
|
||||||
const operatorMode = useSelector(selOpMode);
|
const operatorMode = useSelector(selOpMode);
|
||||||
const directPVMode = useSelector(selDirectPVMode);
|
const directPVMode = useSelector(selDirectPVMode);
|
||||||
const managerObjects = useSelector(
|
|
||||||
(state: AppState) => state.objectBrowser.objectManager.objectsToManage
|
|
||||||
);
|
|
||||||
const features = useSelector(selFeatures);
|
const features = useSelector(selFeatures);
|
||||||
const managerOpen = useSelector(
|
|
||||||
(state: AppState) => state.objectBrowser.objectManager.managerOpen
|
|
||||||
);
|
|
||||||
const newItems = useSelector(
|
|
||||||
(state: AppState) => state.objectBrowser.objectManager.newItems
|
|
||||||
);
|
|
||||||
const licenseInfo = useSelector(
|
|
||||||
(state: AppState) => state?.system?.licenseInfo
|
|
||||||
);
|
|
||||||
|
|
||||||
const [newObject, setNewObject] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const { plan = "" } = licenseInfo || {};
|
|
||||||
|
|
||||||
let logoPlan = "AGPL";
|
|
||||||
|
|
||||||
if (plan === "STANDARD" || plan === "ENTERPRISE") {
|
|
||||||
logoPlan = plan.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (managerObjects.length > 0 && !managerOpen) {
|
|
||||||
setNewObject(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setNewObject(false);
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}, [managerObjects.length, managerOpen]);
|
|
||||||
|
|
||||||
if (features.includes("hide-menu")) {
|
if (features.includes("hide-menu")) {
|
||||||
return <Fragment />;
|
return <Fragment />;
|
||||||
@@ -168,14 +110,7 @@ const PageHeader = ({
|
|||||||
{!operatorMode && !directPVMode ? (
|
{!operatorMode && !directPVMode ? (
|
||||||
<ApplicationLogo
|
<ApplicationLogo
|
||||||
applicationName={"console"}
|
applicationName={"console"}
|
||||||
subVariant={
|
subVariant={getLogoVar()}
|
||||||
logoPlan as
|
|
||||||
| "AGPL"
|
|
||||||
| "simple"
|
|
||||||
| "standard"
|
|
||||||
| "enterprise"
|
|
||||||
| undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -220,33 +155,7 @@ const PageHeader = ({
|
|||||||
className={classes.rightMenu}
|
className={classes.rightMenu}
|
||||||
>
|
>
|
||||||
{actions && actions}
|
{actions && actions}
|
||||||
{managerObjects && managerObjects.length > 0 && (
|
<ObjectManagerButton />
|
||||||
<Button
|
|
||||||
aria-label="Refresh List"
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(toggleList());
|
|
||||||
}}
|
|
||||||
icon={
|
|
||||||
<Fragment>
|
|
||||||
<div
|
|
||||||
className={`${classes.indicator} ${
|
|
||||||
newObject ? "newItem" : ""
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
opacity: managerObjects.length > 0 && newItems ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CircleIcon />
|
|
||||||
</div>
|
|
||||||
<ObjectManagerIcon
|
|
||||||
style={{ width: 20, height: 20, marginTop: -2 }}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
id="object-manager-toggle"
|
|
||||||
style={{ position: "relative", padding: "0 10px" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,12 +17,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import { Theme } from "@mui/material/styles";
|
import { Theme } from "@mui/material/styles";
|
||||||
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
import createStyles from "@mui/styles/createStyles";
|
|
||||||
import withStyles from "@mui/styles/withStyles";
|
|
||||||
|
|
||||||
interface IScreenTitle {
|
interface IScreenTitle {
|
||||||
classes: any;
|
|
||||||
icon?: any;
|
icon?: any;
|
||||||
title?: any;
|
title?: any;
|
||||||
subTitle?: any;
|
subTitle?: any;
|
||||||
@@ -30,80 +27,79 @@ interface IScreenTitle {
|
|||||||
className?: any;
|
className?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = (theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
createStyles({
|
headerBarIcon: {
|
||||||
headerBarIcon: {
|
marginRight: ".7rem",
|
||||||
marginRight: ".7rem",
|
color: theme.palette.primary.main,
|
||||||
color: theme.palette.primary.main,
|
"& .min-icon": {
|
||||||
"& .min-icon": {
|
width: 44,
|
||||||
width: 44,
|
height: 44,
|
||||||
height: 44,
|
|
||||||
},
|
|
||||||
"@media (max-width: 600px)": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
headerBarSubheader: {
|
"@media (max-width: 600px)": {
|
||||||
color: "grey",
|
display: "none",
|
||||||
"@media (max-width: 900px)": {
|
|
||||||
maxWidth: 200,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
screenTitle: {
|
},
|
||||||
display: "flex",
|
headerBarSubheader: {
|
||||||
alignItems: "center",
|
color: "grey",
|
||||||
justifyContent: "space-between",
|
"@media (max-width: 900px)": {
|
||||||
padding: "1rem",
|
maxWidth: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stContainer: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: 8,
|
||||||
|
|
||||||
borderBottom: "1px solid #EAEAEA",
|
borderBottom: "1px solid #EAEAEA",
|
||||||
"@media (max-width: 600px)": {
|
"@media (max-width: 600px)": {
|
||||||
flexFlow: "column",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
titleColumn: {
|
|
||||||
height: "auto",
|
|
||||||
justifyContent: "center",
|
|
||||||
display: "flex",
|
|
||||||
flexFlow: "column",
|
flexFlow: "column",
|
||||||
alignItems: "flex-start",
|
|
||||||
"& h1": {
|
|
||||||
fontSize: 19,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
leftItems: {
|
},
|
||||||
display: "flex",
|
titleColumn: {
|
||||||
alignItems: "center",
|
height: "auto",
|
||||||
"@media (max-width: 600px)": {
|
justifyContent: "center",
|
||||||
flexFlow: "column",
|
display: "flex",
|
||||||
width: "100%",
|
flexFlow: "column",
|
||||||
},
|
alignItems: "flex-start",
|
||||||
|
"& h1": {
|
||||||
|
fontSize: 19,
|
||||||
},
|
},
|
||||||
rightItems: {
|
},
|
||||||
display: "flex",
|
leftItems: {
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
"& button": {
|
alignItems: "center",
|
||||||
marginLeft: 8,
|
"@media (max-width: 600px)": {
|
||||||
},
|
flexFlow: "column",
|
||||||
"@media (max-width: 600px)": {
|
width: "100%",
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
rightItems: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
"& button": {
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
"@media (max-width: 600px)": {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const ScreenTitle = ({
|
const ScreenTitle = ({
|
||||||
classes,
|
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
subTitle,
|
subTitle,
|
||||||
actions,
|
actions,
|
||||||
className,
|
className,
|
||||||
}: IScreenTitle) => {
|
}: IScreenTitle) => {
|
||||||
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid
|
<Grid
|
||||||
item
|
item
|
||||||
xs={12}
|
xs={12}
|
||||||
className={`${classes.screenTitle} ${className ? className : ""}`}
|
className={`${classes.stContainer} ${className ? className : ""}`}
|
||||||
>
|
>
|
||||||
<div className={classes.leftItems}>
|
<div className={classes.leftItems}>
|
||||||
{icon ? <div className={classes.headerBarIcon}>{icon}</div> : null}
|
{icon ? <div className={classes.headerBarIcon}>{icon}</div> : null}
|
||||||
@@ -119,4 +115,4 @@ const ScreenTitle = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withStyles(styles)(ScreenTitle);
|
export default ScreenTitle;
|
||||||
|
|||||||
@@ -20,13 +20,29 @@ import { useSelector } from "react-redux";
|
|||||||
import CommandBar from "./CommandBar";
|
import CommandBar from "./CommandBar";
|
||||||
import { selFeatures } from "./consoleSlice";
|
import { selFeatures } from "./consoleSlice";
|
||||||
import TrafficMonitor from "./Common/ObjectManager/TrafficMonitor";
|
import TrafficMonitor from "./Common/ObjectManager/TrafficMonitor";
|
||||||
|
import { AppState } from "../../store";
|
||||||
|
import AnonymousAccess from "../AnonymousAccess/AnonymousAccess";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
const ConsoleKBar = () => {
|
const ConsoleKBar = () => {
|
||||||
const features = useSelector(selFeatures);
|
const features = useSelector(selFeatures);
|
||||||
|
const anonymousMode = useSelector(
|
||||||
|
(state: AppState) => state.system.anonymousMode
|
||||||
|
);
|
||||||
|
|
||||||
// if we are hiding the menu also disable the k-bar so just return console
|
// if we are hiding the menu also disable the k-bar so just return console
|
||||||
if (features?.includes("hide-menu")) {
|
if (features?.includes("hide-menu")) {
|
||||||
return <Console />;
|
return <Console />;
|
||||||
}
|
}
|
||||||
|
// for anonymous mode, we don't load Console, only AnonymousAccess
|
||||||
|
if (anonymousMode) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<TrafficMonitor />
|
||||||
|
<AnonymousAccess />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KBarProvider
|
<KBarProvider
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import ScreenTitle from "../Common/ScreenTitle/ScreenTitle";
|
|||||||
import DeleteIDPConfigurationModal from "./DeleteIDPConfigurationModal";
|
import DeleteIDPConfigurationModal from "./DeleteIDPConfigurationModal";
|
||||||
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
|
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
|
||||||
import LabelValuePair from "../Common/UsageBarWrapper/LabelValuePair";
|
import LabelValuePair from "../Common/UsageBarWrapper/LabelValuePair";
|
||||||
|
|
||||||
type IDPConfigurationDetailsProps = {
|
type IDPConfigurationDetailsProps = {
|
||||||
classes?: any;
|
classes?: any;
|
||||||
formFields: object;
|
formFields: object;
|
||||||
@@ -384,9 +383,6 @@ const IDPConfigurationDetails = ({
|
|||||||
<PageLayout className={classes.pageContainer}>
|
<PageLayout className={classes.pageContainer}>
|
||||||
<Box>
|
<Box>
|
||||||
<ScreenTitle
|
<ScreenTitle
|
||||||
classes={{
|
|
||||||
screenTitle: classes.screenTitle,
|
|
||||||
}}
|
|
||||||
icon={icon}
|
icon={icon}
|
||||||
title={configurationName === "_" ? "Default" : configurationName}
|
title={configurationName === "_" ? "Default" : configurationName}
|
||||||
actions={
|
actions={
|
||||||
|
|||||||
@@ -79,13 +79,17 @@ const BrowserBreadcrumbs = ({
|
|||||||
const versionedFile = useSelector(
|
const versionedFile = useSelector(
|
||||||
(state: AppState) => state.objectBrowser.versionedFile
|
(state: AppState) => state.objectBrowser.versionedFile
|
||||||
);
|
);
|
||||||
|
const anonymousMode = useSelector(
|
||||||
|
(state: AppState) => state.system.anonymousMode
|
||||||
|
);
|
||||||
|
|
||||||
const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
|
const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const canCreatePath = hasPermission(bucketName, [
|
const canCreatePath =
|
||||||
IAM_SCOPES.S3_PUT_OBJECT,
|
hasPermission(bucketName, [
|
||||||
IAM_SCOPES.S3_PUT_ACTIONS,
|
IAM_SCOPES.S3_PUT_OBJECT,
|
||||||
]);
|
IAM_SCOPES.S3_PUT_ACTIONS,
|
||||||
|
]) || anonymousMode;
|
||||||
|
|
||||||
let paths = internalPaths;
|
let paths = internalPaths;
|
||||||
|
|
||||||
@@ -240,7 +244,7 @@ const BrowserBreadcrumbs = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCreateFolderOpen(true);
|
setCreateFolderOpen(true);
|
||||||
}}
|
}}
|
||||||
disabled={rewindEnabled || !canCreatePath}
|
disabled={anonymousMode ? false : rewindEnabled || !canCreatePath}
|
||||||
icon={<NewPathIcon style={{ fill: "#969FA8" }} />}
|
icon={<NewPathIcon style={{ fill: "#969FA8" }} />}
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2023 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 from "react";
|
||||||
|
import { setSearchObjects } from "./objectBrowserSlice";
|
||||||
|
import SearchBox from "../Common/SearchBox";
|
||||||
|
import { AppState, useAppDispatch } from "../../../store";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const FilterObjectsSB = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const searchObjects = useSelector(
|
||||||
|
(state: AppState) => state.objectBrowser.searchObjects
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<SearchBox
|
||||||
|
placeholder={"Start typing to filter objects in the bucket"}
|
||||||
|
onChange={(value) => {
|
||||||
|
dispatch(setSearchObjects(value));
|
||||||
|
}}
|
||||||
|
value={searchObjects}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default FilterObjectsSB;
|
||||||
178
portal-ui/src/screens/Console/ObjectBrowser/OBHeader.tsx
Normal file
178
portal-ui/src/screens/Console/ObjectBrowser/OBHeader.tsx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2023 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 PageHeader from "../Common/PageHeader/PageHeader";
|
||||||
|
import BackLink from "../../../common/BackLink";
|
||||||
|
import {
|
||||||
|
IAM_PAGES,
|
||||||
|
IAM_PERMISSIONS,
|
||||||
|
IAM_ROLES,
|
||||||
|
IAM_SCOPES,
|
||||||
|
} from "../../../common/SecureComponent/permissions";
|
||||||
|
import { SecureComponent } from "../../../common/SecureComponent";
|
||||||
|
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
|
||||||
|
import { Button, SettingsIcon } from "mds";
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
import AutoColorIcon from "../Common/Components/AutoColorIcon";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { selFeatures } from "../consoleSlice";
|
||||||
|
import hasPermission from "../../../common/SecureComponent/accessControl";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import SearchBox from "../Common/SearchBox";
|
||||||
|
import { setSearchVersions } from "./objectBrowserSlice";
|
||||||
|
import { AppState, useAppDispatch } from "../../../store";
|
||||||
|
import FilterObjectsSB from "./FilterObjectsSB";
|
||||||
|
|
||||||
|
interface IOBHeader {
|
||||||
|
bucketName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OBHeader = ({ bucketName }: IOBHeader) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const features = useSelector(selFeatures);
|
||||||
|
|
||||||
|
const versionsMode = useSelector(
|
||||||
|
(state: AppState) => state.objectBrowser.versionsMode
|
||||||
|
);
|
||||||
|
const versionedFile = useSelector(
|
||||||
|
(state: AppState) => state.objectBrowser.versionedFile
|
||||||
|
);
|
||||||
|
const searchVersions = useSelector(
|
||||||
|
(state: AppState) => state.objectBrowser.searchVersions
|
||||||
|
);
|
||||||
|
|
||||||
|
const obOnly = !!features?.includes("object-browser-only");
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const configureBucketAllowed = hasPermission(bucketName, [
|
||||||
|
IAM_SCOPES.S3_GET_BUCKET_POLICY,
|
||||||
|
IAM_SCOPES.S3_PUT_BUCKET_POLICY,
|
||||||
|
IAM_SCOPES.S3_GET_BUCKET_VERSIONING,
|
||||||
|
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
|
||||||
|
IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION,
|
||||||
|
IAM_SCOPES.S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION,
|
||||||
|
IAM_SCOPES.S3_DELETE_BUCKET,
|
||||||
|
IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS,
|
||||||
|
IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS,
|
||||||
|
IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
|
||||||
|
IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION,
|
||||||
|
IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION,
|
||||||
|
IAM_SCOPES.S3_PUT_LIFECYCLE_CONFIGURATION,
|
||||||
|
IAM_SCOPES.ADMIN_GET_BUCKET_QUOTA,
|
||||||
|
IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA,
|
||||||
|
IAM_SCOPES.S3_PUT_BUCKET_TAGGING,
|
||||||
|
IAM_SCOPES.S3_GET_BUCKET_TAGGING,
|
||||||
|
IAM_SCOPES.S3_LIST_BUCKET_VERSIONS,
|
||||||
|
IAM_SCOPES.S3_GET_BUCKET_POLICY_STATUS,
|
||||||
|
IAM_SCOPES.S3_DELETE_BUCKET_POLICY,
|
||||||
|
IAM_SCOPES.S3_GET_ACTIONS,
|
||||||
|
IAM_SCOPES.S3_PUT_ACTIONS,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const openBucketConfiguration = () => {
|
||||||
|
navigate(`/buckets/${bucketName}/admin`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchBar = (
|
||||||
|
<Fragment>
|
||||||
|
{!versionsMode ? (
|
||||||
|
<SecureComponent
|
||||||
|
scopes={[IAM_SCOPES.S3_LIST_BUCKET, IAM_SCOPES.S3_ALL_LIST_BUCKET]}
|
||||||
|
resource={bucketName}
|
||||||
|
errorProps={{ disabled: true }}
|
||||||
|
>
|
||||||
|
<FilterObjectsSB />
|
||||||
|
</SecureComponent>
|
||||||
|
) : (
|
||||||
|
<Fragment>
|
||||||
|
<SearchBox
|
||||||
|
placeholder={`Start typing to filter versions of ${versionedFile}`}
|
||||||
|
onChange={(value) => {
|
||||||
|
dispatch(setSearchVersions(value));
|
||||||
|
}}
|
||||||
|
value={searchVersions}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{!obOnly ? (
|
||||||
|
<PageHeader
|
||||||
|
label={
|
||||||
|
<BackLink
|
||||||
|
label={"Object Browser"}
|
||||||
|
to={IAM_PAGES.OBJECT_BROWSER_VIEW}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<SecureComponent
|
||||||
|
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
|
||||||
|
resource={bucketName}
|
||||||
|
errorProps={{ disabled: true }}
|
||||||
|
>
|
||||||
|
<TooltipWrapper
|
||||||
|
tooltip={
|
||||||
|
configureBucketAllowed
|
||||||
|
? "Configure Bucket"
|
||||||
|
: "You do not have the required permissions to configure this bucket. Please contact your MinIO administrator to request " +
|
||||||
|
IAM_ROLES.BUCKET_ADMIN +
|
||||||
|
" permisions."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id={"configure-bucket-main"}
|
||||||
|
color="primary"
|
||||||
|
aria-label="Configure Bucket"
|
||||||
|
onClick={openBucketConfiguration}
|
||||||
|
icon={
|
||||||
|
<SettingsIcon
|
||||||
|
style={{ width: 20, height: 20, marginTop: -3 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
padding: "0 10px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TooltipWrapper>
|
||||||
|
</SecureComponent>
|
||||||
|
}
|
||||||
|
middleComponent={searchBar}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
sx={{
|
||||||
|
padding: "20px 32px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<AutoColorIcon marginRight={30} marginTop={10} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs>
|
||||||
|
{searchBar}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OBHeader;
|
||||||
@@ -71,6 +71,7 @@ const initialState: ObjectBrowserState = {
|
|||||||
unit: "",
|
unit: "",
|
||||||
validity: 0,
|
validity: 0,
|
||||||
},
|
},
|
||||||
|
longFileOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const objectBrowserSlice = createSlice({
|
export const objectBrowserSlice = createSlice({
|
||||||
@@ -356,6 +357,9 @@ export const objectBrowserSlice = createSlice({
|
|||||||
setSelectedBucket: (state, action: PayloadAction<string>) => {
|
setSelectedBucket: (state, action: PayloadAction<string>) => {
|
||||||
state.selectedBucket = action.payload;
|
state.selectedBucket = action.payload;
|
||||||
},
|
},
|
||||||
|
setLongFileOpen: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.longFileOpen = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const {
|
export const {
|
||||||
@@ -401,6 +405,7 @@ export const {
|
|||||||
setIsOpeningOD,
|
setIsOpeningOD,
|
||||||
setRetentionConfig,
|
setRetentionConfig,
|
||||||
setSelectedBucket,
|
setSelectedBucket,
|
||||||
|
setLongFileOpen,
|
||||||
} = objectBrowserSlice.actions;
|
} = objectBrowserSlice.actions;
|
||||||
|
|
||||||
export default objectBrowserSlice.reducer;
|
export default objectBrowserSlice.reducer;
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export interface ObjectBrowserState {
|
|||||||
shareFileModalOpen: boolean;
|
shareFileModalOpen: boolean;
|
||||||
isOpeningObjectDetail: boolean;
|
isOpeningObjectDetail: boolean;
|
||||||
retentionConfig: IRetentionConfig | null;
|
retentionConfig: IRetentionConfig | null;
|
||||||
|
longFileOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ObjectManager {
|
export interface ObjectManager {
|
||||||
|
|||||||
90
portal-ui/src/screens/Console/ObjectBrowser/utils.ts
Normal file
90
portal-ui/src/screens/Console/ObjectBrowser/utils.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2023 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 { IFileInfo } from "../Buckets/ListBuckets/Objects/ObjectDetails/types";
|
||||||
|
import { encodeURLString, getClientOS } from "../../../common/utils";
|
||||||
|
import { makeid, storeCallForObjectWithID } from "./transferManager";
|
||||||
|
import { download } from "../Buckets/ListBuckets/Objects/utils";
|
||||||
|
import {
|
||||||
|
cancelObjectInList,
|
||||||
|
completeObject,
|
||||||
|
failObject,
|
||||||
|
setLongFileOpen,
|
||||||
|
setNewObject,
|
||||||
|
updateProgress,
|
||||||
|
} from "./objectBrowserSlice";
|
||||||
|
import { AppDispatch } from "../../../store";
|
||||||
|
|
||||||
|
export const downloadObject = (
|
||||||
|
dispatch: AppDispatch,
|
||||||
|
bucketName: string,
|
||||||
|
internalPaths: string,
|
||||||
|
object: IFileInfo
|
||||||
|
) => {
|
||||||
|
const identityDownload = encodeURLString(
|
||||||
|
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (object.name.length > 200 && getClientOS().toLowerCase().includes("win")) {
|
||||||
|
dispatch(setLongFileOpen(true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ID = makeid(8);
|
||||||
|
|
||||||
|
const downloadCall = download(
|
||||||
|
bucketName,
|
||||||
|
internalPaths,
|
||||||
|
object.version_id,
|
||||||
|
parseInt(object.size || "0"),
|
||||||
|
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: "",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -51,6 +51,7 @@ export const consoleSlice = createSlice({
|
|||||||
|
|
||||||
export const { saveSessionResponse, resetSession } = consoleSlice.actions;
|
export const { saveSessionResponse, resetSession } = consoleSlice.actions;
|
||||||
export const selSession = (state: AppState) => state.console.session;
|
export const selSession = (state: AppState) => state.console.session;
|
||||||
export const selFeatures = (state: AppState) => state.console.session.features;
|
export const selFeatures = (state: AppState) =>
|
||||||
|
state.console.session ? state.console.session.features : [];
|
||||||
|
|
||||||
export default consoleSlice.reducer;
|
export default consoleSlice.reducer;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface SystemState {
|
|||||||
siteReplicationInfo: SRInfoStateType;
|
siteReplicationInfo: SRInfoStateType;
|
||||||
licenseInfo: null | SubnetInfo;
|
licenseInfo: null | SubnetInfo;
|
||||||
overrideStyles: null | IEmbeddedCustomStyles;
|
overrideStyles: null | IEmbeddedCustomStyles;
|
||||||
|
anonymousMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SystemState = {
|
const initialState: SystemState = {
|
||||||
@@ -72,6 +73,7 @@ const initialState: SystemState = {
|
|||||||
distributedSetup: false,
|
distributedSetup: false,
|
||||||
licenseInfo: null,
|
licenseInfo: null,
|
||||||
overrideStyles: null,
|
overrideStyles: null,
|
||||||
|
anonymousMode: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const systemSlice = createSlice({
|
export const systemSlice = createSlice({
|
||||||
@@ -159,6 +161,13 @@ export const systemSlice = createSlice({
|
|||||||
) => {
|
) => {
|
||||||
state.overrideStyles = action.payload;
|
state.overrideStyles = action.payload;
|
||||||
},
|
},
|
||||||
|
setAnonymousMode: (state) => {
|
||||||
|
state.anonymousMode = true;
|
||||||
|
state.loggedIn = true;
|
||||||
|
},
|
||||||
|
resetSystem: () => {
|
||||||
|
return initialState;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,6 +190,8 @@ export const {
|
|||||||
setSiteReplicationInfo,
|
setSiteReplicationInfo,
|
||||||
setLicenseInfo,
|
setLicenseInfo,
|
||||||
setOverrideStyles,
|
setOverrideStyles,
|
||||||
|
setAnonymousMode,
|
||||||
|
resetSystem,
|
||||||
} = systemSlice.actions;
|
} = systemSlice.actions;
|
||||||
|
|
||||||
export const selDistSet = (state: AppState) => state.system.distributedSetup;
|
export const selDistSet = (state: AppState) => state.system.distributedSetup;
|
||||||
|
|||||||
@@ -369,6 +369,9 @@ func NewConsoleCredentials(accessKey, secretKey, location string) (*credentials.
|
|||||||
// getConsoleCredentialsFromSession returns the *consoleCredentials.Login associated to the
|
// getConsoleCredentialsFromSession returns the *consoleCredentials.Login associated to the
|
||||||
// provided session token, this is useful for running the Expire() or IsExpired() operations
|
// provided session token, this is useful for running the Expire() or IsExpired() operations
|
||||||
func getConsoleCredentialsFromSession(claims *models.Principal) *credentials.Credentials {
|
func getConsoleCredentialsFromSession(claims *models.Principal) *credentials.Credentials {
|
||||||
|
if claims == nil {
|
||||||
|
return credentials.NewStaticV4("", "", "")
|
||||||
|
}
|
||||||
return credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken)
|
return credentials.NewStaticV4(claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
|
|||||||
api.KeyAuth = func(token string, scopes []string) (*models.Principal, error) {
|
api.KeyAuth = func(token string, scopes []string) (*models.Principal, error) {
|
||||||
// we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt
|
// we are validating the session token by decrypting the claims inside, if the operation succeed that means the jwt
|
||||||
// was generated and signed by us in the first place
|
// was generated and signed by us in the first place
|
||||||
|
if token == "Anonymous" {
|
||||||
|
return &models.Principal{}, nil
|
||||||
|
}
|
||||||
claims, err := auth.ParseClaimsFromToken(token)
|
claims, err := auth.ParseClaimsFromToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.Logger("Unable to validate the session token %s: %v", token, err)
|
api.Logger("Unable to validate the session token %s: %v", token, err)
|
||||||
@@ -98,6 +101,9 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
|
|||||||
CustomStyleOb: claims.CustomStyleOB,
|
CustomStyleOb: claims.CustomStyleOB,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
api.AnonymousAuth = func(s string) (*models.Principal, error) {
|
||||||
|
return &models.Principal{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Register login handlers
|
// Register login handlers
|
||||||
registerLoginHandlers(api)
|
registerLoginHandlers(api)
|
||||||
@@ -291,6 +297,8 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
|
|||||||
// handle it appropriately.
|
// handle it appropriately.
|
||||||
if len(sessionToken) > 0 {
|
if len(sessionToken) > 0 {
|
||||||
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", string(sessionToken)))
|
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", string(sessionToken)))
|
||||||
|
} else {
|
||||||
|
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "Anonymous"))
|
||||||
}
|
}
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, _ := auth.ParseClaimsFromToken(string(sessionToken))
|
claims, _ := auth.ParseClaimsFromToken(string(sessionToken))
|
||||||
|
|||||||
@@ -1458,6 +1458,14 @@ func init() {
|
|||||||
},
|
},
|
||||||
"/buckets/{bucket_name}/objects": {
|
"/buckets/{bucket_name}/objects": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Object"
|
"Object"
|
||||||
],
|
],
|
||||||
@@ -1489,6 +1497,12 @@ func init() {
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"name": "with_metadata",
|
"name": "with_metadata",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -1566,6 +1580,14 @@ func init() {
|
|||||||
},
|
},
|
||||||
"/buckets/{bucket_name}/objects/download": {
|
"/buckets/{bucket_name}/objects/download": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
],
|
],
|
||||||
@@ -1930,6 +1952,14 @@ func init() {
|
|||||||
},
|
},
|
||||||
"/buckets/{bucket_name}/objects/upload": {
|
"/buckets/{bucket_name}/objects/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"multipart/form-data"
|
"multipart/form-data"
|
||||||
],
|
],
|
||||||
@@ -8469,6 +8499,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
"anonymous": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"name": "X-Anonymous",
|
||||||
|
"in": "header"
|
||||||
|
},
|
||||||
"key": {
|
"key": {
|
||||||
"type": "oauth2",
|
"type": "oauth2",
|
||||||
"flow": "accessCode",
|
"flow": "accessCode",
|
||||||
@@ -9906,6 +9941,14 @@ func init() {
|
|||||||
},
|
},
|
||||||
"/buckets/{bucket_name}/objects": {
|
"/buckets/{bucket_name}/objects": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Object"
|
"Object"
|
||||||
],
|
],
|
||||||
@@ -9937,6 +9980,12 @@ func init() {
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"name": "with_metadata",
|
"name": "with_metadata",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -10014,6 +10063,14 @@ func init() {
|
|||||||
},
|
},
|
||||||
"/buckets/{bucket_name}/objects/download": {
|
"/buckets/{bucket_name}/objects/download": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
],
|
],
|
||||||
@@ -10378,6 +10435,14 @@ func init() {
|
|||||||
},
|
},
|
||||||
"/buckets/{bucket_name}/objects/upload": {
|
"/buckets/{bucket_name}/objects/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"multipart/form-data"
|
"multipart/form-data"
|
||||||
],
|
],
|
||||||
@@ -17043,6 +17108,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
"anonymous": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"name": "X-Anonymous",
|
||||||
|
"in": "header"
|
||||||
|
},
|
||||||
"key": {
|
"key": {
|
||||||
"type": "oauth2",
|
"type": "oauth2",
|
||||||
"flow": "accessCode",
|
"flow": "accessCode",
|
||||||
|
|||||||
@@ -534,6 +534,10 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
|
|||||||
return middleware.NotImplemented("operation user.UpdateUserInfo has not yet been implemented")
|
return middleware.NotImplemented("operation user.UpdateUserInfo has not yet been implemented")
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Applies when the "X-Anonymous" header is set
|
||||||
|
AnonymousAuth: func(token string) (*models.Principal, error) {
|
||||||
|
return nil, errors.NotImplemented("api key auth (anonymous) X-Anonymous from header param [X-Anonymous] has not yet been implemented")
|
||||||
|
},
|
||||||
KeyAuth: func(token string, scopes []string) (*models.Principal, error) {
|
KeyAuth: func(token string, scopes []string) (*models.Principal, error) {
|
||||||
return nil, errors.NotImplemented("oauth2 bearer auth (key) has not yet been implemented")
|
return nil, errors.NotImplemented("oauth2 bearer auth (key) has not yet been implemented")
|
||||||
},
|
},
|
||||||
@@ -584,6 +588,10 @@ type ConsoleAPI struct {
|
|||||||
// - application/json
|
// - application/json
|
||||||
JSONProducer runtime.Producer
|
JSONProducer runtime.Producer
|
||||||
|
|
||||||
|
// AnonymousAuth registers a function that takes a token and returns a principal
|
||||||
|
// it performs authentication based on an api key X-Anonymous provided in the header
|
||||||
|
AnonymousAuth func(string) (*models.Principal, error)
|
||||||
|
|
||||||
// KeyAuth registers a function that takes an access token and a collection of required scopes and returns a principal
|
// KeyAuth registers a function that takes an access token and a collection of required scopes and returns a principal
|
||||||
// it performs authentication based on an oauth2 bearer token provided in the request
|
// it performs authentication based on an oauth2 bearer token provided in the request
|
||||||
KeyAuth func(string, []string) (*models.Principal, error)
|
KeyAuth func(string, []string) (*models.Principal, error)
|
||||||
@@ -975,6 +983,9 @@ func (o *ConsoleAPI) Validate() error {
|
|||||||
unregistered = append(unregistered, "JSONProducer")
|
unregistered = append(unregistered, "JSONProducer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.AnonymousAuth == nil {
|
||||||
|
unregistered = append(unregistered, "XAnonymousAuth")
|
||||||
|
}
|
||||||
if o.KeyAuth == nil {
|
if o.KeyAuth == nil {
|
||||||
unregistered = append(unregistered, "KeyAuth")
|
unregistered = append(unregistered, "KeyAuth")
|
||||||
}
|
}
|
||||||
@@ -1444,6 +1455,12 @@ func (o *ConsoleAPI) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) m
|
|||||||
result := make(map[string]runtime.Authenticator)
|
result := make(map[string]runtime.Authenticator)
|
||||||
for name := range schemes {
|
for name := range schemes {
|
||||||
switch name {
|
switch name {
|
||||||
|
case "anonymous":
|
||||||
|
scheme := schemes[name]
|
||||||
|
result[name] = o.APIKeyAuthenticator(scheme.Name, scheme.In, func(token string) (interface{}, error) {
|
||||||
|
return o.AnonymousAuth(token)
|
||||||
|
})
|
||||||
|
|
||||||
case "key":
|
case "key":
|
||||||
result[name] = o.BearerAuthenticator(name, func(token string, scopes []string) (interface{}, error) {
|
result[name] = o.BearerAuthenticator(name, func(token string, scopes []string) (interface{}, error) {
|
||||||
return o.KeyAuth(token, scopes)
|
return o.KeyAuth(token, scopes)
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ type ListObjectsParams struct {
|
|||||||
/*
|
/*
|
||||||
In: query
|
In: query
|
||||||
*/
|
*/
|
||||||
|
Limit *int32
|
||||||
|
/*
|
||||||
|
In: query
|
||||||
|
*/
|
||||||
Prefix *string
|
Prefix *string
|
||||||
/*
|
/*
|
||||||
In: query
|
In: query
|
||||||
@@ -88,6 +92,11 @@ func (o *ListObjectsParams) BindRequest(r *http.Request, route *middleware.Match
|
|||||||
res = append(res, err)
|
res = append(res, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qLimit, qhkLimit, _ := qs.GetOK("limit")
|
||||||
|
if err := o.bindLimit(qLimit, qhkLimit, route.Formats); err != nil {
|
||||||
|
res = append(res, err)
|
||||||
|
}
|
||||||
|
|
||||||
qPrefix, qhkPrefix, _ := qs.GetOK("prefix")
|
qPrefix, qhkPrefix, _ := qs.GetOK("prefix")
|
||||||
if err := o.bindPrefix(qPrefix, qhkPrefix, route.Formats); err != nil {
|
if err := o.bindPrefix(qPrefix, qhkPrefix, route.Formats); err != nil {
|
||||||
res = append(res, err)
|
res = append(res, err)
|
||||||
@@ -127,6 +136,29 @@ func (o *ListObjectsParams) bindBucketName(rawData []string, hasKey bool, format
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bindLimit binds and validates parameter Limit from query.
|
||||||
|
func (o *ListObjectsParams) bindLimit(rawData []string, hasKey bool, formats strfmt.Registry) error {
|
||||||
|
var raw string
|
||||||
|
if len(rawData) > 0 {
|
||||||
|
raw = rawData[len(rawData)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required: false
|
||||||
|
// AllowEmptyValue: false
|
||||||
|
|
||||||
|
if raw == "" { // empty values pass all other validations
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := swag.ConvertInt32(raw)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InvalidType("limit", "query", "int32", raw)
|
||||||
|
}
|
||||||
|
o.Limit = &value
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// bindPrefix binds and validates parameter Prefix from query.
|
// bindPrefix binds and validates parameter Prefix from query.
|
||||||
func (o *ListObjectsParams) bindPrefix(rawData []string, hasKey bool, formats strfmt.Registry) error {
|
func (o *ListObjectsParams) bindPrefix(rawData []string, hasKey bool, formats strfmt.Registry) error {
|
||||||
var raw string
|
var raw string
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import (
|
|||||||
type ListObjectsURL struct {
|
type ListObjectsURL struct {
|
||||||
BucketName string
|
BucketName string
|
||||||
|
|
||||||
|
Limit *int32
|
||||||
Prefix *string
|
Prefix *string
|
||||||
Recursive *bool
|
Recursive *bool
|
||||||
WithMetadata *bool
|
WithMetadata *bool
|
||||||
@@ -81,6 +82,14 @@ func (o *ListObjectsURL) Build() (*url.URL, error) {
|
|||||||
|
|
||||||
qs := make(url.Values)
|
qs := make(url.Values)
|
||||||
|
|
||||||
|
var limitQ string
|
||||||
|
if o.Limit != nil {
|
||||||
|
limitQ = swag.FormatInt32(*o.Limit)
|
||||||
|
}
|
||||||
|
if limitQ != "" {
|
||||||
|
qs.Set("limit", limitQ)
|
||||||
|
}
|
||||||
|
|
||||||
var prefixQ string
|
var prefixQ string
|
||||||
if o.Prefix != nil {
|
if o.Prefix != nil {
|
||||||
prefixQ = *o.Prefix
|
prefixQ = *o.Prefix
|
||||||
|
|||||||
@@ -205,7 +205,16 @@ func getListObjectsResponse(session *models.Principal, params objectApi.ListObje
|
|||||||
// defining the client to be used
|
// defining the client to be used
|
||||||
minioClient := minioClient{client: mClient}
|
minioClient := minioClient{client: mClient}
|
||||||
|
|
||||||
objs, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, recursive, withVersions, withMetadata)
|
objs, err := listBucketObjects(ListObjectsOpts{
|
||||||
|
ctx: ctx,
|
||||||
|
client: minioClient,
|
||||||
|
bucketName: params.BucketName,
|
||||||
|
prefix: prefix,
|
||||||
|
recursive: recursive,
|
||||||
|
withVersions: withVersions,
|
||||||
|
withMetadata: withMetadata,
|
||||||
|
limit: params.Limit,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrorWithContext(ctx, err)
|
return nil, ErrorWithContext(ctx, err)
|
||||||
}
|
}
|
||||||
@@ -217,20 +226,35 @@ func getListObjectsResponse(session *models.Principal, params objectApi.ListObje
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListObjectsOpts struct {
|
||||||
|
ctx context.Context
|
||||||
|
client MinioClient
|
||||||
|
bucketName string
|
||||||
|
prefix string
|
||||||
|
recursive bool
|
||||||
|
withVersions bool
|
||||||
|
withMetadata bool
|
||||||
|
limit *int32
|
||||||
|
}
|
||||||
|
|
||||||
// listBucketObjects gets an array of objects in a bucket
|
// listBucketObjects gets an array of objects in a bucket
|
||||||
func listBucketObjects(ctx context.Context, client MinioClient, bucketName string, prefix string, recursive, withVersions bool, withMetadata bool) ([]*models.BucketObject, error) {
|
func listBucketObjects(listOpts ListObjectsOpts) ([]*models.BucketObject, error) {
|
||||||
var objects []*models.BucketObject
|
var objects []*models.BucketObject
|
||||||
opts := minio.ListObjectsOptions{
|
opts := minio.ListObjectsOptions{
|
||||||
Prefix: prefix,
|
Prefix: listOpts.prefix,
|
||||||
Recursive: recursive,
|
Recursive: listOpts.recursive,
|
||||||
WithVersions: withVersions,
|
WithVersions: listOpts.withVersions,
|
||||||
WithMetadata: withMetadata,
|
WithMetadata: listOpts.withMetadata,
|
||||||
MaxKeys: 100,
|
MaxKeys: 100,
|
||||||
}
|
}
|
||||||
if withMetadata {
|
if listOpts.withMetadata {
|
||||||
opts.MaxKeys = 1
|
opts.MaxKeys = 1
|
||||||
}
|
}
|
||||||
for lsObj := range client.listObjects(ctx, bucketName, opts) {
|
if listOpts.limit != nil {
|
||||||
|
opts.MaxKeys = int(*listOpts.limit)
|
||||||
|
}
|
||||||
|
var totalObjs int32
|
||||||
|
for lsObj := range listOpts.client.listObjects(listOpts.ctx, listOpts.bucketName, opts) {
|
||||||
if lsObj.Err != nil {
|
if lsObj.Err != nil {
|
||||||
return nil, lsObj.Err
|
return nil, lsObj.Err
|
||||||
}
|
}
|
||||||
@@ -248,37 +272,44 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin
|
|||||||
Etag: lsObj.ETag,
|
Etag: lsObj.ETag,
|
||||||
}
|
}
|
||||||
// only if single object with or without versions; get legalhold, retention and tags
|
// only if single object with or without versions; get legalhold, retention and tags
|
||||||
if !lsObj.IsDeleteMarker && prefix != "" && !strings.HasSuffix(prefix, "/") {
|
if !lsObj.IsDeleteMarker && listOpts.prefix != "" && !strings.HasSuffix(listOpts.prefix, "/") {
|
||||||
// Add Legal Hold Status if available
|
// Add Legal Hold Status if available
|
||||||
legalHoldStatus, err := client.getObjectLegalHold(ctx, bucketName, lsObj.Key, minio.GetObjectLegalHoldOptions{VersionID: lsObj.VersionID})
|
legalHoldStatus, err := listOpts.client.getObjectLegalHold(listOpts.ctx, listOpts.bucketName, lsObj.Key, minio.GetObjectLegalHoldOptions{VersionID: lsObj.VersionID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
|
errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
|
||||||
if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" {
|
if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" {
|
||||||
ErrorWithContext(ctx, fmt.Errorf("error getting legal hold status for %s : %v", lsObj.VersionID, err))
|
ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting legal hold status for %s : %v", lsObj.VersionID, err))
|
||||||
}
|
}
|
||||||
} else if legalHoldStatus != nil {
|
} else if legalHoldStatus != nil {
|
||||||
obj.LegalHoldStatus = string(*legalHoldStatus)
|
obj.LegalHoldStatus = string(*legalHoldStatus)
|
||||||
}
|
}
|
||||||
// Add Retention Status if available
|
// Add Retention Status if available
|
||||||
retention, retUntilDate, err := client.getObjectRetention(ctx, bucketName, lsObj.Key, lsObj.VersionID)
|
retention, retUntilDate, err := listOpts.client.getObjectRetention(listOpts.ctx, listOpts.bucketName, lsObj.Key, lsObj.VersionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
|
errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
|
||||||
if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" {
|
if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" {
|
||||||
ErrorWithContext(ctx, fmt.Errorf("error getting retention status for %s : %v", lsObj.VersionID, err))
|
ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting retention status for %s : %v", lsObj.VersionID, err))
|
||||||
}
|
}
|
||||||
} else if retention != nil && retUntilDate != nil {
|
} else if retention != nil && retUntilDate != nil {
|
||||||
date := *retUntilDate
|
date := *retUntilDate
|
||||||
obj.RetentionMode = string(*retention)
|
obj.RetentionMode = string(*retention)
|
||||||
obj.RetentionUntilDate = date.Format(time.RFC3339)
|
obj.RetentionUntilDate = date.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
tags, err := client.getObjectTagging(ctx, bucketName, lsObj.Key, minio.GetObjectTaggingOptions{VersionID: lsObj.VersionID})
|
objTags, err := listOpts.client.getObjectTagging(listOpts.ctx, listOpts.bucketName, lsObj.Key, minio.GetObjectTaggingOptions{VersionID: lsObj.VersionID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorWithContext(ctx, fmt.Errorf("error getting object tags for %s : %v", lsObj.VersionID, err))
|
ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting object tags for %s : %v", lsObj.VersionID, err))
|
||||||
} else {
|
} else {
|
||||||
obj.Tags = tags.ToMap()
|
obj.Tags = objTags.ToMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
objects = append(objects, obj)
|
objects = append(objects, obj)
|
||||||
|
totalObjs++
|
||||||
|
|
||||||
|
if listOpts.limit != nil {
|
||||||
|
if totalObjs >= *listOpts.limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
@@ -499,7 +530,15 @@ func getDownloadFolderResponse(session *models.Principal, params objectApi.Downl
|
|||||||
return nil, ErrorWithContext(ctx, err)
|
return nil, ErrorWithContext(ctx, err)
|
||||||
}
|
}
|
||||||
minioClient := minioClient{client: mClient}
|
minioClient := minioClient{client: mClient}
|
||||||
objects, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, true, false, false)
|
objects, err := listBucketObjects(ListObjectsOpts{
|
||||||
|
ctx: ctx,
|
||||||
|
client: minioClient,
|
||||||
|
bucketName: params.BucketName,
|
||||||
|
prefix: prefix,
|
||||||
|
recursive: true,
|
||||||
|
withVersions: false,
|
||||||
|
withMetadata: false,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrorWithContext(ctx, err)
|
return nil, ErrorWithContext(ctx, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ func Test_listObjects(t *testing.T) {
|
|||||||
recursive bool
|
recursive bool
|
||||||
withVersions bool
|
withVersions bool
|
||||||
withMetadata bool
|
withMetadata bool
|
||||||
|
limit *int32
|
||||||
listFunc func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
|
listFunc func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
|
||||||
objectLegalHoldFunc func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error)
|
objectLegalHoldFunc func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error)
|
||||||
objectRetentionFunc func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error)
|
objectRetentionFunc func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error)
|
||||||
@@ -553,6 +554,150 @@ func Test_listObjects(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantError: nil,
|
wantError: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: "Return objects",
|
||||||
|
args: args{
|
||||||
|
bucketName: "bucket1",
|
||||||
|
prefix: "prefix",
|
||||||
|
recursive: true,
|
||||||
|
withVersions: false,
|
||||||
|
withMetadata: false,
|
||||||
|
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
|
||||||
|
objectStatCh := make(chan minio.ObjectInfo, 1)
|
||||||
|
go func(objectStatCh chan<- minio.ObjectInfo) {
|
||||||
|
defer close(objectStatCh)
|
||||||
|
for _, bucket := range []minio.ObjectInfo{
|
||||||
|
{
|
||||||
|
Key: "obj1",
|
||||||
|
LastModified: t1,
|
||||||
|
Size: int64(1024),
|
||||||
|
ContentType: "content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "obj2",
|
||||||
|
LastModified: t1,
|
||||||
|
Size: int64(512),
|
||||||
|
ContentType: "content",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
objectStatCh <- bucket
|
||||||
|
}
|
||||||
|
}(objectStatCh)
|
||||||
|
return objectStatCh
|
||||||
|
},
|
||||||
|
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
|
||||||
|
s := minio.LegalHoldEnabled
|
||||||
|
return &s, nil
|
||||||
|
},
|
||||||
|
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
|
||||||
|
m := minio.Governance
|
||||||
|
return &m, &tretention, nil
|
||||||
|
},
|
||||||
|
objectGetTaggingFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error) {
|
||||||
|
tagMap := map[string]string{
|
||||||
|
"tag1": "value1",
|
||||||
|
}
|
||||||
|
otags, err := tags.MapToObjectTags(tagMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return otags, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedResp: []*models.BucketObject{
|
||||||
|
{
|
||||||
|
Name: "obj1",
|
||||||
|
LastModified: t1.Format(time.RFC3339),
|
||||||
|
Size: int64(1024),
|
||||||
|
ContentType: "content",
|
||||||
|
LegalHoldStatus: string(minio.LegalHoldEnabled),
|
||||||
|
RetentionMode: string(minio.Governance),
|
||||||
|
RetentionUntilDate: tretention.Format(time.RFC3339),
|
||||||
|
Tags: map[string]string{
|
||||||
|
"tag1": "value1",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Name: "obj2",
|
||||||
|
LastModified: t1.Format(time.RFC3339),
|
||||||
|
Size: int64(512),
|
||||||
|
ContentType: "content",
|
||||||
|
LegalHoldStatus: string(minio.LegalHoldEnabled),
|
||||||
|
RetentionMode: string(minio.Governance),
|
||||||
|
RetentionUntilDate: tretention.Format(time.RFC3339),
|
||||||
|
Tags: map[string]string{
|
||||||
|
"tag1": "value1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: "Limit 1",
|
||||||
|
args: args{
|
||||||
|
bucketName: "bucket1",
|
||||||
|
prefix: "prefix",
|
||||||
|
recursive: true,
|
||||||
|
withVersions: false,
|
||||||
|
withMetadata: false,
|
||||||
|
limit: swag.Int32(1),
|
||||||
|
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
|
||||||
|
objectStatCh := make(chan minio.ObjectInfo, 1)
|
||||||
|
go func(objectStatCh chan<- minio.ObjectInfo) {
|
||||||
|
defer close(objectStatCh)
|
||||||
|
for _, bucket := range []minio.ObjectInfo{
|
||||||
|
{
|
||||||
|
Key: "obj1",
|
||||||
|
LastModified: t1,
|
||||||
|
Size: int64(1024),
|
||||||
|
ContentType: "content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "obj2",
|
||||||
|
LastModified: t1,
|
||||||
|
Size: int64(512),
|
||||||
|
ContentType: "content",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
objectStatCh <- bucket
|
||||||
|
}
|
||||||
|
}(objectStatCh)
|
||||||
|
return objectStatCh
|
||||||
|
},
|
||||||
|
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
|
||||||
|
s := minio.LegalHoldEnabled
|
||||||
|
return &s, nil
|
||||||
|
},
|
||||||
|
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
|
||||||
|
m := minio.Governance
|
||||||
|
return &m, &tretention, nil
|
||||||
|
},
|
||||||
|
objectGetTaggingFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectTaggingOptions) (*tags.Tags, error) {
|
||||||
|
tagMap := map[string]string{
|
||||||
|
"tag1": "value1",
|
||||||
|
}
|
||||||
|
otags, err := tags.MapToObjectTags(tagMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return otags, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedResp: []*models.BucketObject{
|
||||||
|
{
|
||||||
|
Name: "obj1",
|
||||||
|
LastModified: t1.Format(time.RFC3339),
|
||||||
|
Size: int64(1024),
|
||||||
|
ContentType: "content",
|
||||||
|
LegalHoldStatus: string(minio.LegalHoldEnabled),
|
||||||
|
RetentionMode: string(minio.Governance),
|
||||||
|
RetentionUntilDate: tretention.Format(time.RFC3339),
|
||||||
|
Tags: map[string]string{
|
||||||
|
"tag1": "value1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantError: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -563,7 +708,16 @@ func Test_listObjects(t *testing.T) {
|
|||||||
minioGetObjectLegalHoldMock = tt.args.objectLegalHoldFunc
|
minioGetObjectLegalHoldMock = tt.args.objectLegalHoldFunc
|
||||||
minioGetObjectRetentionMock = tt.args.objectRetentionFunc
|
minioGetObjectRetentionMock = tt.args.objectRetentionFunc
|
||||||
minioGetObjectTaggingMock = tt.args.objectGetTaggingFunc
|
minioGetObjectTaggingMock = tt.args.objectGetTaggingFunc
|
||||||
resp, err := listBucketObjects(ctx, minClient, tt.args.bucketName, tt.args.prefix, tt.args.recursive, tt.args.withVersions, tt.args.withMetadata)
|
resp, err := listBucketObjects(ListObjectsOpts{
|
||||||
|
ctx: ctx,
|
||||||
|
client: minClient,
|
||||||
|
bucketName: tt.args.bucketName,
|
||||||
|
prefix: tt.args.prefix,
|
||||||
|
recursive: tt.args.recursive,
|
||||||
|
withVersions: tt.args.withVersions,
|
||||||
|
withMetadata: tt.args.withMetadata,
|
||||||
|
limit: tt.args.limit,
|
||||||
|
})
|
||||||
switch {
|
switch {
|
||||||
case err == nil && tt.wantError != nil:
|
case err == nil && tt.wantError != nil:
|
||||||
t.Errorf("listBucketObjects() error: %v, wantErr: %v", err, tt.wantError)
|
t.Errorf("listBucketObjects() error: %v, wantErr: %v", err, tt.wantError)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ package restapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -30,7 +30,7 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/console/pkg/utils"
|
"github.com/minio/console/pkg/utils"
|
||||||
|
|
||||||
"github.com/go-openapi/errors"
|
errorsApi "github.com/go-openapi/errors"
|
||||||
"github.com/minio/console/models"
|
"github.com/minio/console/models"
|
||||||
"github.com/minio/console/pkg/auth"
|
"github.com/minio/console/pkg/auth"
|
||||||
"github.com/minio/websocket"
|
"github.com/minio/websocket"
|
||||||
@@ -139,15 +139,15 @@ func (c wsConn) readMessage() (messageType int, p []byte, err error) {
|
|||||||
// Request should come like ws://<host>:<port>/ws/<api>
|
// Request should come like ws://<host>:<port>/ws/<api>
|
||||||
func serveWS(w http.ResponseWriter, req *http.Request) {
|
func serveWS(w http.ResponseWriter, req *http.Request) {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
|
wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
|
||||||
// Perform authentication before upgrading to a Websocket Connection
|
// Perform authentication before upgrading to a Websocket Connection
|
||||||
// authenticate WS connection with Console
|
// authenticate WS connection with Console
|
||||||
session, err := auth.GetClaimsFromTokenInRequest(req)
|
session, err := auth.GetClaimsFromTokenInRequest(req)
|
||||||
if err != nil {
|
if err != nil && (errors.Is(err, auth.ErrReadingToken) && !strings.HasPrefix(wsPath, `/objectManager`)) {
|
||||||
ErrorWithContext(ctx, err)
|
ErrorWithContext(ctx, err)
|
||||||
errors.ServeError(w, req, errors.New(http.StatusUnauthorized, err.Error()))
|
errorsApi.ServeError(w, req, errorsApi.New(http.StatusUnauthorized, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development mode validation
|
// Development mode validation
|
||||||
if getConsoleDevMode() {
|
if getConsoleDevMode() {
|
||||||
upgrader.CheckOrigin = func(r *http.Request) bool {
|
upgrader.CheckOrigin = func(r *http.Request) bool {
|
||||||
@@ -159,11 +159,10 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
|
|||||||
conn, err := upgrader.Upgrade(w, req, nil)
|
conn, err := upgrader.Upgrade(w, req, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ErrorWithContext(ctx, err)
|
ErrorWithContext(ctx, err)
|
||||||
errors.ServeError(w, req, err)
|
errorsApi.ServeError(w, req, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(wsPath, `/trace`):
|
case strings.HasPrefix(wsPath, `/trace`):
|
||||||
wsAdminClient, err := newWebSocketAdminClient(conn, session)
|
wsAdminClient, err := newWebSocketAdminClient(conn, session)
|
||||||
@@ -539,227 +538,6 @@ func (wsc *wsAdminClient) profile(ctx context.Context, opts *profileOptions) {
|
|||||||
sendWsCloseMessage(wsc.conn, err)
|
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() {
|
|
||||||
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(),
|
|
||||||
Prefix: messageRequest.Prefix,
|
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
|
||||||
Prefix: messageRequest.Prefix,
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// sendWsCloseMessage sends Websocket Connection Close Message indicating the Status Code
|
||||||
// see https://tools.ietf.org/html/rfc6455#page-45
|
// see https://tools.ietf.org/html/rfc6455#page-45
|
||||||
func sendWsCloseMessage(conn WSConn, err error) {
|
func sendWsCloseMessage(conn WSConn, err error) {
|
||||||
|
|||||||
248
restapi/ws_objects.go
Normal file
248
restapi/ws_objects.go
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
// This file is part of MinIO Console Server
|
||||||
|
// Copyright (c) 2023 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/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/console/models"
|
||||||
|
"github.com/minio/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
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() {
|
||||||
|
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(),
|
||||||
|
Prefix: messageRequest.Prefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
Prefix: messageRequest.Prefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ securityDefinitions:
|
|||||||
flow: accessCode
|
flow: accessCode
|
||||||
authorizationUrl: http://min.io
|
authorizationUrl: http://min.io
|
||||||
tokenUrl: http://min.io
|
tokenUrl: http://min.io
|
||||||
|
anonymous:
|
||||||
|
name: X-Anonymous
|
||||||
|
in: header
|
||||||
|
type: apiKey
|
||||||
# Apply the key security definition to all APIs
|
# Apply the key security definition to all APIs
|
||||||
security:
|
security:
|
||||||
- key: [ ]
|
- key: [ ]
|
||||||
@@ -290,6 +294,9 @@ paths:
|
|||||||
/buckets/{bucket_name}/objects:
|
/buckets/{bucket_name}/objects:
|
||||||
get:
|
get:
|
||||||
summary: List Objects
|
summary: List Objects
|
||||||
|
security:
|
||||||
|
- key: [ ]
|
||||||
|
- anonymous: [ ]
|
||||||
operationId: ListObjects
|
operationId: ListObjects
|
||||||
parameters:
|
parameters:
|
||||||
- name: bucket_name
|
- name: bucket_name
|
||||||
@@ -312,6 +319,11 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: A successful response.
|
description: A successful response.
|
||||||
@@ -402,6 +414,9 @@ paths:
|
|||||||
/buckets/{bucket_name}/objects/upload:
|
/buckets/{bucket_name}/objects/upload:
|
||||||
post:
|
post:
|
||||||
summary: Uploads an Object.
|
summary: Uploads an Object.
|
||||||
|
security:
|
||||||
|
- key: [ ]
|
||||||
|
- anonymous: [ ]
|
||||||
consumes:
|
consumes:
|
||||||
- multipart/form-data
|
- multipart/form-data
|
||||||
parameters:
|
parameters:
|
||||||
@@ -426,6 +441,9 @@ paths:
|
|||||||
get:
|
get:
|
||||||
summary: Download Object
|
summary: Download Object
|
||||||
operationId: Download Object
|
operationId: Download Object
|
||||||
|
security:
|
||||||
|
- key: [ ]
|
||||||
|
- anonymous: [ ]
|
||||||
produces:
|
produces:
|
||||||
- application/octet-stream
|
- application/octet-stream
|
||||||
parameters:
|
parameters:
|
||||||
@@ -5679,7 +5697,7 @@ definitions:
|
|||||||
latencyHistogram:
|
latencyHistogram:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/kmsLatencyHistogram"
|
$ref: "#/definitions/kmsLatencyHistogram"
|
||||||
uptime:
|
uptime:
|
||||||
type: integer
|
type: integer
|
||||||
cpus:
|
cpus:
|
||||||
|
|||||||
Reference in New Issue
Block a user