refactor: handle larger files in importer (#2692)

This commit is contained in:
Aman Harwara
2023-12-11 16:30:31 +05:30
committed by GitHub
parent 63e69b5e4b
commit 82d5a36932
22 changed files with 614 additions and 513 deletions

View File

@@ -15,20 +15,24 @@ import { SimplenoteConverter } from './SimplenoteConverter/SimplenoteConverter'
import { readFileAsText } from './Utils'
import {
DecryptedItemInterface,
DecryptedTransferPayload,
FileItem,
ItemContent,
NoteContent,
NoteMutator,
SNNote,
isNote,
SNTag,
TagContent,
isFile,
} from '@standardnotes/models'
import { HTMLConverter } from './HTMLConverter/HTMLConverter'
import { SuperConverter } from './SuperConverter/SuperConverter'
import { Converter, CreateNoteFn, CreateTagFn } from './Converter'
import { SuperConverterServiceInterface } from '@standardnotes/files'
import { CleanupItemsFn, Converter, InsertNoteFn, InsertTagFn, LinkItemsFn, UploadFileFn } from './Converter'
import { ConversionResult } from './ConversionResult'
import { FilesClientInterface, SuperConverterServiceInterface } from '@standardnotes/files'
import { ContentType } from '@standardnotes/domain-core'
const BytesInOneMegabyte = 1_000_000
const NoteSizeThreshold = 3 * BytesInOneMegabyte
export class Importer {
converters: Set<Converter> = new Set()
@@ -50,9 +54,11 @@ export class Importer {
linkItems(
item: DecryptedItemInterface<ItemContent>,
itemToLink: DecryptedItemInterface<ItemContent>,
sync: boolean,
): Promise<void>
},
private _generateUuid: GenerateUuid,
private files: FilesClientInterface,
) {
this.registerNativeConverters()
}
@@ -88,19 +94,19 @@ export class Importer {
return null
}
createNote: CreateNoteFn = ({
insertNote: InsertNoteFn = async ({
createdAt,
updatedAt,
title,
text,
noteType,
editorIdentifier,
trashed,
archived,
pinned,
trashed = false,
archived = false,
pinned = false,
useSuperIfPossible,
}) => {
if (noteType === NoteType.Super && !this.isEntitledToSuper()) {
if (noteType === NoteType.Super && !this.canUseSuper()) {
noteType = undefined
}
@@ -112,16 +118,17 @@ export class Importer {
editorIdentifier = undefined
}
const shouldUseSuper = useSuperIfPossible && this.isEntitledToSuper()
const shouldUseSuper = useSuperIfPossible && this.canUseSuper()
return {
created_at: createdAt,
created_at_timestamp: createdAt.getTime(),
updated_at: updatedAt,
updated_at_timestamp: updatedAt.getTime(),
uuid: this._generateUuid.execute().getValue(),
content_type: ContentType.TYPES.Note,
content: {
const noteSize = new Blob([text]).size
if (noteSize > NoteSizeThreshold) {
throw new Error('Note is too large to import')
}
const note = this.items.createTemplateItem<NoteContent, SNNote>(
ContentType.TYPES.Note,
{
title,
text,
references: [],
@@ -131,27 +138,68 @@ export class Importer {
pinned,
editorIdentifier: shouldUseSuper ? NativeFeatureIdentifier.TYPES.SuperEditor : editorIdentifier,
},
}
{
created_at: createdAt,
updated_at: updatedAt,
},
)
return await this.mutator.insertItem(note)
}
createTag: CreateTagFn = ({ createdAt, updatedAt, title }) => {
return {
uuid: this._generateUuid.execute().getValue(),
content_type: ContentType.TYPES.Tag,
created_at: createdAt,
created_at_timestamp: createdAt.getTime(),
updated_at: updatedAt,
updated_at_timestamp: updatedAt.getTime(),
content: {
title: title,
insertTag: InsertTagFn = async ({ createdAt, updatedAt, title, references }) => {
const tag = this.items.createTemplateItem<TagContent, SNTag>(
ContentType.TYPES.Tag,
{
title,
expanded: false,
iconString: '',
references: [],
references,
},
{
created_at: createdAt,
updated_at: updatedAt,
},
)
return await this.mutator.insertItem(tag)
}
canUploadFiles = (): boolean => {
const status = this.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Files).getValue(),
)
return status === FeatureStatus.Entitled
}
uploadFile: UploadFileFn = async (file) => {
if (!this.canUploadFiles()) {
return undefined
}
try {
return await this.filesController.uploadNewFile(file, { showToast: true })
} catch (error) {
console.error(error)
return undefined
}
}
isEntitledToSuper = (): boolean => {
linkItems: LinkItemsFn = async (item, itemToLink) => {
await this.linkingController.linkItems(item, itemToLink, false)
}
cleanupItems: CleanupItemsFn = async (items) => {
for (const item of items) {
if (isFile(item)) {
await this.files.deleteFile(item)
}
await this.mutator.deleteItems([item])
}
}
canUseSuper = (): boolean => {
return (
this.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
@@ -160,7 +208,7 @@ export class Importer {
}
convertHTMLToSuper = (html: string): string => {
if (!this.isEntitledToSuper()) {
if (!this.canUseSuper()) {
return html
}
@@ -168,20 +216,23 @@ export class Importer {
}
convertMarkdownToSuper = (markdown: string): string => {
if (!this.isEntitledToSuper()) {
if (!this.canUseSuper()) {
return markdown
}
return this.superConverterService.convertOtherFormatToSuperString(markdown, 'md')
}
async getPayloadsFromFile(file: File, type: string): Promise<DecryptedTransferPayload[]> {
const isEntitledToSuper = this.isEntitledToSuper()
async importFromFile(file: File, type: string): Promise<ConversionResult> {
const canUseSuper = this.canUseSuper()
if (type === 'super' && !isEntitledToSuper) {
if (type === 'super' && !canUseSuper) {
throw new Error('Importing Super notes requires a subscription')
}
const successful: ConversionResult['successful'] = []
const errored: ConversionResult['errored'] = []
for (const converter of this.converters) {
const isCorrectType = converter.getImportType() === type
@@ -195,65 +246,28 @@ export class Importer {
throw new Error('Content is not valid')
}
return await converter.convert(file, {
createNote: this.createNote,
createTag: this.createTag,
canUseSuper: isEntitledToSuper,
const result = await converter.convert(file, {
insertNote: this.insertNote,
insertTag: this.insertTag,
canUploadFiles: this.canUploadFiles(),
uploadFile: this.uploadFile,
canUseSuper,
convertHTMLToSuper: this.convertHTMLToSuper,
convertMarkdownToSuper: this.convertMarkdownToSuper,
readFileAsText,
linkItems: this.linkItems,
cleanupItems: this.cleanupItems,
})
successful.push(...result.successful)
errored.push(...result.errored)
break
}
return []
}
async importFromTransferPayloads(payloads: DecryptedTransferPayload[]) {
const insertedItems = await Promise.all(
payloads.map(async (payload) => {
const content = payload.content as NoteContent
const note = this.items.createTemplateItem(
payload.content_type,
{
text: content.text,
title: content.title,
noteType: content.noteType,
editorIdentifier: content.editorIdentifier,
references: content.references,
},
{
created_at: payload.created_at,
updated_at: payload.updated_at,
uuid: payload.uuid,
},
)
return this.mutator.insertItem(note)
}),
)
return insertedItems
}
async uploadAndReplaceInlineFilesInInsertedItems(insertedItems: DecryptedItemInterface<ItemContent>[]) {
for (const item of insertedItems) {
if (!isNote(item)) {
continue
}
if (item.noteType !== NoteType.Super) {
continue
}
try {
const text = await this.superConverterService.uploadAndReplaceInlineFilesInSuperString(
item.text,
async (file) => await this.filesController.uploadNewFile(file, { showToast: true, note: item }),
async (file) => await this.linkingController.linkItems(item, file),
this._generateUuid,
)
await this.mutator.changeItem<NoteMutator>(item, (mutator) => {
mutator.text = text
})
} catch (error) {
console.error(error)
}
return {
successful,
errored,
}
}
}