Chocobozzz/PeerTube

View on GitHub
client/src/app/+my-account/my-account-import-export/my-account-import.component.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import { DatePipe, NgIf } from '@angular/common'
import { HttpErrorResponse } from '@angular/common/http'
import { Component, Input, OnDestroy, OnInit } from '@angular/core'
import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core'
import { buildHTTPErrorResponse, genericUploadErrorHandler, getUploadXRetryConfig } from '@app/helpers'
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
import { BytesPipe } from '@app/shared/shared-main/common/bytes.pipe'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { HttpStatusCode, UserImport, UserImportState } from '@peertube/peertube-models'
import { UploadState, UploaderX, UploadxService } from 'ngx-uploadx'
import { Subscription } from 'rxjs'
import { UploadProgressComponent } from '../../shared/standalone-upload/upload-progress.component'
import { UserImportExportService } from './user-import-export.service'

@Component({
  selector: 'my-account-import',
  templateUrl: './my-account-import.component.html',
  styleUrls: [ './my-account-import.component.scss' ],
  standalone: true,
  imports: [ NgIf, UploadProgressComponent, NgbTooltip, DatePipe, AlertComponent ]
})
export class MyAccountImportComponent implements OnInit, OnDestroy, CanComponentDeactivate {
  @Input() videoQuotaUsed: number

  uploadingArchive = false
  archiveUploadFinished = false

  error: string
  enableRetryAfterError: boolean
  uploadPercents = 0

  latestImport: UserImport

  private fileToUpload: File
  private uploadServiceSubscription: Subscription
  private alreadyRefreshedToken = false

  constructor (
    private authService: AuthService,
    private server: ServerService,
    private userImportExportService: UserImportExportService,
    private resumableUploadService: UploadxService,
    private notifier: Notifier
  ) {}

  ngOnInit () {
    this.userImportExportService.getLatestImport({ userId: this.authService.getUser().id })
      .subscribe(res => this.latestImport = res)

    this.uploadServiceSubscription = this.resumableUploadService.events
      .subscribe(state => this.onUploadOngoing(state))
  }

  ngOnDestroy () {
    this.resumableUploadService.disconnect()

    if (this.uploadServiceSubscription) this.uploadServiceSubscription.unsubscribe()
  }

  canDeactivate () {
    return {
      canDeactivate: !this.uploadingArchive,
      text: $localize`Your archive file is not uploaded yet, are you sure you want to leave this page?`
    }
  }

  isImportEnabled () {
    return this.server.getHTMLConfig().import.users.enabled
  }

  isEmailEnabled () {
    return this.server.getHTMLConfig().email.enabled
  }

  onUploadOngoing (state: UploadState) {
    switch (state.status) {
      case 'error': {
        if (!this.alreadyRefreshedToken && state.responseStatus === HttpStatusCode.UNAUTHORIZED_401) {
          this.alreadyRefreshedToken = true

          return this.refreshTokenAndRetryUpload()
        }

        this.handleUploadError(buildHTTPErrorResponse(state))
        break
      }

      case 'cancelled':
        this.uploadingArchive = false
        this.uploadPercents = 0

        this.enableRetryAfterError = false
        this.error = ''
        break

      case 'uploading':
        this.uploadPercents = state.progress
        break

      case 'complete':
        this.archiveUploadFinished = true
        this.uploadPercents = 100
        this.uploadingArchive = false

        break
    }
  }

  onFileChange (event: Event | { target: HTMLInputElement }) {
    const inputEl = event.target as HTMLInputElement
    const file = inputEl.files[0]
    if (!file) return

    const user = this.authService.getUser()

    if (user.videoQuota !== -1 && this.videoQuotaUsed + file.size > user.videoQuota) {
      const bytePipes = new BytesPipe()
      const fileSizeBytes = bytePipes.transform(file.size, 0)
      const videoQuotaUsedBytes = bytePipes.transform(this.videoQuotaUsed, 0)
      const videoQuotaBytes = bytePipes.transform(user.videoQuota, 0)

      this.notifier.error(
        // eslint-disable-next-line max-len
        $localize`Cannot import this file as your video quota would be exceeded (import size: ${fileSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
      )

      inputEl.value = ''

      return
    }

    this.fileToUpload = file

    this.uploadFile(file)
  }

  cancelUpload () {
    this.resumableUploadService.control({ action: 'cancel' })
  }

  retryUpload () {
    this.enableRetryAfterError = false
    this.error = ''
    this.uploadFile(this.fileToUpload)
  }

  hasPendingImport () {
    if (!this.latestImport) return false

    const state = this.latestImport.state.id
    return state === UserImportState.PENDING || state === UserImportState.PROCESSING
  }

  private uploadFile (file: File) {
    this.resumableUploadService.handleFiles(file, {
      endpoint: `${UserImportExportService.BASE_USER_IMPORTS_URL}${this.authService.getUser().id}/imports/import-resumable`,
      multiple: false,

      maxChunkSize: this.server.getHTMLConfig().client.videos.resumableUpload.maxChunkSize,

      token: this.authService.getAccessToken(),

      uploaderClass: UploaderX,

      retryConfig: getUploadXRetryConfig(),

      metadata: {
        filename: file.name
      }
    })

    this.uploadingArchive = true
  }

  private handleUploadError (err: HttpErrorResponse) {
    // Reset progress
    this.uploadPercents = 0
    this.enableRetryAfterError = true

    this.error = genericUploadErrorHandler({
      err,
      name: $localize`archive`,
      notifier: this.notifier,
      sticky: false
    })

    if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) {
      this.cancelUpload()
    }
  }

  private refreshTokenAndRetryUpload () {
    this.authService.refreshAccessToken()
      .subscribe(() => this.retryUpload())
  }
}