Compare commits

...

6 Commits

Author SHA1 Message Date
Alex
e3864b62a4 Release v1.6.1 (#3393) 2024-06-25 10:26:58 -07:00
Harshavardhana
22176f4e0f fix: objectManager implementation avoid racy goroutines (#3392)
fixes #3391
2024-06-25 08:50:31 -07:00
dependabot[bot]
a89d7ec0ea Bump ws from 7.5.9 to 7.5.10 in /web-app (#3390) 2024-06-18 15:22:19 -07:00
Ramon de Klein
8262049e20 Fix showing object-name in legal hold dialog (#3389) 2024-06-18 15:20:03 -07:00
Alex
c61e1e0a2a Added debounce to Share file fields (#3388)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2024-06-17 16:02:13 -06:00
Alex
b376cf6c65 Improvements to Share Link component behavior (#3387)
Improvements to Share Link component to avoid multiple re-renders during common actions

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2024-06-17 11:40:27 -06:00
16 changed files with 179 additions and 187 deletions

View File

@@ -50,11 +50,4 @@ jobs:
working-directory: ./web-app
continue-on-error: false
run: |
# Ignore "pdfjs-dist" advisory, because it's a dependency
# of "react-pdf" that cannot be upgraded. Because the
# "isEvalSupported" value is always set to "false", it
# isn't a security problem. See also
# - https://github.com/wojtekmaj/react-pdf/issues/1789
# - https://github.com/wojtekmaj/react-pdf/discussions/1786
# - https://www.npmjs.com/advisories/1097244
yarn npm audit --recursive --environment production --no-deprecations

View File

@@ -2,6 +2,18 @@
# Changelog
## Release v1.6.1
Bug Fix:
- Fixed objectManager issues under certain conditions
- Fixed Security vulnerability in dependencies
Additional Changes:
- Improved Share Link behavior
## Release v1.6.0
Bug Fix:

View File

@@ -35,9 +35,11 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
defer func() {
// We close socket at the end of requests
wsc.conn.close()
cancelContexts.Range(func(_, value interface{}) bool {
cancelContexts.Range(func(key, value interface{}) bool {
cancelFunc := value.(context.CancelFunc)
cancelFunc()
cancelContexts.Delete(key)
return true
})
}()
@@ -55,6 +57,7 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
// Read goroutine
go func() {
defer close(writeChannel)
for {
select {
case <-done:
@@ -72,10 +75,9 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
// We get request data & review information
var messageRequest ObjectsRequest
err := json.Unmarshal(message, &messageRequest)
if err != nil {
LogInfo("Error on message request unmarshal")
return
if err := json.Unmarshal(message, &messageRequest); err != nil {
LogInfo("Error on message request unmarshal", err)
continue
}
// new message, new context
@@ -84,6 +86,21 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
// We store the cancel func associated with this request
cancelContexts.Store(messageRequest.RequestID, cancel)
switch messageRequest.Mode {
case "objects", "rewind":
// cancel all previous open objects requests for listing
cancelContexts.Range(func(key, value interface{}) bool {
rid := key.(int64)
if rid < messageRequest.RequestID {
cancelFunc := value.(context.CancelFunc)
cancelFunc()
cancelContexts.Delete(key)
}
return true
})
}
const itemsPerBatch = 1000
switch messageRequest.Mode {
case "close":
@@ -95,173 +112,151 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
cancelContexts.Delete(messageRequest.RequestID)
}
case "objects":
// cancel all previous open objects requests for listing
cancelContexts.Range(func(key, value interface{}) bool {
rid := key.(int64)
if rid < messageRequest.RequestID {
cancelFunc := value.(context.CancelFunc)
cancelFunc()
}
return true
})
// 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()))
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
if err != nil {
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
})
return
}
var buffer []ObjectResponse
for lsObj := range startObjectsListing(ctx, wsc.client, objectRqConfigs) {
if lsObj.Err != nil {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, err),
Error: ErrorWithContext(ctx, lsObj.Err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
})
return
continue
}
var buffer []ObjectResponse
for lsObj := range startObjectsListing(ctx, wsc.client, objectRqConfigs) {
if _, ok := cancelContexts.Load(messageRequest.RequestID); !ok {
return
}
if lsObj.Err != nil {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, lsObj.Err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
})
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 {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
})
buffer = nil
}
objItem := ObjectResponse{
Name: lsObj.Key,
Size: lsObj.Size,
LastModified: lsObj.LastModified.Format(time.RFC3339),
VersionID: lsObj.VersionID,
IsLatest: lsObj.IsLatest,
DeleteMarker: lsObj.IsDeleteMarker,
}
if len(buffer) > 0 {
buffer = append(buffer, objItem)
if len(buffer) >= itemsPerBatch {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
})
buffer = nil
}
}
if len(buffer) > 0 {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
RequestEnd: true,
RequestID: messageRequest.RequestID,
Data: buffer,
})
}
// remove the cancellation context
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
RequestEnd: true,
})
// if we have that request id, cancel it
if cancelFunc, ok := cancelContexts.Load(messageRequest.RequestID); ok {
cancelFunc.(context.CancelFunc)()
cancelContexts.Delete(messageRequest.RequestID)
}()
}
case "rewind":
// cancel all previous open objects requests for listing
cancelContexts.Range(func(key, value interface{}) bool {
rid := key.(int64)
if rid < messageRequest.RequestID {
cancelFunc := value.(context.CancelFunc)
cancelFunc()
}
return true
})
// 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()))
objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
if err != nil {
LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
})
return
}
clientIP := wsc.conn.remoteAddress()
s3Client, err := newS3BucketClient(session, objectRqConfigs.BucketName, objectRqConfigs.Prefix, clientIP)
if err != nil {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
})
return
}
mcS3C := mcClient{client: s3Client}
var buffer []ObjectResponse
for lsObj := range startRewindListing(ctx, mcS3C, objectRqConfigs) {
if lsObj.Err != nil {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, err),
Error: ErrorWithContext(ctx, lsObj.Err.ToGoError()),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
})
return
continue
}
clientIP := wsc.conn.remoteAddress()
name := strings.Replace(lsObj.URL.Path, fmt.Sprintf("/%s/", objectRqConfigs.BucketName), "", 1)
s3Client, err := newS3BucketClient(session, objectRqConfigs.BucketName, objectRqConfigs.Prefix, clientIP)
if err != nil {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, err),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
})
cancel()
return
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)
mcS3C := mcClient{client: s3Client}
var buffer []ObjectResponse
for lsObj := range startRewindListing(ctx, mcS3C, objectRqConfigs) {
if lsObj.Err != nil {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Error: ErrorWithContext(ctx, lsObj.Err.ToGoError()),
Prefix: messageRequest.Prefix,
BucketName: messageRequest.BucketName,
})
continue
}
name := strings.Replace(lsObj.URL.Path, fmt.Sprintf("/%s/", objectRqConfigs.BucketName), "", 1)
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 {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
})
buffer = nil
}
}
if len(buffer) > 0 {
if len(buffer) >= itemsPerBatch {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
Data: buffer,
})
buffer = nil
}
}
if len(buffer) > 0 {
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
RequestEnd: true,
RequestID: messageRequest.RequestID,
Data: buffer,
})
}
// remove the cancellation context
sendWSResponse(WSResponse{
RequestID: messageRequest.RequestID,
RequestEnd: true,
})
// if we have that request id, cancel it
if cancelFunc, ok := cancelContexts.Load(messageRequest.RequestID); ok {
cancelFunc.(context.CancelFunc)()
cancelContexts.Delete(messageRequest.RequestID)
}()
}
}
}
}

