github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 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  import { BucketObjectItem } from "./ListObjects/types";
    18  import { decodeURLString, encodeURLString } from "../../../../../common/utils";
    19  import { removeTrace } from "../../../ObjectBrowser/transferManager";
    20  import store from "../../../../../store";
    21  import { ContentType, PermissionResource } from "api/consoleApi";
    22  import { api } from "../../../../../api";
    23  import { setErrorSnackMessage } from "../../../../../systemSlice";
    24  import { StatusCodes } from "http-status-codes";
    25  const downloadWithLink = (href: string, downloadFileName: string) => {
    26    const link = document.createElement("a");
    27    link.href = href;
    28    link.download = downloadFileName;
    29    document.body.appendChild(link);
    30    link.click();
    31    document.body.removeChild(link);
    32  };
    33  
    34  export const downloadSelectedAsZip = async (
    35    bucketName: string,
    36    objectList: string[],
    37    resultFileName: string,
    38  ) => {
    39    const state = store.getState();
    40    const anonymousMode = state.system.anonymousMode;
    41  
    42    try {
    43      const resp = await api.buckets.downloadMultipleObjects(
    44        bucketName,
    45        objectList,
    46        {
    47          type: ContentType.Json,
    48          headers: anonymousMode
    49            ? {
    50                "X-Anonymous": "1",
    51              }
    52            : undefined,
    53        },
    54      );
    55      const blob = await resp.blob();
    56      const href = window.URL.createObjectURL(blob);
    57      downloadWithLink(href, resultFileName);
    58    } catch (err: any) {
    59      store.dispatch(
    60        setErrorSnackMessage({
    61          errorMessage: `Download of multiple files failed. ${err.statusText}`,
    62          detailedError: "",
    63        }),
    64      );
    65    }
    66  };
    67  
    68  const isFolder = (objectPath: string) => {
    69    return decodeURLString(objectPath).endsWith("/");
    70  };
    71  
    72  export const download = (
    73    bucketName: string,
    74    objectPath: string,
    75    versionID: any,
    76    fileSize: number,
    77    overrideFileName: string | null = null,
    78    id: string,
    79    progressCallback: (progress: number) => void,
    80    completeCallback: () => void,
    81    errorCallback: (msg: string) => void,
    82    abortCallback: () => void,
    83    toastCallback: () => void,
    84  ) => {
    85    let basename = document.baseURI.replace(window.location.origin, "");
    86    const state = store.getState();
    87    const anonymousMode = state.system.anonymousMode;
    88  
    89    let path = `${
    90      window.location.origin
    91    }${basename}api/v1/buckets/${bucketName}/objects/download?prefix=${objectPath}${
    92      overrideFileName !== null && overrideFileName.trim() !== ""
    93        ? `&override_file_name=${encodeURLString(overrideFileName || "")}`
    94        : ""
    95    }`;
    96    if (versionID) {
    97      path = path.concat(`&version_id=${versionID}`);
    98    }
    99  
   100    // If file is greater than 50GiB then we force browser download, if not then we use HTTP Request for Object Manager
   101    if (fileSize > 53687091200) {
   102      return new BrowserDownload(path, id, completeCallback, toastCallback);
   103    }
   104  
   105    let req = new XMLHttpRequest();
   106    req.open("GET", path, true);
   107    if (anonymousMode) {
   108      req.setRequestHeader("X-Anonymous", "1");
   109    }
   110    req.addEventListener(
   111      "progress",
   112      function (evt) {
   113        let percentComplete = Math.round((evt.loaded / fileSize) * 100);
   114        if (progressCallback) {
   115          progressCallback(percentComplete);
   116        }
   117      },
   118      false,
   119    );
   120  
   121    req.responseType = "blob";
   122    req.onreadystatechange = () => {
   123      if (req.readyState === XMLHttpRequest.DONE) {
   124        // Ensure object was downloaded fully, if it's a folder we don't get the fileSize
   125        let completeDownload =
   126          isFolder(objectPath) || req.response.size === fileSize;
   127  
   128        if (req.status === StatusCodes.OK && completeDownload) {
   129          const rspHeader = req.getResponseHeader("Content-Disposition");
   130  
   131          let filename = "download";
   132          if (rspHeader) {
   133            let rspHeaderDecoded = decodeURIComponent(rspHeader);
   134            filename = rspHeaderDecoded.split('"')[1];
   135          }
   136  
   137          if (completeCallback) {
   138            completeCallback();
   139          }
   140  
   141          removeTrace(id);
   142  
   143          downloadWithLink(window.URL.createObjectURL(req.response), filename);
   144        } else {
   145          if (req.getResponseHeader("Content-Type") === "application/json") {
   146            const rspBody: { detailedMessage?: string } = JSON.parse(
   147              req.response,
   148            );
   149            if (rspBody.detailedMessage) {
   150              errorCallback(rspBody.detailedMessage);
   151              return;
   152            }
   153          }
   154          errorCallback(`Unexpected response, download incomplete.`);
   155        }
   156      }
   157    };
   158    req.onerror = () => {
   159      if (errorCallback) {
   160        errorCallback("A network error occurred.");
   161      }
   162    };
   163    req.onabort = () => {
   164      if (abortCallback) {
   165        abortCallback();
   166      }
   167    };
   168  
   169    return req;
   170  };
   171  
   172  class BrowserDownload {
   173    path: string;
   174    id: string;
   175    completeCallback: () => void;
   176    toastCallback: () => void;
   177  
   178    constructor(
   179      path: string,
   180      id: string,
   181      completeCallback: () => void,
   182      toastCallback: () => void,
   183    ) {
   184      this.path = path;
   185      this.id = id;
   186      this.completeCallback = completeCallback;
   187      this.toastCallback = toastCallback;
   188    }
   189  
   190    send(): void {
   191      this.toastCallback();
   192      const link = document.createElement("a");
   193      link.href = this.path;
   194      document.body.appendChild(link);
   195      link.click();
   196      document.body.removeChild(link);
   197      this.completeCallback();
   198      removeTrace(this.id);
   199    }
   200  }
   201  
   202  export type AllowedPreviews = "image" | "pdf" | "audio" | "video" | "none";
   203  export const contentTypePreview = (contentType: string): AllowedPreviews => {
   204    if (contentType) {
   205      const mimeObjectType = (contentType || "").toLowerCase();
   206  
   207      if (mimeObjectType.includes("image")) {
   208        return "image";
   209      }
   210      if (mimeObjectType.includes("pdf")) {
   211        return "pdf";
   212      }
   213      if (mimeObjectType.includes("audio")) {
   214        return "audio";
   215      }
   216      if (mimeObjectType.includes("video")) {
   217        return "video";
   218      }
   219    }
   220  
   221    return "none";
   222  };
   223  
   224  // Review file extension by name & returns the type of preview browser that can be used
   225  export const extensionPreview = (fileName: string): AllowedPreviews => {
   226    const imageExtensions = [
   227      "jif",
   228      "jfif",
   229      "apng",
   230      "avif",
   231      "svg",
   232      "webp",
   233      "bmp",
   234      "ico",
   235      "jpg",
   236      "jpe",
   237      "jpeg",
   238      "gif",
   239      "png",
   240      "heic",
   241    ];
   242    const pdfExtensions = ["pdf"];
   243    const audioExtensions = ["wav", "mp3", "alac", "aiff", "dsd", "pcm"];
   244    const videoExtensions = [
   245      "mp4",
   246      "avi",
   247      "mpg",
   248      "webm",
   249      "mov",
   250      "flv",
   251      "mkv",
   252      "wmv",
   253      "avchd",
   254      "mpeg-4",
   255    ];
   256  
   257    let fileExtension = fileName.split(".").pop();
   258  
   259    if (!fileExtension) {
   260      return "none";
   261    }
   262  
   263    fileExtension = fileExtension.toLowerCase();
   264  
   265    if (imageExtensions.includes(fileExtension)) {
   266      return "image";
   267    }
   268  
   269    if (pdfExtensions.includes(fileExtension)) {
   270      return "pdf";
   271    }
   272  
   273    if (audioExtensions.includes(fileExtension)) {
   274      return "audio";
   275    }
   276  
   277    if (videoExtensions.includes(fileExtension)) {
   278      return "video";
   279    }
   280  
   281    return "none";
   282  };
   283  
   284  export const previewObjectType = (
   285    metaData: Record<any, any>,
   286    objectName: string,
   287  ) => {
   288    const metaContentType = (
   289      (metaData && metaData["Content-Type"]) ||
   290      ""
   291    ).toString();
   292  
   293    const extensionType = extensionPreview(objectName || "");
   294    const contentType = contentTypePreview(metaContentType);
   295  
   296    let objectType: AllowedPreviews = extensionType;
   297  
   298    if (extensionType === contentType) {
   299      objectType = extensionType;
   300    } else if (extensionType === "none" && contentType !== "none") {
   301      objectType = contentType;
   302    } else if (contentType === "none" && extensionType !== "none") {
   303      objectType = extensionType;
   304    }
   305  
   306    return objectType;
   307  };
   308  export const sortListObjects = (fieldSort: string) => {
   309    switch (fieldSort) {
   310      case "name":
   311        return (a: BucketObjectItem, b: BucketObjectItem) =>
   312          a.name.localeCompare(b.name);
   313      case "last_modified":
   314        return (a: BucketObjectItem, b: BucketObjectItem) =>
   315          new Date(a.last_modified).getTime() -
   316          new Date(b.last_modified).getTime();
   317      case "size":
   318        return (a: BucketObjectItem, b: BucketObjectItem) =>
   319          (a.size || -1) - (b.size || -1);
   320    }
   321  };
   322  
   323  export const permissionItems = (
   324    bucketName: string,
   325    currentPath: string,
   326    permissionsArray: PermissionResource[],
   327  ): BucketObjectItem[] | null => {
   328    if (permissionsArray.length === 0) {
   329      return null;
   330    }
   331  
   332    // We get permissions applied to the current bucket
   333    const filteredPermissionsForBucket = permissionsArray.filter(
   334      (permissionItem) =>
   335        permissionItem.resource?.endsWith(`:${bucketName}`) ||
   336        permissionItem.resource?.includes(`:${bucketName}/`),
   337    );
   338  
   339    // No permissions for this bucket. we can throw the error message at this point
   340    if (filteredPermissionsForBucket.length === 0) {
   341      return null;
   342    }
   343  
   344    let returnElements: BucketObjectItem[] = [];
   345  
   346    // We split current path
   347    const splitCurrentPath = currentPath.split("/");
   348  
   349    filteredPermissionsForBucket.forEach((permissionElement) => {
   350      // We review paths in resource address
   351  
   352      // We split ARN & get the last item to check the URL
   353      const splitARN = permissionElement.resource?.split(":");
   354      const urlARN = splitARN?.pop() || "";
   355  
   356      // We split the paths of the URL & compare against current location to see if there are more items to include. In case current level is a wildcard or is the last one, we omit this validation
   357  
   358      const splitURLARN = urlARN.split("/");
   359  
   360      // splitURL has more items than bucket name, we can continue validating
   361      if (splitURLARN.length > 1) {
   362        splitURLARN.every((currentElementInPath, index) => {
   363          // It is a wildcard element. We can store the verification as value should be included (?)
   364          if (currentElementInPath === "*") {
   365            return false;
   366          }
   367  
   368          // Element is not included in the path. The user is trying to browse something else.
   369          if (
   370            splitCurrentPath[index] &&
   371            splitCurrentPath[index] !== currentElementInPath
   372          ) {
   373            return false;
   374          }
   375  
   376          // This element is not included by index in the current paths list. We add it so user can browse into it
   377          if (!splitCurrentPath[index]) {
   378            returnElements.push({
   379              name: `${currentElementInPath}/`,
   380              size: 0,
   381              last_modified: "",
   382              version_id: "",
   383            });
   384          }
   385  
   386          return true;
   387        });
   388      }
   389  
   390      // We review prefixes in allow resources for StringEquals variant only.
   391      if (
   392        permissionElement.conditionOperator === "StringEquals" ||
   393        permissionElement.conditionOperator === "StringLike"
   394      ) {
   395        permissionElement.prefixes?.forEach((prefixItem) => {
   396          // Prefix Item is not empty?
   397          if (prefixItem !== "") {
   398            const splitItems = prefixItem.split("/");
   399  
   400            let pathToRouteElements: string[] = [];
   401  
   402            // We verify if currentPath is contained in the path begin, if is not contained the  user has no access to this subpath
   403            const cleanCurrPath = currentPath.replace(/\/$/, "");
   404  
   405            if (!prefixItem.startsWith(cleanCurrPath) && currentPath !== "") {
   406              return;
   407            }
   408  
   409            // For every split element we iterate and check if we can construct a URL
   410            splitItems.every((splitElement, index) => {
   411              if (!splitElement.includes("*") && splitElement !== "") {
   412                if (splitElement !== splitCurrentPath[index]) {
   413                  returnElements.push({
   414                    name: `${pathToRouteElements.join("/")}${
   415                      pathToRouteElements.length > 0 ? "/" : ""
   416                    }${splitElement}/`,
   417                    size: 0,
   418                    last_modified: "",
   419                    version_id: "",
   420                  });
   421                  return false;
   422                }
   423                if (splitElement !== "") {
   424                  pathToRouteElements.push(splitElement);
   425                }
   426  
   427                return true;
   428              }
   429              return false;
   430            });
   431          }
   432        });
   433      }
   434    });
   435  
   436    // We clean duplicated name entries
   437    if (returnElements.length > 0) {
   438      let clElements: BucketObjectItem[] = [];
   439      let keys: string[] = [];
   440  
   441      returnElements.forEach((itm) => {
   442        if (!keys.includes(itm.name)) {
   443          clElements.push(itm);
   444          keys.push(itm.name);
   445        }
   446      });
   447  
   448      returnElements = clElements;
   449    }
   450  
   451    return returnElements;
   452  };