chromium/ui/file_manager/file_manager/background/js/file_operation_handler.ts

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {startIOTask} from '../../common/js/api.js';
import {PolicyErrorType, ProgressCenterItem, ProgressItemState, ProgressItemType} from '../../common/js/progress_center_common.js';
import {getFileErrorString, str, strf} from '../../common/js/translations.js';
import {checkAPIError, visitURL} from '../../common/js/util.js';
import {VolumeType} from '../../common/js/volume_manager_types.js';
import {getStore} from '../../state/store.js';

import type {ProgressCenter} from './progress_center.js';

/**
 * An event handler of the background page for file operations.
 */
export class FileOperationHandler {
  constructor(private progressCenter_: ProgressCenter) {
    chrome.fileManagerPrivate.onIOTaskProgressStatus.addListener(
        this.onIoTaskProgressStatus_.bind(this));
  }

  /**
   * Process the IO Task ProgressStatus events.
   */
  private onIoTaskProgressStatus_(
      event: chrome.fileManagerPrivate.ProgressStatus) {
    const taskId = String(event.taskId);
    let newItem = false;
    let item = this.progressCenter_.getItemById(taskId);
    if (!item) {
      item = new ProgressCenterItem();
      newItem = true;
      item.id = taskId;
      item.type = getTypeFromIoTaskType(event.type);
      item.itemCount = event.itemCount;
      const state = getStore().getState();
      const volume = state.volumes[event.destinationVolumeId];
      item.isDestinationDrive = volume?.volumeType === VolumeType.DRIVE;
      item.cancelCallback = () => {
        chrome.fileManagerPrivate.cancelIOTask(event.taskId);
      };
    }
    item.message = getMessageFromProgressEvent(event);
    item.sourceMessage = event.sourceName;
    item.destinationMessage = event.destinationName;

    switch (event.state) {
      case chrome.fileManagerPrivate.IoTaskState.QUEUED:
        item.progressMax = event.totalBytes;
        item.progressValue = event.bytesTransferred;
        item.remainingTime = event.remainingSeconds;
        break;
      case chrome.fileManagerPrivate.IoTaskState.SCANNING:
        item.sourceMessage = event.sourceName;
        item.destinationMessage = event.destinationName;
        item.state = ProgressItemState.SCANNING;
        // For scanning, the progress is the percentage of scanned items out of
        // the total count.
        item.progressMax = event.itemCount;
        item.progressValue = event.sourcesScanned;
        item.remainingTime = event.remainingSeconds;
        break;
      case chrome.fileManagerPrivate.IoTaskState.PAUSED:
        // Check if the task is paused because of warning level restrictions.
        if (event.pauseParams && event.pauseParams.policyParams) {
          item.state = ProgressItemState.PAUSED;
          item.policyFileCount = event.pauseParams.policyParams.policyFileCount;
          item.policyFileName = event.pauseParams.policyParams.fileName;
          const extraButtonText = getPolicyExtraButtonText(event);
          if (event.pauseParams.policyParams.policyFileCount === 1 &&
              !event.pauseParams.policyParams.alwaysShowReview) {
            item.setExtraButton(
                ProgressItemState.PAUSED, extraButtonText, () => {
                  // Proceed/cancel the action directly from the notification.
                  chrome.fileManagerPrivate.resumeIOTask(event.taskId, {
                    policyParams: {type: event.pauseParams!.policyParams!.type},
                    conflictParams: undefined,
                  });
                });
          } else {
            item.setExtraButton(
                ProgressItemState.PAUSED, extraButtonText, () => {
                  // Show the dialog to proceed/cancel.
                  chrome.fileManagerPrivate.showPolicyDialog(
                      event.taskId,
                      chrome.fileManagerPrivate.PolicyDialogType.WARNING,
                      checkAPIError);
                });
          }
          break;
        }
        // Otherwise same is in-progress - fall through
      case chrome.fileManagerPrivate.IoTaskState.IN_PROGRESS:
        item.progressMax = event.totalBytes;
        item.progressValue = event.bytesTransferred;
        item.remainingTime = event.remainingSeconds;
        item.state = ProgressItemState.PROGRESSING;
        break;
      case chrome.fileManagerPrivate.IoTaskState.SUCCESS:
      case chrome.fileManagerPrivate.IoTaskState.CANCELLED:
      case chrome.fileManagerPrivate.IoTaskState.ERROR:
        if (newItem) {
          // ERROR events can be dispatched before BEGIN events.
          item.progressMax = 1;
        }
        if (event.state === chrome.fileManagerPrivate.IoTaskState.SUCCESS) {
          item.state = ProgressItemState.COMPLETED;
          item.progressValue = item.progressMax;
          item.remainingTime = event.remainingSeconds;
          if (item.type === ProgressItemType.TRASH) {
            const infoEntries: Entry[] =
                (event.outputs ||
                 []).filter((o: Entry) => o.name.endsWith('.trashinfo'));
            item.setExtraButton(
                ProgressItemState.COMPLETED, str('UNDO_DELETE_ACTION_LABEL'),
                () => {
                  startIOTask(
                      chrome.fileManagerPrivate.IoTaskType.RESTORE, infoEntries,
                      /*params=*/ {});
                });
          }
        } else if (
            event.state === chrome.fileManagerPrivate.IoTaskState.CANCELLED) {
          item.state = ProgressItemState.CANCELED;
        } else {  // ERROR
          item.state = ProgressItemState.ERROR;
          item.skippedEncryptedFiles = event.skippedEncryptedFiles;
          // Check if there was a policy error.
          if (event.policyError) {
            item.policyError =
                getPolicyErrorFromIOTaskPolicyError(event.policyError.type);
            item.policyFileCount = event.policyError.policyFileCount;
            item.policyFileName = event.policyError.fileName;
            item.dismissCallback = () => {
              // For policy errors, we keep track of the task's info since it
              // might be required to review the details. Notify when dismissed
              // that this can be cleared.
              chrome.fileManagerPrivate.dismissIOTask(
                  event.taskId, checkAPIError);
            };
            const extraButtonText = getPolicyExtraButtonText(event);
            if (event.policyError.type !==
                    PolicyErrorType.DLP_WARNING_TIMEOUT &&
                (event.policyError.policyFileCount > 1 ||
                 event.policyError.alwaysShowReview)) {
              item.setExtraButton(
                  ProgressItemState.ERROR, extraButtonText, () => {
                    chrome.fileManagerPrivate.showPolicyDialog(
                        event.taskId,
                        chrome.fileManagerPrivate.PolicyDialogType.ERROR,
                        checkAPIError);
                  });
            } else if (
                event.policyError.type !==
                PolicyErrorType.ENTERPRISE_CONNECTORS) {
              // There is not a default learn more URL for EC, and when a custom
              // one is set we show the review button defined above instead.
              item.setExtraButton(
                  ProgressItemState.ERROR, extraButtonText, () => {
                    visitURL(str('DLP_HELP_URL'));
                  });
            }
          }
        }
        break;
      case chrome.fileManagerPrivate.IoTaskState.NEED_PASSWORD:
        // Set state to canceled so notification doesn't display.
        item.state = ProgressItemState.CANCELED;
        break;
      default:
        console.error(`Invalid IoTaskState: ${event.state}`);
    }

    if (!event.showNotification) {
      // Set state to canceled so notification doesn't display.
      item.state = ProgressItemState.CANCELED;
    }

    this.progressCenter_.updateItem(item);
  }
}