View File

@@ -1,7 +1,7 @@
{
"files": {
"main.css": "./static/css/main.e60e4760.css",
"main.js": "./static/js/main.3bcbce18.js",
"main.js": "./static/js/main.ea59eadd.js",
"static/js/5301.2c626a41.chunk.js": "./static/js/5301.2c626a41.chunk.js",
"static/js/9361.ce3b326c.chunk.js": "./static/js/9361.ce3b326c.chunk.js",
"static/js/843.454ac75f.chunk.js": "./static/js/843.454ac75f.chunk.js",
@@ -40,7 +40,7 @@
"static/js/4103.926c44ef.chunk.js": "./static/js/4103.926c44ef.chunk.js",
"static/js/1702.851e407f.chunk.js": "./static/js/1702.851e407f.chunk.js",
"static/js/7601.4e033e78.chunk.js": "./static/js/7601.4e033e78.chunk.js",
"static/js/7945.24a46b62.chunk.js": "./static/js/7945.24a46b62.chunk.js",
"static/js/7945.996e45d2.chunk.js": "./static/js/7945.996e45d2.chunk.js",
"static/js/9619.a756233f.chunk.js": "./static/js/9619.a756233f.chunk.js",
"static/js/8017.d5b163f3.chunk.js": "./static/js/8017.d5b163f3.chunk.js",
"static/js/3323.f86a698b.chunk.js": "./static/js/3323.f86a698b.chunk.js",
@@ -119,7 +119,7 @@
"static/media/placeholderimage.png": "./static/media/placeholderimage.077ea48bd1ef1f4a883f.png",
"index.html": "./index.html",
"main.e60e4760.css.map": "./static/css/main.e60e4760.css.map",
"main.3bcbce18.js.map": "./static/js/main.3bcbce18.js.map",
"main.ea59eadd.js.map": "./static/js/main.ea59eadd.js.map",
"5301.2c626a41.chunk.js.map": "./static/js/5301.2c626a41.chunk.js.map",
"9361.ce3b326c.chunk.js.map": "./static/js/9361.ce3b326c.chunk.js.map",
"843.454ac75f.chunk.js.map": "./static/js/843.454ac75f.chunk.js.map",
@@ -158,7 +158,7 @@
"4103.926c44ef.chunk.js.map": "./static/js/4103.926c44ef.chunk.js.map",
"1702.851e407f.chunk.js.map": "./static/js/1702.851e407f.chunk.js.map",
"7601.4e033e78.chunk.js.map": "./static/js/7601.4e033e78.chunk.js.map",
"7945.24a46b62.chunk.js.map": "./static/js/7945.24a46b62.chunk.js.map",
"7945.996e45d2.chunk.js.map": "./static/js/7945.996e45d2.chunk.js.map",
"9619.a756233f.chunk.js.map": "./static/js/9619.a756233f.chunk.js.map",
"8017.d5b163f3.chunk.js.map": "./static/js/8017.d5b163f3.chunk.js.map",
"3323.f86a698b.chunk.js.map": "./static/js/3323.f86a698b.chunk.js.map",
@@ -217,6 +217,6 @@
},
"entrypoints": [
"static/css/main.e60e4760.css",
"static/js/main.3bcbce18.js"
"static/js/main.ea59eadd.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="/"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="#081C42" media="(prefers-color-scheme: light)" name="theme-color"/><meta content="#081C42" media="(prefers-color-scheme: dark)" name="theme-color"/><meta content="MinIO Console" name="description"/><meta name="minio-license" content="agpl"/><link href="./styles/root-styles.css" rel="stylesheet"/><link href="./apple-icon-180x180.png" rel="apple-touch-icon" sizes="180x180"/><link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/><link href="./favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/><link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/><link href="./manifest.json" rel="manifest"/><link color="#3a4e54" href="./safari-pinned-tab.svg" rel="mask-icon"/><title>MinIO Console</title><script defer="defer" src="./static/js/main.3bcbce18.js"></script><link href="./static/css/main.e60e4760.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="preload"><img src="./images/background.svg"/> <img src="./images/background-wave-orig2.svg"/></div><div id="loader-block"><img src="./Loader.svg"/></div></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="/"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="#081C42" media="(prefers-color-scheme: light)" name="theme-color"/><meta content="#081C42" media="(prefers-color-scheme: dark)" name="theme-color"/><meta content="MinIO Console" name="description"/><meta name="minio-license" content="agpl"/><link href="./styles/root-styles.css" rel="stylesheet"/><link href="./apple-icon-180x180.png" rel="apple-touch-icon" sizes="180x180"/><link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/><link href="./favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"/><link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/><link href="./manifest.json" rel="manifest"/><link color="#3a4e54" href="./safari-pinned-tab.svg" rel="mask-icon"/><title>MinIO Console</title><script defer="defer" src="./static/js/main.ea59eadd.js"></script><link href="./static/css/main.e60e4760.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="preload"><img src="./images/background.svg"/> <img src="./images/background-wave-orig2.svg"/></div><div id="loader-block"><img src="./Loader.svg"/></div></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -99,7 +99,7 @@ const SetLegalHoldModal = ({
>
<FormLayout withBorders={false} containerPadding={false}>
<Box className={"inputItem"}>
<strong>Object</strong>: {bucketName}
<strong>Object</strong>: {bucketName + "/" + objectName}
</Box>
<Switch
value="legalhold"

View File

@@ -40,6 +40,7 @@ import { api } from "api";
import { errorToHandler } from "api/errors";
import { getMaxShareLinkExpTime } from "screens/Console/ObjectBrowser/objectBrowserThunks";
import { maxShareLinkExpTime } from "screens/Console/ObjectBrowser/objectBrowserSlice";
import debounce from "lodash/debounce";
interface IShareFileProps {
open: boolean;
@@ -56,7 +57,7 @@ const ShareFile = ({
}: IShareFileProps) => {
const dispatch = useAppDispatch();
const distributedSetup = useSelector(selDistSet);
const maxshareLinkExpTimeVal = useSelector(maxShareLinkExpTime);
const maxShareLinkExpTimeVal = useSelector(maxShareLinkExpTime);
const [shareURL, setShareURL] = useState<string>("");
const [isLoadingVersion, setIsLoadingVersion] = useState<boolean>(true);
const [isLoadingFile, setIsLoadingFile] = useState<boolean>(false);
@@ -64,9 +65,7 @@ const ShareFile = ({
const [dateValid, setDateValid] = useState<boolean>(true);
const [versionID, setVersionID] = useState<string>("null");
const initialDate = new Date();
const dateChanged = (newDate: string, isValid: boolean) => {
const debouncedDateChange = debounce((newDate: string, isValid: boolean) => {
setDateValid(isValid);
if (isValid) {
setSelectedDate(newDate);
@@ -74,7 +73,7 @@ const ShareFile = ({
}
setSelectedDate("");
setShareURL("");
};
}, 300);
useEffect(() => {
dispatch(getMaxShareLinkExpTime());
@@ -205,7 +204,7 @@ const ShareFile = ({
The following URL lets you share this object without requiring
a login. <br />
The URL expires automatically at the earlier of your
configured time ({niceTimeFromSeconds(maxshareLinkExpTimeVal)}
configured time ({niceTimeFromSeconds(maxShareLinkExpTimeVal)}
) or the expiration of your current web session.
</span>
</Tooltip>
@@ -213,11 +212,10 @@ const ShareFile = ({
<br />
<Grid item xs={12}>
<DaysSelector
initialDate={initialDate}
id="date"
label="Active for"
maxSeconds={maxshareLinkExpTimeVal}
onChange={dateChanged}
maxSeconds={maxShareLinkExpTimeVal}
onChange={debouncedDateChange}
entity="Link"
/>
</Grid>

View File

@@ -24,28 +24,23 @@ const HOUR_MINUTES = 60;
interface IDaysSelector {
id: string;
initialDate: Date;
maxSeconds: number;
label: string;
entity: string;
onChange: (newDate: string, isValid: boolean) => void;
}
const calculateNewTime = (
initialDate: Date,
days: number,
hours: number,
minutes: number,
) => {
return DateTime.fromJSDate(initialDate).plus({
hours: hours + days * 24,
minutes,
}); // Lump days into hours to avoid daylight savings causing issues
const calculateNewTime = (days: number, hours: number, minutes: number) => {
return DateTime.now()
.plus({
hours: hours + days * 24,
minutes,
})
.toISO(); // Lump days into hours to avoid daylight savings causing issues
};
const DaysSelector = ({
id,
initialDate,
label,
maxSeconds,
entity,
@@ -59,7 +54,7 @@ const DaysSelector = ({
const [selectedHours, setSelectedHours] = useState<number>(0);
const [selectedMinutes, setSelectedMinutes] = useState<number>(0);
const [validDate, setValidDate] = useState<boolean>(true);
const [dateSelected, setDateSelected] = useState<DateTime>(DateTime.now());
const [dateSelected, setDateSelected] = useState<string | null>(null);
// Set initial values
useEffect(() => {
@@ -75,19 +70,16 @@ const DaysSelector = ({
!isNaN(selectedMinutes)
) {
setDateSelected(
calculateNewTime(
initialDate,
selectedDays,
selectedHours,
selectedMinutes,
),
calculateNewTime(selectedDays, selectedHours, selectedMinutes),
);
}
}, [initialDate, selectedDays, selectedHours, selectedMinutes]);
}, [selectedDays, selectedHours, selectedMinutes]);
useEffect(() => {
if (validDate) {
const formattedDate = dateSelected.toFormat("yyyy-MM-dd HH:mm:ss");
if (validDate && dateSelected) {
const formattedDate = DateTime.fromISO(dateSelected).toFormat(
"yyyy-MM-dd HH:mm:ss",
);
onChange(formattedDate.split(" ").join("T"), true);
} else {
onChange("0000-00-00", false);
@@ -270,12 +262,14 @@ const DaysSelector = ({
},
}}
>
{validDate ? (
{validDate && dateSelected ? (
<div className={"validityText"}>
<LinkIcon />
<div>{entity} will be available until:</div>{" "}
<div className={"validTill"}>
{dateSelected.toFormat("MM/dd/yyyy HH:mm:ss ZZZZ")}
{DateTime.fromISO(dateSelected).toFormat(
"MM/dd/yyyy HH:mm:ss ZZZZ",
)}
</div>
</div>
) : (

View File

@@ -18748,8 +18748,8 @@ __metadata:
linkType: hard
"ws@npm:^7.2.0, ws@npm:^7.4.6":
version: 7.5.9
resolution: "ws@npm:7.5.9"
version: 7.5.10
resolution: "ws@npm:7.5.10"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
@@ -18758,7 +18758,7 @@ __metadata:
optional: true
utf-8-validate:
optional: true
checksum: 10c0/aec4ef4eb65821a7dde7b44790f8699cfafb7978c9b080f6d7a98a7f8fc0ce674c027073a78574c94786ba7112cc90fa2cc94fc224ceba4d4b1030cff9662494
checksum: 10c0/bd7d5f4aaf04fae7960c23dcb6c6375d525e00f795dd20b9385902bd008c40a94d3db3ce97d878acc7573df852056ca546328b27b39f47609f80fb22a0a9b61d
languageName: node
linkType: hard