Files
standardnotes-app-web/app/assets/javascripts/UIModels/AppState/FilesState.ts
2022-04-22 21:58:10 +05:30

218 lines
6.2 KiB
TypeScript

import { BYTES_IN_ONE_MEGABYTE } from '@/Constants'
import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
import {
ClassicFileReader,
StreamingFileReader,
StreamingFileSaver,
ClassicFileSaver,
parseFileName,
} from '@standardnotes/filepicker'
import {
ApplicationEvent,
ClientDisplayableError,
ContentType,
SNFile,
SubscriptionSettingName,
} from '@standardnotes/snjs'
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit'
import { action, makeObservable, observable } from 'mobx'
import { WebApplication } from '../Application'
export class FilesState {
filesQuotaUsed: number
filesQuotaTotal: number
constructor(private application: WebApplication, appObservers: (() => void)[]) {
this.filesQuotaUsed = 0
this.filesQuotaTotal = 0
makeObservable(this, {
filesQuotaUsed: observable,
filesQuotaTotal: observable,
getFilesQuota: action,
})
appObservers.push(
application.addEventObserver(async (event) => {
switch (event) {
case ApplicationEvent.Launched:
case ApplicationEvent.SignedIn:
this.getFilesQuota().catch(console.error)
}
}),
application.streamItems(ContentType.File, () => {
this.getFilesQuota().catch(console.error)
}),
)
}
public async getFilesQuota() {
const filesQuotaUsed = await this.application.settings.getSubscriptionSetting(
SubscriptionSettingName.FileUploadBytesUsed,
)
const filesQuotaTotal = await this.application.settings.getSubscriptionSetting(
SubscriptionSettingName.FileUploadBytesLimit,
)
if (filesQuotaUsed) {
this.filesQuotaUsed = parseFloat(filesQuotaUsed)
}
if (filesQuotaTotal) {
this.filesQuotaTotal = parseFloat(filesQuotaTotal)
}
}
public async downloadFile(file: SNFile): Promise<void> {
let downloadingToastId = ''
try {
const saver = StreamingFileSaver.available()
? new StreamingFileSaver(file.name)
: new ClassicFileSaver()
const isUsingStreamingSaver = saver instanceof StreamingFileSaver
if (isUsingStreamingSaver) {
await saver.selectFileToSaveTo()
}
downloadingToastId = addToast({
type: ToastType.Loading,
message: 'Downloading file...',
})
const decryptedBytesArray: Uint8Array[] = []
await this.application.files.downloadFile(file, async (decryptedBytes: Uint8Array) => {
if (isUsingStreamingSaver) {
await saver.pushBytes(decryptedBytes)
} else {
decryptedBytesArray.push(decryptedBytes)
}
})
if (isUsingStreamingSaver) {
await saver.finish()
} else {
const finalBytes = concatenateUint8Arrays(decryptedBytesArray)
saver.saveFile(file.name, finalBytes)
}
addToast({
type: ToastType.Success,
message: 'Successfully downloaded file',
})
} catch (error) {
console.error(error)
addToast({
type: ToastType.Error,
message: 'There was an error while downloading the file',
})
}
if (downloadingToastId.length > 0) {
dismissToast(downloadingToastId)
}
}
public async uploadNewFile(fileOrHandle?: File | FileSystemFileHandle) {
let toastId = ''
try {
const minimumChunkSize = this.application.files.minimumChunkSize()
const shouldUseStreamingReader = StreamingFileReader.available()
const picker = shouldUseStreamingReader ? StreamingFileReader : ClassicFileReader
const maxFileSize = picker.maximumFileSize()
const selectedFiles =
fileOrHandle instanceof File
? [fileOrHandle]
: StreamingFileReader.available() && fileOrHandle instanceof FileSystemFileHandle
? await StreamingFileReader.getFilesFromHandles([fileOrHandle])
: await picker.selectFiles()
if (selectedFiles.length === 0) {
return
}
const uploadedFiles: SNFile[] = []
for (const file of selectedFiles) {
if (!shouldUseStreamingReader && maxFileSize && file.size >= maxFileSize) {
this.application.alertService
.alert(
`This file exceeds the limits supported in this browser. To upload files greater than ${
maxFileSize / BYTES_IN_ONE_MEGABYTE
}MB, please use the desktop application or the Chrome browser.`,
`Cannot upload file "${file.name}"`,
)
.catch(console.error)
continue
}
toastId = addToast({
type: ToastType.Loading,
message: `Uploading file "${file.name}"...`,
})
const operation = await this.application.files.beginNewFileUpload()
if (operation instanceof ClientDisplayableError) {
addToast({
type: ToastType.Error,
message: 'Unable to start upload session',
})
throw new Error('Unable to start upload session')
}
const onChunk = async (chunk: Uint8Array, index: number, isLast: boolean) => {
await this.application.files.pushBytesForUpload(operation, chunk, index, isLast)
}
const fileResult = await picker.readFile(file, minimumChunkSize, onChunk)
if (!fileResult.mimeType) {
const { ext } = parseFileName(file.name)
fileResult.mimeType = await this.application.getArchiveService().getMimeType(ext)
}
const uploadedFile = await this.application.files.finishUpload(operation, fileResult)
if (uploadedFile instanceof ClientDisplayableError) {
addToast({
type: ToastType.Error,
message: 'Unable to close upload session',
})
throw new Error('Unable to close upload session')
}
uploadedFiles.push(uploadedFile)
dismissToast(toastId)
addToast({
type: ToastType.Success,
message: `Uploaded file "${uploadedFile.name}"`,
})
}
return uploadedFiles
} catch (error) {
console.error(error)
if (toastId.length > 0) {
dismissToast(toastId)
}
addToast({
type: ToastType.Error,
message: 'There was an error while uploading the file',
})
}
return undefined
}
}