From 417141323e92f79ac5d7b09f5f1485bf9573a353 Mon Sep 17 00:00:00 2001 From: lovehunter9 Date: Wed, 30 Oct 2024 14:24:29 +0800 Subject: [PATCH] feat: awss3 support first commit: only support list, meta and copy from awss3 copy to other arch drives --- packages/backend/http/awss3.go | 466 ++++++++++++++++++++++++++ packages/backend/http/google_drive.go | 2 +- packages/backend/http/paste.go | 121 ++++++- packages/backend/http/resource.go | 25 +- 4 files changed, 588 insertions(+), 26 deletions(-) create mode 100644 packages/backend/http/awss3.go diff --git a/packages/backend/http/awss3.go b/packages/backend/http/awss3.go new file mode 100644 index 0000000..a637b6f --- /dev/null +++ b/packages/backend/http/awss3.go @@ -0,0 +1,466 @@ +package http + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" + "sync" +) + +type Awss3ListParam struct { + Path string `json:"path"` + Drive string `json:"drive"` + Name string `json:"name"` +} + +type Awss3ListResponse struct { + StatusCode string `json:"status_code"` + FailReason *string `json:"fail_reason,omitempty"` + Data []*Awss3ListResponseFileData `json:"data"` + sync.Mutex +} + +type Awss3ListResponseFileData struct { + Path string `json:"path"` + Name string `json:"name"` + Size int64 `json:"size"` + FileSize int64 `json:"fileSize"` + Extension string `json:"extension"` + Modified *string `json:"modified,omitempty"` + Mode string `json:"mode"` + IsDir bool `json:"isDir"` + IsSymlink bool `json:"isSymlink"` + Type string `json:"type"` + Meta *Awss3ListResponseFileMeta `json:"meta,omitempty"` +} + +type Awss3ListResponseFileMeta struct { + ETag string `json:"e_tag"` + Key string `json:"key"` + LastModified *string `json:"last_modified,omitempty"` + Owner *string `json:"owner,omitempty"` + Size int `json:"size"` + StorageClass string `json:"storage_class"` +} + +type Awss3MetaResponseMeta struct { + ETag string `json:"e_tag"` + Key string `json:"key"` + LastModified *string `json:"last_modified,omitempty"` + Owner *string `json:"owner"` + Size int64 `json:"size"` + StorageClass *string `json:"storage_class"` +} + +type Awss3MetaResponseData struct { + Path string `json:"path"` + Name string `json:"name"` + Size int64 `json:"size"` + FileSize int64 `json:"fileSize"` + Extension string `json:"extension"` + Modified *string `json:"modified,omitempty"` + Mode string `json:"mode"` + IsDir bool `json:"isDir"` + IsSymlink bool `json:"isSymlink"` + Type string `json:"type"` + Meta Awss3MetaResponseMeta `json:"meta"` +} + +type Awss3MetaResponse struct { + StatusCode string `json:"status_code"` + FailReason *string `json:"fail_reason"` + Data Awss3MetaResponseData `json:"data"` +} + +type Awss3FocusedMetaInfos struct { + Key string `json:"key"` + Path string `json:"path"` + Name string `json:"name"` + Size int64 `json:"size"` + IsDir bool `json:"is_dir"` +} + +type Awss3DownloadFileSyncParam struct { + LocalFolder string `json:"local_folder"` + CloudFilePath string `json:"cloud_file_path"` + Drive string `json:"drive"` + Name string `json:"name"` +} + +func getAwss3FocusedMetaInfos(src string, w http.ResponseWriter, r *http.Request) (info *Awss3FocusedMetaInfos, err error) { + src = strings.TrimSuffix(src, "/") + info = nil + err = nil + + srcDrive, srcName, srcPath := parseAwss3Path(src) + + param := Awss3ListParam{ + Path: srcPath, + Drive: srcDrive, // "my_drive", + Name: srcName, // "file_name", + } + + // 将数据序列化为 JSON + jsonBody, err := json.Marshal(param) + if err != nil { + fmt.Println("Error marshalling JSON:", err) + return + } + fmt.Println("Awss3 Awss3MetaResponseMeta Params:", string(jsonBody)) + respBody, err := Awss3Call("/drive/get_file_meta_data", "POST", jsonBody, w, r, true) + if err != nil { + fmt.Println("Error calling drive/get_file_meta_data:", err) + return + } + + var bodyJson Awss3MetaResponse + if err = json.Unmarshal(respBody, &bodyJson); err != nil { + fmt.Println(err) + return + } + + info = &Awss3FocusedMetaInfos{ + Key: bodyJson.Data.Meta.Key, + Path: bodyJson.Data.Path, + Name: bodyJson.Data.Name, + Size: bodyJson.Data.FileSize, + IsDir: bodyJson.Data.IsDir, + } + return +} + +func generateAwss3FilesData(body []byte, stopChan <-chan struct{}, dataChan chan<- string, + w http.ResponseWriter, r *http.Request, param Awss3ListParam) { + defer close(dataChan) + + var bodyJson Awss3ListResponse + if err := json.Unmarshal(body, &bodyJson); err != nil { + fmt.Println(err) + return + } + + var A []*Awss3ListResponseFileData + bodyJson.Lock() + A = append(A, bodyJson.Data...) + bodyJson.Unlock() + + for len(A) > 0 { + fmt.Println("len(A): ", len(A)) + firstItem := A[0] + fmt.Println("firstItem Path: ", firstItem.Path) + fmt.Println("firstItem Name:", firstItem.Name) + + if firstItem.IsDir { + firstParam := Awss3ListParam{ + Path: firstItem.Path, + Drive: param.Drive, + Name: param.Name, + } + firstJsonBody, err := json.Marshal(firstParam) + if err != nil { + fmt.Println("Error marshalling JSON:", err) + fmt.Println(err) + return + } + var firstRespBody []byte + firstRespBody, err = Awss3Call("/drive/ls", "POST", firstJsonBody, w, r, true) + + var firstBodyJson Awss3ListResponse + if err := json.Unmarshal(firstRespBody, &firstBodyJson); err != nil { + fmt.Println(err) + return + } + + A = append(firstBodyJson.Data, A[1:]...) + } else { + dataChan <- formatSSEvent(firstItem) + + A = A[1:] + } + + select { + case <-stopChan: + return + default: + } + } +} + +func streamAwss3Files(w http.ResponseWriter, r *http.Request, body []byte, param Awss3ListParam) { + w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + stopChan := make(chan struct{}) + dataChan := make(chan string) + + go generateAwss3FilesData(body, stopChan, dataChan, w, r, param) + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) + return + } + + for { + select { + case event, ok := <-dataChan: + if !ok { + return + } + _, err := w.Write([]byte(event)) + if err != nil { + fmt.Println(err) + return + } + flusher.Flush() + + case <-r.Context().Done(): + close(stopChan) + return + } + } +} + +func awss3FileToBuffer(src, bufferFilePath string, w http.ResponseWriter, r *http.Request) error { + src = strings.TrimSuffix(src, "/") + if !strings.HasSuffix(bufferFilePath, "/") { + bufferFilePath += "/" + } + srcDrive, srcName, srcPath := parseAwss3Path(src) + //srcPathId, srcDrive, srcName, srcDir, srcFilename, err := GoogleDrivePathToId(src, w, r, false) + fmt.Println("srcDrive:", srcDrive, "srcName:", srcName, "srcPath:", srcPath) + if srcPath == "" { + fmt.Println("Src parse failed.") + return nil + } + + // 填充数据 + param := Awss3DownloadFileSyncParam{ + LocalFolder: bufferFilePath, + CloudFilePath: srcPath, + Drive: srcDrive, // "my_drive", + Name: srcName, // "file_name", + } + + // 将数据序列化为 JSON + jsonBody, err := json.Marshal(param) + if err != nil { + fmt.Println("Error marshalling JSON:", err) + return err + } + fmt.Println("Download File Params:", string(jsonBody)) + + //var respBody []byte + _, err = Awss3Call("/drive/download_sync", "POST", jsonBody, w, r, true) + if err != nil { + fmt.Println("Error calling drive/download_sync:", err) + return err + } + return nil + //var respJson GoogleDriveTaskResponse + //if err = json.Unmarshal(respBody, &respJson); err != nil { + // fmt.Println(err) + // return err + //} + //taskId := respJson.Data.ID + //taskParam := GoogleDriveTaskQueryParam{ + // TaskIds: []string{taskId}, + //} + //// 将数据序列化为 JSON + //taskJsonBody, err := json.Marshal(taskParam) + //if err != nil { + // fmt.Println("Error marshalling JSON:", err) + // return err + //} + //fmt.Println("Task Params:", string(taskJsonBody)) + // + //for { + // time.Sleep(1000 * time.Millisecond) + // var taskRespBody []byte + // taskRespBody, err = GoogleDriveCall("/drive/task/query/task_ids", "POST", taskJsonBody, w, r, true) + // if err != nil { + // fmt.Println("Error calling drive/download_async:", err) + // return err + // } + // var taskRespJson GoogleDriveTaskQueryResponse + // if err = json.Unmarshal(taskRespBody, &taskRespJson); err != nil { + // fmt.Println(err) + // return err + // } + // if len(taskRespJson.Data) == 0 { + // return e.New("Task Info Not Found") + // } + // if taskRespJson.Data[0].Status != "Waiting" && taskRespJson.Data[0].Status != "InProgress" { + // if taskRespJson.Data[0].Status == "Completed" { + // return nil + // } + // return e.New(taskRespJson.Data[0].Status) + // } + //} +} + +func Awss3Call(dst, method string, reqBodyJson []byte, w http.ResponseWriter, r *http.Request, returnResp bool) ([]byte, error) { + bflName := r.Header.Get("X-Bfl-User") + if bflName == "" { + return nil, os.ErrPermission + } + + authority := r.Header.Get("Authority") + fmt.Println("*****Awss3 Call URL authority:", authority) + host := r.Header.Get("Origin") + if host == "" { + host = getHost(w, r) // r.Header.Get("Origin") + } + fmt.Println("*****Awss3 Call URL host:", host) + dstUrl := host + dst // "/api/resources%2FHome%2FDocuments%2F" + + fmt.Println("dstUrl:", dstUrl) + + var req *http.Request + var err error + if reqBodyJson != nil { + req, err = http.NewRequest(method, dstUrl, bytes.NewBuffer(reqBodyJson)) + } else { + req, err = http.NewRequest(method, dstUrl, nil) + } + + if err != nil { + fmt.Println("Error creating request:", err) + return nil, err + } + + // 设置请求头 + req.Header = r.Header.Clone() + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Println("Error making request:", err) + return nil, err + } + defer resp.Body.Close() + + // 检查Content-Type + contentType := resp.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "application/json") { + fmt.Println("Awss3 Call Response is not JSON format:", contentType) + } + + // 读取响应体 + var body []byte + if resp.Header.Get("Content-Encoding") == "gzip" { + // 如果响应体被gzip压缩 + reader, err := gzip.NewReader(resp.Body) + if err != nil { + fmt.Println("Error creating gzip reader:", err) + return nil, err + } + defer reader.Close() + + body, err = ioutil.ReadAll(reader) + if err != nil { + fmt.Println("Error reading gzipped response body:", err) + return nil, err + } + } else { + // 如果响应体没有被压缩 + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body:", err) + return nil, err + } + } + + // 解析JSON + var datas map[string]interface{} + err = json.Unmarshal(body, &datas) + if err != nil { + fmt.Println("Error unmarshaling JSON response:", err) + return nil, err + } + + // 打印解析后的数据 + fmt.Println("Parsed JSON response:", datas) + // 将解析后的JSON响应体转换为字符串(格式化输出) + responseText, err := json.MarshalIndent(datas, "", " ") + if err != nil { + http.Error(w, "Error marshaling JSON response to text: "+err.Error(), http.StatusInternalServerError) + return nil, err + } + + if returnResp { + return responseText, nil + } + // 设置响应头并写入响应体 + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Write([]byte(responseText)) + return nil, nil +} + +func parseAwss3Path(src string) (drive, name, path string) { + if strings.HasPrefix(src, "/Drive/awss3") { + src = src[12:] + drive = "awss3" + } + + slashes := []int{} + for i, char := range src { + if char == '/' { + slashes = append(slashes, i) + } + } + + if len(slashes) < 2 { + fmt.Println("Path does not contain enough slashes.") + return drive, "", "" + } + + name = src[1:slashes[1]] + path = src[slashes[1]:] + return drive, name, path +} + +func resourceGetAwss3(w http.ResponseWriter, r *http.Request, stream int, meta int) (int, error) { + src := r.URL.Path + fmt.Println("src Path:", src) + + srcDrive, srcName, srcPath := parseAwss3Path(src) + fmt.Println("srcDrive: ", srcDrive, ", srcName: ", srcName, ", src Path: ", srcPath) + + param := Awss3ListParam{ + Path: srcPath, + Drive: srcDrive, // "my_drive", + Name: srcName, // "file_name", + } + + // 将数据序列化为 JSON + jsonBody, err := json.Marshal(param) + if err != nil { + fmt.Println("Error marshalling JSON:", err) + return errToStatus(err), err + } + fmt.Println("Awss3 List Params:", string(jsonBody)) + if stream == 1 { + var body []byte + body, err = Awss3Call("/drive/ls", "POST", jsonBody, w, r, true) + streamAwss3Files(w, r, body, param) + return 0, nil + } + if meta == 1 { + _, err = Awss3Call("/drive/get_file_meta_data", "POST", jsonBody, w, r, false) + } else { + _, err = Awss3Call("/drive/ls", "POST", jsonBody, w, r, false) + } + if err != nil { + fmt.Println("Error calling drive/ls:", err) + return errToStatus(err), err + } + return 0, nil +} diff --git a/packages/backend/http/google_drive.go b/packages/backend/http/google_drive.go index 6cc1abb..54548ec 100644 --- a/packages/backend/http/google_drive.go +++ b/packages/backend/http/google_drive.go @@ -732,7 +732,7 @@ func getGoogleDriveIdFocusedMetaInfos(src string, w http.ResponseWriter, r *http fmt.Println("Error marshalling JSON:", err) return } - fmt.Println("Google Drive Meta Params:", string(jsonBody)) + fmt.Println("Google Drive Awss3MetaResponseMeta Params:", string(jsonBody)) respBody, err := GoogleDriveCall("/drive/get_file_meta_data", "POST", jsonBody, w, r, true) if err != nil { fmt.Println("Error calling drive/get_file_meta_data:", err) diff --git a/packages/backend/http/paste.go b/packages/backend/http/paste.go index 70cb6e8..b7be7ab 100644 --- a/packages/backend/http/paste.go +++ b/packages/backend/http/paste.go @@ -191,7 +191,9 @@ func generateBufferFolder(originalFilePath, bflName string) (string, error) { // 获得原始文件夹路径 originalPathName := filepath.Base(strings.TrimSuffix(originalFilePath, "/")) extension := filepath.Ext(originalPathName) - originalPathName = strings.TrimSuffix(originalPathName, extension) + "_" + extension[1:] + if len(extension) > 0 { + originalPathName = strings.TrimSuffix(originalPathName, extension) + "_" + extension[1:] + } originalPathName = url.QueryEscape(originalPathName) // 构建新的文件名 @@ -205,7 +207,7 @@ func generateBufferFolder(originalFilePath, bflName string) (string, error) { return bufferFolderPath, nil } -func makeDiskBuffer(filePath string, bufferSize int64) error { +func makeDiskBuffer(filePath string, bufferSize int64, delete bool) error { //filePath := "buffer.bin" //bufferSize := int64(1024 * 1024) // 1 MB @@ -228,6 +230,16 @@ func makeDiskBuffer(filePath string, bufferSize int64) error { return err } fmt.Println("Buffer file size:", fileInfo.Size(), "bytes") + + if delete { + err = os.Remove(filePath) + if err != nil { + fmt.Printf("Error removing test buffer: %v\n", err) + return err + } + + fmt.Println("Test buffer removed successfully") + } return nil } @@ -1259,11 +1271,11 @@ func resourcePasteHandler(fileCache FileCache) handleFunc { dstType = "drive" } fmt.Println(srcType, src, dstType, dst) - if srcType != "drive" && srcType != "sync" && srcType != "cache" && srcType != "google" { + if srcType != "drive" && srcType != "sync" && srcType != "cache" && srcType != "google" && srcType != "awss3" { fmt.Println("Src type is invalid!") return http.StatusForbidden, nil } - if dstType != "drive" && dstType != "sync" && dstType != "cache" && dstType != "google" { + if dstType != "drive" && dstType != "sync" && dstType != "cache" && dstType != "google" && dstType != "awss3" { fmt.Println("Dst type is invalid!") return http.StatusForbidden, nil } @@ -1307,9 +1319,13 @@ func resourcePasteHandler(fileCache FileCache) handleFunc { var srcName, dstName string if srcType == "google" { _, srcName, _, _ = parseGoogleDrivePath(src) + } else if srcType == "awss3" { + _, srcName, _ = parseAwss3Path(src) } if dstType == "google" { _, dstName, _, _ = parseGoogleDrivePath(dst) + } else if srcType == "awss3" { + _, dstName, _ = parseAwss3Path(dst) } if srcName != dstName { same = false @@ -1440,7 +1456,14 @@ func getStat(fs afero.Fs, srcType, src string, w http.ResponseWriter, r *http.Re if err != nil { return nil, 0, 0, false, err } - return nil, metaInfo.Size, 0, metaInfo.IsDir, nil + return nil, metaInfo.Size, 0755, metaInfo.IsDir, nil + } else if srcType == "awss3" { + src = strings.TrimSuffix(src, "/") + metaInfo, err := getAwss3FocusedMetaInfos(src, w, r) + if err != nil { + return nil, 0, 0, false, err + } + return nil, metaInfo.Size, 0755, metaInfo.IsDir, nil } else if srcType == "cache" { //host := r.Host //infoUrl := "http://" + host + "/api/resources" + src @@ -1707,8 +1730,6 @@ func copyDir(fs afero.Fs, srcType, src, dstType, dst string, d *data, fileMode o return err } mode = srcinfo.Mode() - } else if srcType == "google" { - mode = 0755 } else { mode = fileMode } @@ -1730,6 +1751,8 @@ func copyDir(fs afero.Fs, srcType, src, dstType, dst string, d *data, fileMode o if err != nil { return err } + } else if dstType == "awss3" { + } else if dstType == "cache" { err := cacheMkdirAll(dst, fileMode, r) if err != nil { @@ -1833,6 +1856,53 @@ func copyDir(fs afero.Fs, srcType, src, dstType, dst string, d *data, fileMode o } } } + } else if srcType == "awss3" { + src = strings.TrimSuffix(src, "/") + + srcDrive, srcName, srcPath := parseAwss3Path(src) + + param := Awss3ListParam{ + Path: srcPath, + Drive: srcDrive, + Name: srcName, + } + + // 将数据序列化为 JSON + jsonBody, err := json.Marshal(param) + if err != nil { + fmt.Println("Error marshalling JSON:", err) + return err + } + fmt.Println("Awss3 List Params:", string(jsonBody)) + var respBody []byte + respBody, err = Awss3Call("/drive/ls", "POST", jsonBody, w, r, true) + if err != nil { + fmt.Println("Error calling drive/ls:", err) + return err + } + var bodyJson Awss3ListResponse + if err = json.Unmarshal(respBody, &bodyJson); err != nil { + fmt.Println(err) + return err + } + for _, item := range bodyJson.Data { + fsrc := filepath.Join(src, item.Name) + fdst := filepath.Join(fdstBase, item.Name) + fmt.Println(fsrc, fdst) + if item.IsDir { + // 创建子目录,递归处理 + err = copyDir(fs, srcType, fsrc, dstType, fdst, d, os.FileMode(0755), w, r, driveIdCache) + if err != nil { + return err + } + } else { + // 执行文件复制 + err = copyFile(fs, srcType, fsrc, dstType, fdst, d, os.FileMode(0755), item.FileSize, w, r, driveIdCache) + if err != nil { + return err + } + } + } } else if srcType == "cache" { type Item struct { Path string `json:"path"` @@ -2123,7 +2193,7 @@ func copyFile(fs afero.Fs, srcType, src, dstType, dst string, d *data, mode os.F return err } fmt.Println("Buffer file path: ", bufferPath) - err = makeDiskBuffer(bufferPath, diskSize) + err = makeDiskBuffer(bufferPath, diskSize, false) if err != nil { return err } @@ -2146,10 +2216,10 @@ func copyFile(fs afero.Fs, srcType, src, dstType, dst string, d *data, mode os.F bufferPath = filepath.Join(bufferFilePath, srcInfo.Name) fmt.Println("Buffer file path: ", bufferFilePath) fmt.Println("Buffer path: ", bufferPath) - //err = makeDiskBuffer(bufferPath, diskSize) - //if err != nil { - // return err - //} + err = makeDiskBuffer(bufferPath, diskSize, true) + if err != nil { + return err + } _, err = googleFileToBuffer(src, bufferFilePath, w, r) //bufferPath = filepath.Join(bufferFilePath, bufferFilename) //fmt.Println("Buffer file path: ", bufferFilePath) @@ -2157,6 +2227,29 @@ func copyFile(fs afero.Fs, srcType, src, dstType, dst string, d *data, mode os.F if err != nil { return err } + } else if srcType == "awss3" { + var err error + if diskSize >= 4*1024*1024*1024 { + fmt.Println("file size exceeds 4GB") + return e.New("file size exceeds 4GB") //os.ErrPermission + } + fmt.Println("Will reserve disk size: ", diskSize) + srcInfo, err := getAwss3FocusedMetaInfos(src, w, r) + bufferFilePath, err := generateBufferFolder(srcInfo.Path, bflName) + if err != nil { + return err + } + bufferPath = filepath.Join(bufferFilePath, srcInfo.Name) + fmt.Println("Buffer file path: ", bufferFilePath) + fmt.Println("Buffer path: ", bufferPath) + err = makeDiskBuffer(bufferPath, diskSize, true) + if err != nil { + return err + } + err = awss3FileToBuffer(src, bufferFilePath, w, r) + if err != nil { + return err + } } else if srcType == "cache" { var err error if diskSize >= 4*1024*1024*1024 { @@ -2169,7 +2262,7 @@ func copyFile(fs afero.Fs, srcType, src, dstType, dst string, d *data, mode os.F return err } fmt.Println("Buffer file path: ", bufferPath) - err = makeDiskBuffer(bufferPath, diskSize) + err = makeDiskBuffer(bufferPath, diskSize, false) if err != nil { return err } @@ -2189,7 +2282,7 @@ func copyFile(fs afero.Fs, srcType, src, dstType, dst string, d *data, mode os.F return err } fmt.Println("Buffer file path: ", bufferPath) - err = makeDiskBuffer(bufferPath, diskSize) + err = makeDiskBuffer(bufferPath, diskSize, false) if err != nil { return err } diff --git a/packages/backend/http/resource.go b/packages/backend/http/resource.go index a224ab7..632c852 100644 --- a/packages/backend/http/resource.go +++ b/packages/backend/http/resource.go @@ -152,20 +152,23 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d } fmt.Println("stream: ", stream) + metaStr := r.URL.Query().Get("meta") + meta := 0 + if metaStr != "" { + meta, err = strconv.Atoi(metaStr) + if err != nil { + return http.StatusBadRequest, err + } + } + fmt.Println("meta: ", meta) + srcType := r.URL.Query().Get("src") if srcType == "sync" { return resourceGetSync(w, r, stream) } else if srcType == "google" { - metaStr := r.URL.Query().Get("meta") - meta := 0 - if metaStr != "" { - meta, err = strconv.Atoi(metaStr) - if err != nil { - return http.StatusBadRequest, err - } - } - fmt.Println("meta: ", meta) return resourceGetGoogle(w, r, stream, meta) + } else if srcType == "awss3" { + return resourceGetAwss3(w, r, stream, meta) } xBflUser := r.Header.Get("X-Bfl-User") @@ -200,8 +203,8 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d } } - fmt.Println("USB Data:", usbData) - fmt.Println("HDD Data:", hddData) + fmt.Println("USB Awss3MetaResponseData:", usbData) + fmt.Println("HDD Awss3MetaResponseData:", hddData) } var file *files.FileInfo