/**
 * Obtains ProgressItemType from OperationType of ProgressStatus.type.
 */
function getTypeFromIoTaskType(type: chrome.fileManagerPrivate.IoTaskType):
    ProgressItemType {
  switch (type) {
    case chrome.fileManagerPrivate.IoTaskType.COPY:
      return ProgressItemType.COPY;
    case chrome.fileManagerPrivate.IoTaskType.DELETE:
      return ProgressItemType.DELETE;
    case chrome.fileManagerPrivate.IoTaskType.EMPTY_TRASH:
      return ProgressItemType.EMPTY_TRASH;
    case chrome.fileManagerPrivate.IoTaskType.EXTRACT:
      return ProgressItemType.EXTRACT;
    case chrome.fileManagerPrivate.IoTaskType.MOVE:
      return ProgressItemType.MOVE;
    case chrome.fileManagerPrivate.IoTaskType.RESTORE:
      return ProgressItemType.RESTORE;
    case chrome.fileManagerPrivate.IoTaskType.RESTORE_TO_DESTINATION:
      return ProgressItemType.RESTORE_TO_DESTINATION;
    case chrome.fileManagerPrivate.IoTaskType.TRASH:
      return ProgressItemType.TRASH;
    case chrome.fileManagerPrivate.IoTaskType.ZIP:
      return ProgressItemType.ZIP;
    default:
      console.error('Unknown operation type: ' + type);
      return ProgressItemType.TRANSFER;
  }
}

/**
 * Generate a progress message from the event.
 */
