chromium/ui/file_manager/file_manager/background/js/mock_volume_manager.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 {assert} from 'chrome://resources/js/assert.js';

import {isComputersRoot, isFakeEntry, isSameEntry, isTeamDriveRoot} from '../../common/js/entry_utils.js';
import type {FilesAppEntry} from '../../common/js/files_app_entry_types.js';
import {type MockEntry, MockFileSystem} from '../../common/js/mock_entry.js';
import {str} from '../../common/js/translations.js';
import {FileSystemType, getRootTypeFromVolumeType, RootType, Source, VolumeType} from '../../common/js/volume_manager_types.js';

import {EntryLocation} from './entry_location_impl.js';
import {VolumeInfo} from './volume_info.js';
import {VolumeInfoList} from './volume_info_list.js';
import {VolumeManager} from './volume_manager.js';
import {volumeManagerFactory} from './volume_manager_factory.js';

export const fakeMyFilesVolumeId = VolumeType.DOWNLOADS + ':test_mount_path';
export const fakeDriveVolumeId = VolumeType.DRIVE + ':test_mount_path';

let volumeManagerInstance: VolumeManager|null = null;

/**
 * Mock class for VolumeManager.
 */
export class MockVolumeManager extends VolumeManager {
  override volumeInfoList = new VolumeInfoList();
  driveConnectionState: chrome.fileManagerPrivate.DriveConnectionState = {
    type: chrome.fileManagerPrivate.DriveConnectionStateType.ONLINE,
  };

  constructor() {
    super();
    // Create Drive.   Drive attempts to resolve FilesSystemURLs for '/root',
    // '/team_drives' and '/Computers' during initialization. Create a
    // filesystem with those entries now, and mock
    // webkitResolveLocalFileSystemURL.
    const driveFs = new MockFileSystem(
        fakeDriveVolumeId, 'filesystem:' + fakeDriveVolumeId);
    driveFs.populate(['/root/', '/team_drives/', '/Computers/']);

    // Mock window.webkitResolve to return entries.
    const orig = window.webkitResolveLocalFileSystemURL;
    window.webkitResolveLocalFileSystemURL = (url, success) => {
      const rootURL = `filesystem:${fakeDriveVolumeId}`;
      const match = url.match(new RegExp(`^${rootURL}(\/.*)`));
      if (match) {
        const path = match[1]!;
        const entry = driveFs.entries[path];
        if (entry) {
          return setTimeout(success, 0, entry);
        }
      }
      throw new DOMException('Unknown drive url: ' + url, 'NotFoundError');
    };

    // Create Drive, swap entries back in, revert window.webkitResolve.
    const drive = this.createVolumeInfo(
        VolumeType.DRIVE, fakeDriveVolumeId, str('DRIVE_DIRECTORY_LABEL'));
    (drive.fileSystem as MockFileSystem)
        .populate(Object.values(driveFs.entries));
    window.webkitResolveLocalFileSystemURL = orig;

    // Create Downloads.
    this.createVolumeInfo(
        VolumeType.DOWNLOADS, fakeMyFilesVolumeId,
        str('DOWNLOADS_DIRECTORY_LABEL'));
  }

  override getFuseBoxOnlyFilterEnabled() {
    return false;
  }

  override getMediaStoreFilesOnlyFilterEnabled() {
    return false;
  }

  override dispose() {}

  /**
   * Replaces the VolumeManager singleton with a MockVolumeManager.
   */
  static installMockSingleton(singleton?: MockVolumeManager) {
    volumeManagerInstance = singleton || new MockVolumeManager();

    volumeManagerFactory.getInstance = async () => {
      assert(volumeManagerInstance);
      return volumeManagerInstance;
    };
  }

  /**
   * Creates, installs and returns a mock VolumeInfo instance.
   */
  createVolumeInfo(
      type: VolumeType, volumeId: string, label: string, providerId?: string,
      remoteMountPath?: string): VolumeInfo {
    const volumeInfo = MockVolumeManager.createMockVolumeInfo(
        type, volumeId, label, undefined, providerId, remoteMountPath);
    this.volumeInfoList.add(volumeInfo);
    return volumeInfo;
  }

  /**
   * Obtains location information from an entry.
   * Current implementation can handle only fake entries.
   *
   * @param entry A fake entry.
   * @return Location information.
   */
  override getLocationInfo(entry: Entry|FilesAppEntry): EntryLocation|null {
    if (isFakeEntry(entry)) {
      const isReadOnly = entry.rootType !== RootType.RECENT &&
          entry.rootType !== RootType.TRASH;
      return new EntryLocation(
          this.volumeInfoList.item(0), entry.rootType!, /* isRootType= */ true,
          isReadOnly);
    }

    if (entry.filesystem?.name === fakeDriveVolumeId) {
      const volumeInfo = this.volumeInfoList.item(0);
      let rootType = RootType.DRIVE;
      let isRootEntry = entry.fullPath === '/root';
      if (entry.fullPath.startsWith('/team_drives')) {
        if (entry.fullPath === '/team_drives') {
          rootType = RootType.SHARED_DRIVES_GRAND_ROOT;
          isRootEntry = true;
        } else {
          rootType = RootType.SHARED_DRIVE;
          isRootEntry = isTeamDriveRoot(entry);
        }
      } else if (entry.fullPath.startsWith('/Computers')) {
        if (entry.fullPath === '/Computers') {
          rootType = RootType.COMPUTERS_GRAND_ROOT;
          isRootEntry = true;
        } else {
          rootType = RootType.COMPUTER;
          isRootEntry = isComputersRoot(entry);
        }
      } else if (/^\/\.(files|shortcut-targets)-by-id/.test(entry.fullPath)) {
        rootType = RootType.DRIVE_SHARED_WITH_ME;
      }
      return new EntryLocation(volumeInfo, rootType, isRootEntry, true);
    }

    const volumeInfo = this.getVolumeInfo(entry);
    // For filtered out volumes, its volume info won't exist in the volume info
    // list.
    if (!volumeInfo) {
      return null;
    }
    assert(volumeInfo.volumeType);
    const rootType = getRootTypeFromVolumeType(volumeInfo.volumeType);
    const isRootEntry = isSameEntry(entry, volumeInfo.fileSystem.root);
    return new EntryLocation(volumeInfo, rootType, isRootEntry, false);
  }

