axios#CancelTokenSource TypeScript Examples

The following examples show how to use axios#CancelTokenSource. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: customHooks.ts    From frames with Mozilla Public License 2.0 6 votes vote down vote up
fetcher = <S>(url: string, cancel?: CancelTokenSource) => {
    return new Promise<S>(resolve => {
        axios({
            method: 'GET',
            url,
            cancelToken: cancel?.token
        }).then(res => resolve(res.data))
            .catch(e => {
                if (axios.isCancel(e)) return
                console.log(e)
            })
    })
}
Example #2
Source File: update_lock.ts    From SpaceEye with MIT License 6 votes vote down vote up
/**
     * Generate a new Axios download cancel token.
     *
     * @throws {LockNotHeldError} if lock is no longer held
     * @returns New cancel token
     */
    public generateCancelToken(): CancelTokenSource {
        if (!this.isStillHeld()) {
            throw new LockNotHeldError()
        }
        const token = Axios.CancelToken.source()
        this.downloadCancelTokens.add(token)
        return token
    }
Example #3
Source File: image_downloader.ts    From SpaceEye with MIT License 5 votes vote down vote up
// eslint-disable-next-line jsdoc/require-returns
/**
 * Download a file as a cancellable stream.
 *
 * @param url - URL to download
 * @param destPath - Destination file path to save to
 * @param cancelToken - Cancel token
 * @param onProgress - Called whenever the percentage downloaded value is updated
 */
async function downloadStream(
    url: string,
    destPath: string,
    cancelToken: CancelTokenSource,
    onProgress: (percentage?: number) => void
): Promise<void> {
    return new Promise<void>((resolve, reject) => {
        Axios.get(url, {
            responseType: 'stream',
            cancelToken: cancelToken.token
        })
            .then(({ data, headers }) => {
                const writer = Fs.createWriteStream(destPath)
                const dataStream = data as Readable
                // Get the content size (if available)
                const contentLength = parseInt((headers ?? {})['content-length'] ?? '-1', 10)
                // If none found, signal that the download has begun indeterminately
                if (contentLength === -1) {
                    onProgress(-1)
                } else {
                    // If length found, set up to send percentage updates
                    let lengthDownloaded = 0
                    let previousPercentage = -1
                    // TODO: NOT TYPESAFE!!!!!!!!!
                    dataStream.on('data', chunk => {
                        lengthDownloaded += chunk.length
                        const percentage = Math.round((lengthDownloaded / contentLength) * 100)
                        // Only send an update if a percentage point changed
                        if (percentage !== previousPercentage) {
                            previousPercentage = percentage
                            onProgress(percentage)
                        }
                    })
                }
                dataStream.pipe(writer)
                let isCancelled = false
                cancelToken.token.promise.then(async cancellation => {
                    log.debug('Download canceled for:', url)
                    isCancelled = true
                    writer.destroy()
                    // Delete partially downloaded file
                    if (await existsAsync(destPath)) {
                        log.debug('Deleting partially downloaded file:', destPath)
                        await unlinkAsync(destPath)
                        reject(cancellation)
                    }
                })
                writer.on('close', () => {
                    // Signal that we are done downloading
                    onProgress(undefined)
                    if (!isCancelled) {
                        resolve()
                    }
                })
            })
            .catch(error => {
                reject(error)
            })
    })
}
Example #4
Source File: image_downloader.ts    From SpaceEye with MIT License 5 votes vote down vote up
/**
 * Download an image, canceling any other downloads with the same lock key
 * already in progress.
 *
 * If 2 downloads have the same key and one is already in progress when the
 * other is called, the first will be canceled so the second can begin.
 *
 * If 2+ downloads all have different keys, they will be allowed to download
 * in parallel.
 *
 * @param image - Image to download
 * @param cancelToken - Cancel token for this download
 * @param lock - Active lock on the download pipeline
 * @param onProgress - Called whenever the percentage downloaded value is updated
 * @throws {RequestCancelledError} If request is cancelled
 * @returns Downloaded image information
 */