function getMessageFromProgressEvent(
    event: chrome.fileManagerPrivate.ProgressStatus): string {
  // The non-error states text is managed directly in the
  // ProgressCenterPanel.
  if (event.state !== chrome.fileManagerPrivate.IoTaskState.ERROR) {
    return '';
  }
  // TODO(b/295438773): Remove this special case for the "in use" error once
  // the files app error strings are made consistent and an "in use" string is
  // properly added.
  if (event.errorName === 'InUseError' && event.itemCount === 1) {
    switch (event.type) {
      case chrome.fileManagerPrivate.IoTaskType.MOVE:
        return str('MOVE_IN_USE_ERROR');
      case chrome.fileManagerPrivate.IoTaskType.DELETE:
        return str('DELETE_IN_USE_ERROR');
    }
  }
  const detail = getFileErrorString(event.errorName);
  switch (event.type) {
    case chrome.fileManagerPrivate.IoTaskType.COPY:
      return strf('COPY_FILESYSTEM_ERROR', detail);
    case chrome.fileManagerPrivate.IoTaskType.EMPTY_TRASH:
      return str('EMPTY_TRASH_UNEXPECTED_ERROR');
    case chrome.fileManagerPrivate.IoTaskType.EXTRACT:
      return strf('EXTRACT_FILESYSTEM_ERROR', detail);
    case chrome.fileManagerPrivate.IoTaskType.MOVE:
      return strf('MOVE_FILESYSTEM_ERROR', detail);
    case chrome.fileManagerPrivate.IoTaskType.ZIP:
      return strf('ZIP_FILESYSTEM_ERROR', detail);
    case chrome.fileManagerPrivate.IoTaskType.DELETE:
      return str('DELETE_ERROR');
    case chrome.fileManagerPrivate.IoTaskType.RESTORE:
    case chrome.fileManagerPrivate.IoTaskType.RESTORE_TO_DESTINATION:
      return str('RESTORE_FROM_TRASH_ERROR');
    case chrome.fileManagerPrivate.IoTaskType.TRASH:
      return str('TRASH_UNEXPECTED_ERROR');
    default:
      console.warn(`Unexpected operation type: ${event.type}`);
      return str('FILE_ERROR_GENERIC');
  }
}

/**
 * Converts fileManagerPrivate.PolicyErrorType to
 * ProgressCenterItem.PolicyErrorType.
 */
function getPolicyErrorFromIOTaskPolicyError(
    error?: chrome.fileManagerPrivate.PolicyErrorType): PolicyErrorType|null {
  if (!error) {
    return null;
  }
  switch (error) {
    case chrome.fileManagerPrivate.PolicyErrorType.DLP:
      return PolicyErrorType.DLP;
    case chrome.fileManagerPrivate.PolicyErrorType.ENTERPRISE_CONNECTORS:
      return PolicyErrorType.ENTERPRISE_CONNECTORS;
    case chrome.fileManagerPrivate.PolicyErrorType.DLP_WARNING_TIMEOUT:
      return PolicyErrorType.DLP_WARNING_TIMEOUT;
    default:
      console.warn(`Unexpected policy error type: ${error}`);
      return null;
  }
}

/**
 * Returns the extra button text for policy panel items. Currently only
 * supported for PAUSED and ERROR states due to policy, and for COPY or MOVE
 * operation types.
 */
function getPolicyExtraButtonText(
    event: chrome.fileManagerPrivate.ProgressStatus): string {
  if (event.state === chrome.fileManagerPrivate.IoTaskState.PAUSED &&
      event.pauseParams && event.pauseParams.policyParams) {
    if (event.pauseParams.policyParams.policyFileCount > 1 ||
        event.pauseParams.policyParams.alwaysShowReview) {
      return str('DLP_FILES_REVIEW_BUTTON');
    }
    // Single item:
    switch (event.type) {
      case chrome.fileManagerPrivate.IoTaskType.COPY:
        return str('DLP_FILES_COPY_WARN_CONTINUE_BUTTON');
      case chrome.fileManagerPrivate.IoTaskType.MOVE:
      case chrome.fileManagerPrivate.IoTaskType.RESTORE_TO_DESTINATION:
        return str('DLP_FILES_MOVE_WARN_CONTINUE_BUTTON');
      default:
        console.error('Unexpected operation type: ' + event.type);
        return '';
    }
  }
  if (event.state === chrome.fileManagerPrivate.IoTaskState.ERROR &&
      event.policyError) {
    if (event.policyError.type !== PolicyErrorType.DLP_WARNING_TIMEOUT &&
        (event.policyError.policyFileCount > 1 ||
         event.policyError.alwaysShowReview)) {
      return str('DLP_FILES_REVIEW_BUTTON');
    } else {
      return str('LEARN_MORE_LABEL');
    }
  }
  return '';
}