  /**
   * @param volumeType Volume type.
   * @return Volume info.
   */
  override getCurrentProfileVolumeInfo(volumeType: VolumeType): null
      |VolumeInfo {
    for (let i = 0; i < this.volumeInfoList.length; i++) {
      const volumeInfo = this.volumeInfoList.item(i);
      if (volumeInfo.profile.isCurrentProfile &&
          volumeInfo.volumeType === volumeType) {
        return volumeInfo;
      }
    }
    return null;
  }

  /**
   * @return Current drive connection state.
   */
  override getDriveConnectionState():
      chrome.fileManagerPrivate.DriveConnectionState {
    return this.driveConnectionState;
  }

  /**
   * Utility function to create a mock VolumeInfo.
   * @param type Volume type.
   * @param volumeId Volume id.
   * @param label Label.
   * @param devicePath Device path.
   * @param providerId Provider id.
   * @param remoteMountPath Remote mount path.
   * @return Created mock
   *     VolumeInfo.
   */
  static createMockVolumeInfo(
      type: VolumeType, volumeId: string, label?: string, devicePath?: string,
      providerId?: string, remoteMountPath?: string): VolumeInfo {
    const fileSystem = new MockFileSystem(volumeId, 'filesystem:' + volumeId);

    let diskFileSystemType = FileSystemType.UNKNOWN;
    if (devicePath && devicePath.startsWith('fusebox')) {
      diskFileSystemType = 'fusebox' as FileSystemType;
    }

    let source = Source.NETWORK;
    if (type === VolumeType.ARCHIVE) {
      source = Source.FILE;
    } else if (type === VolumeType.REMOVABLE) {
      source = Source.DEVICE;
    }

    // If there's no label set it to volumeId to make it shorter to write
    // tests.
    const volumeInfo = new VolumeInfo(
        type,
        volumeId,
        fileSystem,
        '',                                         // error
        '',                                         // deviceType
        devicePath || '',                           // devicePath
        false,                                      // isReadOnly
        false,                                      // isReadOnlyRemovableDevice
        {isCurrentProfile: true, displayName: ''},  // profile
        label || volumeId,                          // label
        providerId,                                 // providerId
        false,                                      // configurable
        false,                                      // watchable
        source,                                     // source
        diskFileSystemType,                         // diskFileSystemType
        {icon16x16Url: '', icon32x32Url: ''},       // iconSet
        '',                                         // driveLabel
        remoteMountPath,                            // remoteMountPath
        undefined,                                  // vmType
    );


    return volumeInfo;
  }

  override async mountArchive(_fileUrl: string, _password?: string):
      Promise<VolumeInfo> {
    throw new Error('Not implemented');
  }

  override async cancelMounting(_fileUrl: string): Promise<void> {
    throw new Error('Not implemented');
  }

  override async unmount(_volumeInfo: VolumeInfo): Promise<void> {
    throw new Error('Not implemented');
  }

  override async configure(_volumeInfo: VolumeInfo): Promise<void> {
    throw new Error('Not implemented');
  }

  override hasDisabledVolumes(): boolean {
    return false;
  }

  override isDisabled(_volume: VolumeType): boolean {
    return false;
  }


  override isAllowedVolume(_volumeInfo: VolumeInfo): boolean {
    return true;
  }

  /**
   * Used to window.webkitResolveLocalFileSystemURL for testing. This
   * emulates the real function by parsing `url` and finding the matching entry
   * in `volumeManager`. E.g. filesystem:downloads:test_mount_path/dir/file.txt
   * will look up the volume with ID 'downloads:test_mount_path' for
   * /dir/file.txt.
   *
   * @param volumeManager VolumeManager to resolve URLs with.
   * @param url URL to resolve.
   * @param successCallback Success callback.
   * @param errorCallback Error callback.
   */
  static resolveLocalFileSystemUrl(
      volumeManager: VolumeManager, url: string,
      successCallback: (arg0: MockEntry) => void,
      errorCallback?: (arg0: FileError) => void) {
    const match = url.match(/^filesystem:(\w+):\w+(\/.*)/);
    if (match) {
      const volumeType = match[1]! as VolumeType;
      let path = match[2]!;
      const volume = volumeManager.getCurrentProfileVolumeInfo(volumeType);
      if (volume) {
        // Decode URI in file paths.
        path = path.split('/').map(decodeURIComponent).join('/');
        const entry = (volume.fileSystem as MockFileSystem).entries[path];
        if (entry) {
          setTimeout(successCallback, 0, entry);
          return;
        }
      }
    }
    const message =
        `MockVolumeManager.resolveLocalFileSystemUrl not found: ${url}`;
    console.warn(message);
    const error = new DOMException(message, 'NotFoundError');
    if (errorCallback) {
      setTimeout(errorCallback, 0, error);
    } else {
      throw error;
    }
  }
}