export async function downloadImage(
    image: ImageSource,
    cancelToken: CancelTokenSource,
    lock: UpdateLock,
    onProgress: (percentage?: number) => void
): Promise<DownloadedImage> {
    // FIXME: Don't leave as hardcoded jpg
    const downloadedImage = new DownloadedImage(image.id, moment.utc(), 'jpg')
    log.debug('Downloading image to:', downloadedImage.getDownloadPath())

    try {
        await downloadStream(image.url, downloadedImage.getDownloadPath(), cancelToken, onProgress)
        lock.destroyCancelToken(cancelToken)
    } catch (error) {
        lock.destroyCancelToken(cancelToken)
        // Throw special error if request is cancelled
        if (Axios.isCancel(error)) {
            log.debug('Download cancelled for image:', image.id)
            throw new RequestCancelledError()
        }
        log.debug('Unknown error while downloading image:', image.id)
        // Rethrow if not
        throw error
    }
    // Sanity check to make sure the image actually exists
    if (await existsAsync(downloadedImage.getDownloadPath())) {
        log.debug('Successfully downloaded image:', image.id)
        await fse.rename(downloadedImage.getDownloadPath(), downloadedImage.getPath())
        return downloadedImage
    }
    // Else, throw an error
    log.debug("Image file doesn't exist at:", downloadedImage.getPath())
    throw new FileDoesNotExistError(
        `Downloaded image "${downloadedImage.getPath()}" does not exist`
    )
}
Example #5
Source File: update_lock.ts    From SpaceEye with MIT License 5 votes vote down vote up
/**
     * Remove reference to a token.
     *
     * @param token - Token to remove
     */
    public destroyCancelToken(token: CancelTokenSource): void {
        this.downloadCancelTokens.delete(token)
    }
Example #6
Source File: update_lock.ts    From SpaceEye with MIT License 5 votes vote down vote up
// Key-mapped download cancel tokens for a lock
    private downloadCancelTokens: Set<CancelTokenSource>
