github.com/minio/console@v1.3.0/api/ws_objects.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2023 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package api
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/minio/console/models"
    27  	"github.com/minio/websocket"
    28  )
    29  
    30  func (wsc *wsMinioClient) objectManager(session *models.Principal) {
    31  	// Storage of Cancel Contexts for this connection
    32  	cancelContexts := make(map[int64]context.CancelFunc)
    33  	// Initial goroutine
    34  	defer func() {
    35  		// We close socket at the end of requests
    36  		wsc.conn.close()
    37  		for _, c := range cancelContexts {
    38  			// invoke cancel
    39  			c()
    40  		}
    41  	}()
    42  
    43  	writeChannel := make(chan WSResponse)
    44  	done := make(chan interface{})
    45  
    46  	// Read goroutine
    47  	go func() {
    48  		for {
    49  			mType, message, err := wsc.conn.readMessage()
    50  			if err != nil {
    51  				LogInfo("Error while reading objectManager message", err)
    52  				close(done)
    53  				return
    54  			}
    55  
    56  			if mType == websocket.TextMessage {
    57  				// We get request data & review information
    58  				var messageRequest ObjectsRequest
    59  
    60  				err := json.Unmarshal(message, &messageRequest)
    61  				if err != nil {
    62  					LogInfo("Error on message request unmarshal")
    63  
    64  					close(done)
    65  					return
    66  				}
    67  
    68  				// new message, new context
    69  				ctx, cancel := context.WithCancel(context.Background())
    70  
    71  				// We store the cancel func associated with this request
    72  				cancelContexts[messageRequest.RequestID] = cancel
    73  
    74  				const itemsPerBatch = 1000
    75  				switch messageRequest.Mode {
    76  				case "close":
    77  					close(done)
    78  					return
    79  				case "cancel":
    80  					// if we have that request id, cancel it
    81  					if cancelFunc, ok := cancelContexts[messageRequest.RequestID]; ok {
    82  						cancelFunc()
    83  						delete(cancelContexts, messageRequest.RequestID)
    84  					}
    85  				case "objects":
    86  					// cancel all previous open objects requests for listing
    87  					for rid, c := range cancelContexts {
    88  						if rid < messageRequest.RequestID {
    89  							// invoke cancel
    90  							c()
    91  						}
    92  					}
    93  
    94  					// start listing and writing to web socket
    95  					go func() {
    96  						objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
    97  						if err != nil {
    98  							LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
    99  
   100  							writeChannel <- WSResponse{
   101  								RequestID:  messageRequest.RequestID,
   102  								Error:      ErrorWithContext(ctx, err),
   103  								Prefix:     messageRequest.Prefix,
   104  								BucketName: messageRequest.BucketName,
   105  							}
   106  
   107  							return
   108  						}
   109  						var buffer []ObjectResponse
   110  						for lsObj := range startObjectsListing(ctx, wsc.client, objectRqConfigs) {
   111  							if cancelContexts[messageRequest.RequestID] == nil {
   112  								return
   113  							}
   114  							if lsObj.Err != nil {
   115  								writeChannel <- WSResponse{
   116  									RequestID:  messageRequest.RequestID,
   117  									Error:      ErrorWithContext(ctx, lsObj.Err),
   118  									Prefix:     messageRequest.Prefix,
   119  									BucketName: messageRequest.BucketName,
   120  								}
   121  
   122  								continue
   123  							}
   124  							objItem := ObjectResponse{
   125  								Name:         lsObj.Key,
   126  								Size:         lsObj.Size,
   127  								LastModified: lsObj.LastModified.Format(time.RFC3339),
   128  								VersionID:    lsObj.VersionID,
   129  								IsLatest:     lsObj.IsLatest,
   130  								DeleteMarker: lsObj.IsDeleteMarker,
   131  							}
   132  							buffer = append(buffer, objItem)
   133  
   134  							if len(buffer) >= itemsPerBatch {
   135  								writeChannel <- WSResponse{
   136  									RequestID: messageRequest.RequestID,
   137  									Data:      buffer,
   138  								}
   139  								buffer = nil
   140  							}
   141  						}
   142  						if len(buffer) > 0 {
   143  							writeChannel <- WSResponse{
   144  								RequestID: messageRequest.RequestID,
   145  								Data:      buffer,
   146  							}
   147  						}
   148  
   149  						writeChannel <- WSResponse{
   150  							RequestID:  messageRequest.RequestID,
   151  							RequestEnd: true,
   152  						}
   153  
   154  						// remove the cancellation context
   155  						delete(cancelContexts, messageRequest.RequestID)
   156  					}()
   157  				case "rewind":
   158  					// cancel all previous open objects requests for listing
   159  					for rid, c := range cancelContexts {
   160  						if rid < messageRequest.RequestID {
   161  							// invoke cancel
   162  							c()
   163  						}
   164  					}
   165  
   166  					// start listing and writing to web socket
   167  					go func() {
   168  						objectRqConfigs, err := getObjectsOptionsFromReq(messageRequest)
   169  						if err != nil {
   170  							LogInfo(fmt.Sprintf("Error during Objects OptionsParse %s", err.Error()))
   171  							writeChannel <- WSResponse{
   172  								RequestID:  messageRequest.RequestID,
   173  								Error:      ErrorWithContext(ctx, err),
   174  								Prefix:     messageRequest.Prefix,
   175  								BucketName: messageRequest.BucketName,
   176  							}
   177  
   178  							return
   179  						}
   180  
   181  						clientIP := wsc.conn.remoteAddress()
   182  
   183  						s3Client, err := newS3BucketClient(session, objectRqConfigs.BucketName, objectRqConfigs.Prefix, clientIP)
   184  						if err != nil {
   185  							writeChannel <- WSResponse{
   186  								RequestID:  messageRequest.RequestID,
   187  								Error:      ErrorWithContext(ctx, err),
   188  								Prefix:     messageRequest.Prefix,
   189  								BucketName: messageRequest.BucketName,
   190  							}
   191  
   192  							cancel()
   193  							return
   194  						}
   195  
   196  						mcS3C := mcClient{client: s3Client}
   197  
   198  						var buffer []ObjectResponse
   199  
   200  						for lsObj := range startRewindListing(ctx, mcS3C, objectRqConfigs) {
   201  							if lsObj.Err != nil {
   202  								writeChannel <- WSResponse{
   203  									RequestID:  messageRequest.RequestID,
   204  									Error:      ErrorWithContext(ctx, lsObj.Err.ToGoError()),
   205  									Prefix:     messageRequest.Prefix,
   206  									BucketName: messageRequest.BucketName,
   207  								}
   208  
   209  								continue
   210  							}
   211  
   212  							name := strings.Replace(lsObj.URL.Path, fmt.Sprintf("/%s/", objectRqConfigs.BucketName), "", 1)
   213  
   214  							objItem := ObjectResponse{
   215  								Name:         name,
   216  								Size:         lsObj.Size,
   217  								LastModified: lsObj.Time.Format(time.RFC3339),
   218  								VersionID:    lsObj.VersionID,
   219  								IsLatest:     lsObj.IsLatest,
   220  								DeleteMarker: lsObj.IsDeleteMarker,
   221  							}
   222  							buffer = append(buffer, objItem)
   223  
   224  							if len(buffer) >= itemsPerBatch {
   225  								writeChannel <- WSResponse{
   226  									RequestID: messageRequest.RequestID,
   227  									Data:      buffer,
   228  								}
   229  								buffer = nil
   230  							}
   231  
   232  						}
   233  						if len(buffer) > 0 {
   234  							writeChannel <- WSResponse{
   235  								RequestID: messageRequest.RequestID,
   236  								Data:      buffer,
   237  							}
   238  						}
   239  
   240  						writeChannel <- WSResponse{
   241  							RequestID:  messageRequest.RequestID,
   242  							RequestEnd: true,
   243  						}
   244  
   245  						// remove the cancellation context
   246  						delete(cancelContexts, messageRequest.RequestID)
   247  					}()
   248  				}
   249  			}
   250  		}
   251  	}()
   252  
   253  	// Write goroutine
   254  	go func() {
   255  		for {
   256  			select {
   257  			case <-done:
   258  				return
   259  			case writeM := <-writeChannel:
   260  				jsonData, err := json.Marshal(writeM)
   261  				if err != nil {
   262  					LogInfo("Error while marshaling the response", err)
   263  					return
   264  				}
   265  
   266  				err = wsc.conn.writeMessage(websocket.TextMessage, jsonData)
   267  				if err != nil {
   268  					LogInfo("Error while writing the message", err)
   269  					return
   270  				}
   271  			}
   272  		}
   273  	}()
   274  
   275  	<-done
   276  }