Example #7
Source File: MatrixHttpClient.ts    From beacon-sdk with MIT License 5 votes vote down vote up
private readonly cancelTokenSource: CancelTokenSource
Example #8
Source File: DiscoveryImageForm.tsx    From assisted-ui-lib with Apache License 2.0 5 votes vote down vote up
DiscoveryImageForm: React.FC<DiscoveryImageFormProps> = ({
  cluster,
  onCancel,
  onSuccess,
}) => {
  const { infraEnv, error: infraEnvError, isLoading } = useInfraEnv(cluster.id);
  const cancelSourceRef = React.useRef<CancelTokenSource>();
  const dispatch = useDispatch();
  const ocmPullSecret = usePullSecret();

  React.useEffect(() => {
    cancelSourceRef.current = Axios.CancelToken.source();
    return () => cancelSourceRef.current?.cancel('Image generation cancelled by user.');
  }, []);

  const handleCancel = () => {
    dispatch(forceReload());
    onCancel();
  };

  const handleSubmit = async (
    formValues: DiscoveryImageFormValues,
    formikActions: FormikHelpers<DiscoveryImageFormValues>,
  ) => {
    if (cluster.id && infraEnv?.id) {
      try {
        const { updatedCluster } = await DiscoveryImageFormService.update(
          cluster.id,
          infraEnv.id,
          cluster.kind,
          formValues,
          ocmPullSecret,
        );
        onSuccess();
        dispatch(updateCluster(updatedCluster));
      } catch (error) {
        handleApiError(error, () => {
          formikActions.setStatus({
            error: {
              title: 'Failed to download the discovery Image',
              message: getErrorMessage(error),
            },
          });
        });
      }
    }
  };

  if (isLoading) {
    return <LoadingState></LoadingState>;
  } else if (infraEnvError) {
    return <ErrorState></ErrorState>;
  }

  return (
    <DiscoveryImageConfigForm
      onCancel={handleCancel}
      handleSubmit={handleSubmit}
      sshPublicKey={infraEnv?.sshAuthorizedKey}
      httpProxy={infraEnv?.proxy?.httpProxy}
      httpsProxy={infraEnv?.proxy?.httpsProxy}
      noProxy={infraEnv?.proxy?.noProxy}
      imageType={infraEnv?.type}
    />
  );
}
Example #9
Source File: CrustPinner.tsx    From crust-apps with Apache License 2.0 4 votes vote down vote up
function CrustPinner ({ className, user }: Props): React.ReactElement<Props> {
  const { t } = useTranslation();
  const { queueAction } = useContext<QueueProps>(StatusContext);
  const [isBusy, setBusy] = useState(false);
  const [password, setPassword] = useState('');
  const [cidObject, setCidObject] = useState({ cid: '', prefetchedSize: 0 });
  const [validatingCID, setValidatingCID] = useState(false);
  const [isValidCID, setValidCID] = useState(false);
  const [CIDTips, setCIDTips] = useState({ tips: '', level: 'info' });
  const ipfsApiEndpoint = createIpfsApiEndpoints(t)[0];

  useEffect(() => {
    let cancelTokenSource: CancelTokenSource | null;

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    (async function () {
      const cid = cidObject.cid;

      if (_.isEmpty(cid)) {
        setValidCID(false);
        setCIDTips({ tips: '', level: 'info' });

        return;
      }

      setCIDTips(t('Checking CID...'));
      let isValid = false;

      try {
        const cidObj = CID.parse(cid);

        isValid = CID.asCID(cidObj) != null;
      } catch (error) {
        // eslint-disable-next-line
        console.log(`Invalid CID: ${error.message}`);
      }

      if (!isValid) {
        setValidCID(false);
        setCIDTips({ tips: t('Invalid CID'), level: 'warn' });

        return;
      }

      setCIDTips({ tips: t('Retrieving file size...'), level: 'info' });

      let fileSize = 0;

      try {
        setValidatingCID(true);
        cancelTokenSource = axios.CancelToken.source();
        const res: AxiosResponse = await axios.request({
          cancelToken: cancelTokenSource.token,
          method: 'POST',
          url: `${ipfsApiEndpoint.baseUrl}/api/v0/files/stat?arg=/ipfs/${cid}`,
          timeout: 30000
        });

        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        fileSize = _.get(res, 'data.CumulativeSize', 0);

        if (fileSize > 5 * 1024 * 1024 * 1024) {
          setValidCID(false);
          setCIDTips({ tips: t('File size exceeds 5GB'), level: 'warn' });
        } else if (fileSize > 2 * 1024 * 1024 * 1024) {
          setValidCID(true);
          setCIDTips({ tips: t('Note: File may be oversize for full network capability and performance'), level: 'warn' });
        } else {
          setValidCID(true);
          setCIDTips({ tips: `${t('File Size')}: ${fileSize} Bytes`, level: 'info' });
        }

        setCidObject({ cid, prefetchedSize: fileSize });
        setValidatingCID(false);
        cancelTokenSource = null;
      } catch (error) {
        console.error(error);

        if (axios.isCancel(error)) {
          return;
        }

        fileSize = 2 * 1024 * 1024 * 1024;
        setValidCID(true);
        setCIDTips({ tips: t('Unknown File'), level: 'warn' });
        setValidatingCID(false);
        cancelTokenSource = null;
      }
    })();

    return () => {
      if (cancelTokenSource) {
        cancelTokenSource.cancel('Ipfs api request cancelled');
      }

      cancelTokenSource = null;

      setValidatingCID(false);
      setBusy(false);
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [cidObject.cid]);

  const onChangeCID = useCallback<OnInputChange>((e) => {
    const cid = (e.target.value ?? '').trim();

    setCidObject({ cid, prefetchedSize: 0 });
  }, []);

  const wFiles = useFiles('pins:files');
  const { onChangePinner, pinner, pins } = useAuthPinner();
  const onClickPin = useCallback(async () => {
    try {
      if (!isValidCID || !user.sign || (user.isLocked && !password)) return;
      setBusy(true);
      const prefix = getPerfix(user);
      const msg = user.wallet === 'near' ? user.pubKey || '' : user.account;
      const signature = await user.sign(msg, password);
      const perSignData = user.wallet === 'elrond' ? signature : `${prefix}-${msg}:${signature}`;
      const base64Signature = window.btoa(perSignData);
      const AuthBearer = `Bearer ${base64Signature}`;

      await axios.request({
        data: {
          cid: cidObject.cid
        },
        headers: { Authorization: AuthBearer },
        method: 'POST',
        url: `${pinner.value}/psa/pins`
      });
      const filter = wFiles.files.filter((item) => item.Hash !== cidObject.cid);

      wFiles.setFiles([{
        Hash: cidObject.cid,
        Name: '',
        Size: cidObject.prefetchedSize.toString(),
        UpEndpoint: '',
        PinEndpoint: pinner.value
      }, ...filter]);
      setCidObject({ cid: '', prefetchedSize: 0 });
      setCIDTips({ tips: '', level: 'info' });
      setBusy(false);
    } catch (e) {
      setBusy(false);
      queueAction({
        status: 'error',
        message: t('Error'),
        action: t('Pin')
      });
    }
  }, [isValidCID, pinner, cidObject, queueAction, user, password, wFiles, t]);

  const _onImportResult = useCallback<(m: string, s?: ActionStatusBase['status']) => void>(
    (message, status = 'queued') => {
      queueAction && queueAction({
        action: t('Import files'),
        message,
        status
      });
    },
  [queueAction, t]
  );
  const importInputRef = useRef<HTMLInputElement>(null);
  const _clickImport = useCallback(() => {
    if (!importInputRef.current) return;
    importInputRef.current.click();
  }, [importInputRef]);
  const _onInputImportFile = useCallback<FunInputFile>((e) => {
    try {
      _onImportResult(t('Importing'));
      const fileReader = new FileReader();
      const files = e.target.files;

      if (!files) return;
      fileReader.readAsText(files[0], 'UTF-8');

      if (!(/(.json)$/i.test(e.target.value))) {
        return _onImportResult(t('File error'), 'error');
      }

      fileReader.onload = (e) => {
        const _list = JSON.parse(e.target?.result as string) as SaveFile[];

        if (!Array.isArray(_list)) {
          return _onImportResult(t('File content error'), 'error');
        }

        const fitter: SaveFile[] = [];
        const mapImport: { [key: string]: boolean } = {};

        for (const item of _list) {
          if (item.Hash && item.PinEndpoint) {
            fitter.push(item);
            mapImport[item.Hash] = true;
          }
        }

        const filterOld = wFiles.files.filter((item) => !mapImport[item.Hash]);

        wFiles.setFiles([...fitter, ...filterOld]);
        _onImportResult(t('Import Success'), 'success');
      };
    } catch (e) {
      _onImportResult(t('File content error'), 'error');
    }
  }, [wFiles, _onImportResult, t]);

  const _export = useCallback(() => {
    const blob = new Blob([JSON.stringify(wFiles.files)], { type: 'application/json; charset=utf-8' });

    FileSaver.saveAs(blob, 'pins.json');
  }, [wFiles]);

  return <main className={className}>
    <header>
      <div className='inputPanel'>
        <div className='inputCIDWithTips'>
          <input
            className={'inputCID'}
            onChange={onChangeCID}
            placeholder={t('Enter CID')}
            value={cidObject.cid}
          />
          {CIDTips.tips && <div className='inputCIDTipsContainer'>
            {validatingCID && <MSpinner noLabel />}
            <div className={`inputCIDTips ${CIDTips.level !== 'info' ? 'inputCIDTipsWarn' : ''}`}>
              {CIDTips.tips}
            </div>
          </div>}
        </div>
        <Dropdown
          help={t<string>('Your file will be pinned to IPFS for long-term storage.')}
          isDisabled={true}
          label={t<string>('Select a Web3 IPFS Pinner')}
          onChange={onChangePinner}
          options={pins}
          value={pinner.value}
        />
        {
          user.isLocked &&
          <Password
            help={t<string>('The account\'s password specified at the creation of this account.')}
            isError={false}
            label={t<string>('Password')}
            onChange={setPassword}
            value={password}
          />
        }
        <Button
          className={'btnPin'}
          disable={isBusy || !isValidCID || (user.isLocked && !password)}
          isBuzy={isBusy}
          label={t('Pin')}
          onClick={onClickPin}/>
      </div>
    </header>
    <div className={'importExportPanel'}>
      <input
        onChange={_onInputImportFile}
        ref={importInputRef}
        style={{ display: 'none' }}
        type={'file'}
      />
      <RCButton
        icon={'file-import'}
        label={t('Import')}
        onClick={_clickImport}
      />
      <RCButton
        icon={'file-export'}
        label={t('Export')}
        onClick={_export}
      />
    </div>
    <Table
      empty={t<string>('empty')}
      emptySpinner={t<string>('Loading')}
      header={[
        [t('Pins'), 'start', 2],
        [t('File Size'), 'expand', 2],
        [t('Status'), 'expand'],
        [t('Action'), 'expand'],
        []
      ]}
    >
      {wFiles.files.map((f, index) =>
        <ItemFile key={`files_item-${index}`}>
          <td
            className='start'
            colSpan={2}
          >
            {shortStr(f.Hash)}
            <MCopyButton value={f.Hash}>
              <Badge
                color='highlight'
                hover={t<string>('Copy File CID')}
                icon='copy'
              />
            </MCopyButton>
          </td>
          <td
            className='end'
            colSpan={2}
          >{(_.isEmpty(f.Size) || Number(f.Size) === 0) ? '-' : filesize(Number(f.Size), { round: 2 })}</td>
          <td
            className='end'
            colSpan={1}
          >
            <a
              href={`${window.location.origin}/#/storage_files/status/${f.Hash}`}
              rel='noreferrer'
              target='_blank'
            >{t('View status in Crust')}</a>
          </td>
          <td
            className='end'
            colSpan={1}
          >
            <div className='actions'>
              <Badge
                color='highlight'
                hover={t<string>('Open File')}
                icon='external-link-square-alt'
                onClick={createOnDown(f)}
              />
              <MCopyButton value={createUrl(f)}>
                <Badge
                  color='highlight'
                  hover={t<string>('Copy Download Link')}
                  icon='copy'
                />
              </MCopyButton>

            </div>
          </td>
          <td colSpan={1}/>
        </ItemFile>
      )}
    </Table>
    <div>
      {t('Note: The file list is cached locally, switching browsers or devices will not keep displaying the original browser information.')}
    </div>
  </main>;
}
Example #10
Source File: UploadModal.tsx    From crust-apps with Apache License 2.0 4 votes vote down vote up
function UploadModal (p: Props): React.ReactElement<Props> {
  const { file, onClose = NOOP, onSuccess = NOOP, user } = p;
  const { t } = useTranslation();
  const { endpoint, endpoints, onChangeEndpoint } = useAuthGateway();
  const { onChangePinner, pinner, pins } = useAuthPinner();
  const [password, setPassword] = useState('');
  const [isBusy, setBusy] = useState(false);
  const fileSizeError = useMemo(() => {
    const MAX = 100 * 1024 * 1024;

    if (file.file) {
      return file.file.size > MAX;
    } else if (file.files) {
      let sum = 0;

      for (const f of file.files) {
        sum += f.size;
      }

      return sum > MAX;
    }

    return false;
  }, [file]);
  // const fileSizeError = file.size > 100 * 1024 * 1024;
  const [error, setError] = useState('');
  const errorText = fileSizeError ? t<string>('Do not upload files larger than 100MB!') : error;
  const [upState, setUpState] = useState({ progress: 0, up: false });
  const [cancelUp, setCancelUp] = useState<CancelTokenSource | null>(null);

  const _onClose = useCallback(() => {
    if (cancelUp) cancelUp.cancel();
    onClose();
  }, [cancelUp, onClose]);

  const _onClickUp = useCallback(async () => {
    setError('');

    if (!user.account || !user.sign) {
      return;
    }

    try {
      // 1: sign
      setBusy(true);

      const prefix = getPerfix(user);
      const msg = user.wallet === 'near' ? user.pubKey || '' : user.account;
      const signature = await user.sign(msg, password);
      const perSignData = user.wallet === 'elrond' ? signature : `${prefix}-${msg}:${signature}`;
      const base64Signature = window.btoa(perSignData);
      const AuthBasic = `Basic ${base64Signature}`;
      const AuthBearer = `Bearer ${base64Signature}`;
      // 2: up file
      const cancel = axios.CancelToken.source();

      setCancelUp(cancel);
      setUpState({ progress: 0, up: true });
      const form = new FormData();

      if (file.file) {
        form.append('file', file.file, file.file.name);
      } else if (file.files) {
        for (const f of file.files) {
          form.append('file', f, f.webkitRelativePath);
        }
      }

      const UpEndpoint = endpoint.value;
      const upResult = await axios.request<UploadRes | string>({
        cancelToken: cancel.token,
        data: form,
        headers: { Authorization: AuthBasic },
        maxContentLength: 100 * 1024 * 1024,
        method: 'POST',
        onUploadProgress: (p: { loaded: number, total: number }) => {
          const percent = p.loaded / p.total;

          setUpState({ progress: Math.round(percent * 99), up: true });
        },
        params: { pin: true },
        url: `${UpEndpoint}/api/v0/add`
      });

      let upRes: UploadRes;

      if (typeof upResult.data === 'string') {
        const jsonStr = upResult.data.replaceAll('}\n{', '},{');
        const items = JSON.parse(`[${jsonStr}]`) as UploadRes[];
        const folder = items.length - 1;

        upRes = items[folder];
        delete items[folder];
        upRes.items = items;
      } else {
        upRes = upResult.data;
      }

      console.info('upResult:', upResult);
      setCancelUp(null);
      setUpState({ progress: 100, up: false });
      // remote pin order
      const PinEndpoint = pinner.value;

      await axios.request({
        data: {
          cid: upRes.Hash,
          name: upRes.Name
        },
        headers: { Authorization: AuthBearer },
        method: 'POST',
        url: `${PinEndpoint}/psa/pins`
      });
      onSuccess({
        ...upRes,
        PinEndpoint,
        UpEndpoint
      });
    } catch (e) {
      setUpState({ progress: 0, up: false });
      setBusy(false);
      console.error(e);
      setError(t('Network Error,Please try to switch a Gateway.'));
      // setError((e as Error).message);
    }
  }, [user, file, password, pinner, endpoint, onSuccess, t]);

  return (
    <Modal
      header={t<string>(file.dir ? 'Upload Folder' : 'Upload File')}
      onClose={_onClose}
      open={true}
      size={'large'}
    >
      <Modal.Content>
        <Modal.Columns>
          <div style={{ paddingLeft: '2rem', width: '100%', maxHeight: 300, overflow: 'auto' }}>
            {
              file.file && <ShowFile file={file.file}/>
            }
            {file.files && file.files.map((f, i) =>
              <ShowFile
                file={f}
                key={`file_item:${i}`}/>
            )}
          </div>
        </Modal.Columns>
        <Modal.Columns>
          <Dropdown
            help={t<string>('File streaming and wallet authentication will be processed by the chosen gateway.')}
            isDisabled={isBusy}
            label={t<string>('Select a Web3 IPFS Gateway')}
            onChange={onChangeEndpoint}
            options={endpoints}
            value={endpoint.value}
          />
        </Modal.Columns>
        <Modal.Columns>
          <Dropdown
            help={t<string>('Your file will be pinned to IPFS for long-term storage.')}
            isDisabled={true}
            label={t<string>('Select a Web3 IPFS Pinner')}
            onChange={onChangePinner}
            options={pins}
            value={pinner.value}
          />
        </Modal.Columns>
        <Modal.Columns>
          {
            !upState.up && user.isLocked &&
            <Password
              help={t<string>('The account\'s password specified at the creation of this account.')}
              isError={false}
              label={t<string>('password')}
              onChange={setPassword}
              value={password}
            />
          }
          <Progress
            progress={upState.progress}
            style={{ marginLeft: '2rem', marginTop: '2rem', width: 'calc(100% - 2rem)' }}
          />
          {
            errorText &&
            <div
              style={{
                color: 'orangered',
                padding: '1rem',
                whiteSpace: 'pre-wrap',
                wordBreak: 'break-all'
              }}
            >
              {errorText}
            </div>
          }
        </Modal.Columns>
      </Modal.Content>
      <Modal.Actions onCancel={_onClose}>
        <Button
          icon={'arrow-circle-up'}
          isBusy={isBusy}
          isDisabled={fileSizeError}
          label={t<string>('Sign and Upload')}
          onClick={_onClickUp}
        />
      </Modal.Actions>
    </Modal>
  );
}
Example #11
Source File: TorrentMediainfo.tsx    From flood with GNU General Public License v3.0 4 votes vote down vote up
TorrentMediainfo: FC = () => {
  const {i18n} = useLingui();
  const cancelToken = useRef<CancelTokenSource>(axios.CancelToken.source());
  const clipboardRef = useRef<HTMLInputElement>(null);
  const [mediainfo, setMediainfo] = useState<string | null>(null);
  const [fetchMediainfoError, setFetchMediainfoError] = useState<Error | null>(null);
  const [isFetchingMediainfo, setIsFetchingMediainfo] = useState<boolean>(true);
  const [isCopiedToClipboard, setIsCopiedToClipboard] = useState<boolean>(false);

  useEffect(() => {
    const {current: currentCancelToken} = cancelToken;

    if (UIStore.activeModal?.id === 'torrent-details') {
      TorrentActions.fetchMediainfo(UIStore.activeModal?.hash, cancelToken.current.token).then(
        (fetchedMediainfo) => {
          setMediainfo(fetchedMediainfo.output);
          setIsFetchingMediainfo(false);
        },
        (error) => {
          if (!axios.isCancel(error)) {
            setFetchMediainfoError(error.response.data);
            setIsFetchingMediainfo(false);
          }
        },
      );
    }

    return () => {
      currentCancelToken.cancel();
    };
  }, []);

  let headingMessageId = 'mediainfo.heading';
  if (isFetchingMediainfo) {
    headingMessageId = 'mediainfo.fetching';
  } else if (fetchMediainfoError) {
    headingMessageId = 'mediainfo.execError';
  }

  return (
    <div className="torrent-details__section mediainfo modal__content--nested-scroll__content">
      <div className="mediainfo__toolbar">
        <div className="mediainfo__toolbar__item">
          <span className="torrent-details__table__heading--tertiary">
            <Trans id={headingMessageId} />
          </span>
        </div>
        {mediainfo && (
          <Tooltip
            content={i18n._(isCopiedToClipboard ? 'general.clipboard.copied' : 'general.clipboard.copy')}
            wrapperClassName="tooltip__wrapper mediainfo__toolbar__item"
          >
            <Button
              priority="tertiary"
              onClick={() => {
                if (mediainfo != null) {
                  if (typeof navigator.clipboard?.writeText === 'function') {
                    navigator.clipboard.writeText(mediainfo).then(() => {
                      setIsCopiedToClipboard(true);
                    });
                  } else if (clipboardRef.current != null) {
                    clipboardRef.current.value = mediainfo;
                    clipboardRef.current.select();
                    document.execCommand('copy');
                    setIsCopiedToClipboard(true);
                  }
                }
              }}
            >
              {isCopiedToClipboard ? <Checkmark /> : <Clipboard />}
            </Button>
          </Tooltip>
        )}
      </div>
      <input ref={clipboardRef} style={{width: '0.1px', height: '1px', position: 'absolute', right: 0}} />
      {fetchMediainfoError ? (
        <pre className="mediainfo__output mediainfo__output--error">{fetchMediainfoError.message}</pre>
      ) : (
        <pre className="mediainfo__output">{mediainfo}</pre>
      )}
    </div>
  );
}