feat: Added "Import" option in the account menu that allows you to import from plaintext/markdown files, Evernote exports, Simplenote exports, Google Keep exports and also convert Aegis exports to TokenVault notes
This commit is contained in:
@@ -3,7 +3,6 @@ import { ContentType } from '@standardnotes/common'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { WebApplicationInterface } from '@standardnotes/services'
|
||||
import { Importer } from '../Importer'
|
||||
|
||||
type AegisData = {
|
||||
db: {
|
||||
@@ -18,6 +17,8 @@ type AegisData = {
|
||||
}
|
||||
}
|
||||
|
||||
const AegisEntryTypes = ['hotp', 'totp', 'steam', 'yandex'] as const
|
||||
|
||||
type AuthenticatorEntry = {
|
||||
service: string
|
||||
account: string
|
||||
@@ -25,9 +26,26 @@ type AuthenticatorEntry = {
|
||||
notes: string
|
||||
}
|
||||
|
||||
export class AegisToAuthenticatorConverter extends Importer {
|
||||
constructor(protected override application: WebApplicationInterface) {
|
||||
super(application)
|
||||
export class AegisToAuthenticatorConverter {
|
||||
constructor(protected application: WebApplicationInterface) {}
|
||||
|
||||
static isValidAegisJson(json: any): boolean {
|
||||
return json.db && json.db.entries && json.db.entries.every((entry: any) => AegisEntryTypes.includes(entry.type))
|
||||
}
|
||||
|
||||
async convertAegisBackupFileToNote(
|
||||
file: File,
|
||||
addEditorInfo: boolean,
|
||||
): Promise<DecryptedTransferPayload<NoteContent>> {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const entries = this.parseEntries(content)
|
||||
|
||||
if (!entries) {
|
||||
throw new Error('Could not parse entries')
|
||||
}
|
||||
|
||||
return this.createNoteFromEntries(entries, file, addEditorInfo)
|
||||
}
|
||||
|
||||
createNoteFromEntries(
|
||||
@@ -57,21 +75,6 @@ export class AegisToAuthenticatorConverter extends Importer {
|
||||
}
|
||||
}
|
||||
|
||||
async convertAegisBackupFileToNote(
|
||||
file: File,
|
||||
addEditorInfo: boolean,
|
||||
): Promise<DecryptedTransferPayload<NoteContent>> {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const entries = this.parseEntries(content)
|
||||
|
||||
if (!entries) {
|
||||
throw new Error('Could not parse entries')
|
||||
}
|
||||
|
||||
return this.createNoteFromEntries(entries, file, addEditorInfo)
|
||||
}
|
||||
|
||||
parseEntries(data: string): AuthenticatorEntry[] | null {
|
||||
try {
|
||||
const json = JSON.parse(data) as AegisData
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ContentType } from '@standardnotes/common'
|
||||
import { WebApplicationInterface } from '@standardnotes/services'
|
||||
import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { Importer } from '../Importer'
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
@@ -11,10 +10,8 @@ dayjs.extend(utc)
|
||||
|
||||
const dateFormat = 'YYYYMMDDTHHmmss'
|
||||
|
||||
export class EvernoteConverter extends Importer {
|
||||
constructor(protected override application: WebApplicationInterface) {
|
||||
super(application)
|
||||
}
|
||||
export class EvernoteConverter {
|
||||
constructor(protected application: WebApplicationInterface) {}
|
||||
|
||||
async convertENEXFileToNotesAndTags(file: File, stripHTML: boolean): Promise<DecryptedTransferPayload[]> {
|
||||
const content = await readFileAsText(file)
|
||||
@@ -138,6 +135,9 @@ export class EvernoteConverter extends Importer {
|
||||
}
|
||||
|
||||
const allItems: DecryptedTransferPayload[] = [...notes, ...tags]
|
||||
if (allItems.length === 0) {
|
||||
throw new Error('Could not parse any notes or tags from Evernote file.')
|
||||
}
|
||||
if (defaultTag) {
|
||||
allItems.push(defaultTag)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { WebApplicationInterface } from '@standardnotes/services'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { Importer } from '../Importer'
|
||||
|
||||
type GoogleKeepJsonNote = {
|
||||
color: string
|
||||
@@ -14,10 +13,8 @@ type GoogleKeepJsonNote = {
|
||||
userEditedTimestampUsec: number
|
||||
}
|
||||
|
||||
export class GoogleKeepConverter extends Importer {
|
||||
constructor(protected override application: WebApplicationInterface) {
|
||||
super(application)
|
||||
}
|
||||
export class GoogleKeepConverter {
|
||||
constructor(protected application: WebApplicationInterface) {}
|
||||
|
||||
async convertGoogleKeepBackupFileToNote(
|
||||
file: File,
|
||||
@@ -99,9 +96,24 @@ export class GoogleKeepConverter extends Importer {
|
||||
return
|
||||
}
|
||||
|
||||
static isValidGoogleKeepJson(json: any): boolean {
|
||||
return (
|
||||
json.title &&
|
||||
json.textContent &&
|
||||
json.userEditedTimestampUsec &&
|
||||
typeof json.isArchived === 'boolean' &&
|
||||
typeof json.isTrashed === 'boolean' &&
|
||||
typeof json.isPinned === 'boolean' &&
|
||||
json.color
|
||||
)
|
||||
}
|
||||
|
||||
tryParseAsJson(data: string): DecryptedTransferPayload<NoteContent> | null {
|
||||
try {
|
||||
const parsed = JSON.parse(data) as GoogleKeepJsonNote
|
||||
if (!GoogleKeepConverter.isValidGoogleKeepJson(parsed)) {
|
||||
return null
|
||||
}
|
||||
const date = new Date(parsed.userEditedTimestampUsec / 1000)
|
||||
return {
|
||||
created_at: date,
|
||||
|
||||
@@ -1,14 +1,91 @@
|
||||
import { WebApplicationInterface } from '@standardnotes/services'
|
||||
import { DecryptedTransferPayload } from '@standardnotes/snjs'
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { FeatureStatus, WebApplicationInterface } from '@standardnotes/services'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter'
|
||||
import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter'
|
||||
import { GoogleKeepConverter } from './GoogleKeepConverter/GoogleKeepConverter'
|
||||
import { PlaintextConverter } from './PlaintextConverter/PlaintextConverter'
|
||||
import { SimplenoteConverter } from './SimplenoteConverter/SimplenoteConverter'
|
||||
import { readFileAsText } from './Utils'
|
||||
import { DecryptedTransferPayload } from '@standardnotes/models'
|
||||
|
||||
export type NoteImportType = 'plaintext' | 'evernote' | 'google-keep' | 'simplenote' | 'aegis'
|
||||
|
||||
export class Importer {
|
||||
constructor(protected application: WebApplicationInterface) {}
|
||||
aegisConverter: AegisToAuthenticatorConverter
|
||||
googleKeepConverter: GoogleKeepConverter
|
||||
simplenoteConverter: SimplenoteConverter
|
||||
plaintextConverter: PlaintextConverter
|
||||
evernoteConverter: EvernoteConverter
|
||||
|
||||
async importFromTransferPayloads(payloads: DecryptedTransferPayload[]): Promise<void> {
|
||||
for (const payload of payloads) {
|
||||
const itemPayload = this.application.items.createPayloadFromObject(payload)
|
||||
const item = this.application.items.createItemFromPayload(itemPayload)
|
||||
await this.application.mutator.insertItem(item)
|
||||
constructor(protected application: WebApplicationInterface) {
|
||||
this.aegisConverter = new AegisToAuthenticatorConverter(application)
|
||||
this.googleKeepConverter = new GoogleKeepConverter(application)
|
||||
this.simplenoteConverter = new SimplenoteConverter(application)
|
||||
this.plaintextConverter = new PlaintextConverter(application)
|
||||
this.evernoteConverter = new EvernoteConverter(application)
|
||||
}
|
||||
|
||||
static detectService = async (file: File): Promise<NoteImportType | null> => {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const { ext } = parseFileName(file.name)
|
||||
|
||||
if (ext === 'enex') {
|
||||
return 'evernote'
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(content)
|
||||
|
||||
if (AegisToAuthenticatorConverter.isValidAegisJson(json)) {
|
||||
return 'aegis'
|
||||
}
|
||||
|
||||
if (GoogleKeepConverter.isValidGoogleKeepJson(json)) {
|
||||
return 'google-keep'
|
||||
}
|
||||
|
||||
if (SimplenoteConverter.isValidSimplenoteJson(json)) {
|
||||
return 'simplenote'
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
if (PlaintextConverter.isValidPlaintextFile(file)) {
|
||||
return 'plaintext'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async getPayloadsFromFile(file: File, type: NoteImportType): Promise<DecryptedTransferPayload[]> {
|
||||
if (type === 'aegis') {
|
||||
const isEntitledToAuthenticator =
|
||||
this.application.features.getFeatureStatus(FeatureIdentifier.TokenVaultEditor) === FeatureStatus.Entitled
|
||||
return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)]
|
||||
} else if (type === 'google-keep') {
|
||||
return [await this.googleKeepConverter.convertGoogleKeepBackupFileToNote(file, true)]
|
||||
} else if (type === 'simplenote') {
|
||||
return await this.simplenoteConverter.convertSimplenoteBackupFileToNotes(file)
|
||||
} else if (type === 'evernote') {
|
||||
return await this.evernoteConverter.convertENEXFileToNotesAndTags(file, false)
|
||||
} else if (type === 'plaintext') {
|
||||
return [await this.plaintextConverter.convertPlaintextFileToNote(file)]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async importFromTransferPayloads(payloads: DecryptedTransferPayload[]) {
|
||||
const insertedItems = await Promise.all(
|
||||
payloads.map(async (payload) => {
|
||||
const itemPayload = this.application.items.createPayloadFromObject(payload)
|
||||
const item = this.application.items.createItemFromPayload(itemPayload)
|
||||
return this.application.mutator.insertItem(item)
|
||||
}),
|
||||
)
|
||||
return insertedItems
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { WebApplicationInterface } from '@standardnotes/services'
|
||||
import { readFileAsText } from '../Utils'
|
||||
|
||||
export class PlaintextConverter {
|
||||
constructor(protected application: WebApplicationInterface) {}
|
||||
|
||||
static isValidPlaintextFile(file: File): boolean {
|
||||
return file.type === 'text/plain' || file.type === 'text/markdown'
|
||||
}
|
||||
|
||||
async convertPlaintextFileToNote(file: File): Promise<DecryptedTransferPayload<NoteContent>> {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const { name } = parseFileName(file.name)
|
||||
|
||||
const createdAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
const updatedAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
|
||||
|
||||
return {
|
||||
created_at: createdAtDate,
|
||||
created_at_timestamp: createdAtDate.getTime(),
|
||||
updated_at: updatedAtDate,
|
||||
updated_at_timestamp: updatedAtDate.getTime(),
|
||||
uuid: this.application.generateUUID(),
|
||||
content_type: ContentType.Note,
|
||||
content: {
|
||||
title: name,
|
||||
text: content,
|
||||
references: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { readFileAsText } from '../Utils'
|
||||
import { WebApplicationInterface } from '@standardnotes/services'
|
||||
import { Importer } from '../Importer'
|
||||
|
||||
type SimplenoteItem = {
|
||||
creationDate: string
|
||||
@@ -15,9 +14,28 @@ type SimplenoteData = {
|
||||
trashedNotes: SimplenoteItem[]
|
||||
}
|
||||
|
||||
export class SimplenoteConverter extends Importer {
|
||||
constructor(protected override application: WebApplicationInterface) {
|
||||
super(application)
|
||||
const isSimplenoteEntry = (entry: any): boolean => entry.id && entry.content && entry.creationDate && entry.lastModified
|
||||
|
||||
export class SimplenoteConverter {
|
||||
constructor(protected application: WebApplicationInterface) {}
|
||||
|
||||
static isValidSimplenoteJson(json: any): boolean {
|
||||
return (
|
||||
(json.activeNotes && json.activeNotes.every(isSimplenoteEntry)) ||
|
||||
(json.trashedNotes && json.trashedNotes.every(isSimplenoteEntry))
|
||||
)
|
||||
}
|
||||
|
||||
async convertSimplenoteBackupFileToNotes(file: File): Promise<DecryptedTransferPayload<NoteContent>[]> {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const notes = this.parse(content)
|
||||
|
||||
if (!notes) {
|
||||
throw new Error('Could not parse notes')
|
||||
}
|
||||
|
||||
return notes
|
||||
}
|
||||
|
||||
createNoteFromItem(item: SimplenoteItem, trashed: boolean): DecryptedTransferPayload<NoteContent> {
|
||||
@@ -51,18 +69,6 @@ export class SimplenoteConverter extends Importer {
|
||||
}
|
||||
}
|
||||
|
||||
async convertSimplenoteBackupFileToNotes(file: File): Promise<DecryptedTransferPayload<NoteContent>[]> {
|
||||
const content = await readFileAsText(file)
|
||||
|
||||
const notes = this.parse(content)
|
||||
|
||||
if (!notes) {
|
||||
throw new Error('Could not parse notes')
|
||||
}
|
||||
|
||||
return notes
|
||||
}
|
||||
|
||||
parse(data: string) {
|
||||
try {
|
||||
const parsed = JSON.parse(data) as SimplenoteData
|
||||
|
||||
7
packages/ui-services/src/Import/index.ts
Normal file
7
packages/ui-services/src/Import/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './AegisConverter/AegisToAuthenticatorConverter'
|
||||
export * from './SimplenoteConverter/SimplenoteConverter'
|
||||
export * from './GoogleKeepConverter/GoogleKeepConverter'
|
||||
export * from './EvernoteConverter/EvernoteConverter'
|
||||
export * from './PlaintextConverter/PlaintextConverter'
|
||||
export * from './Utils'
|
||||
export * from './Importer'
|
||||
@@ -32,7 +32,4 @@ export * from './Theme/ThemeManager'
|
||||
export * from './Toast/ToastService'
|
||||
export * from './Toast/ToastServiceInterface'
|
||||
export * from './StatePersistence/StatePersistence'
|
||||
export * from './Import/AegisConverter/AegisToAuthenticatorConverter'
|
||||
export * from './Import/SimplenoteConverter/SimplenoteConverter'
|
||||
export * from './Import/GoogleKeepConverter/GoogleKeepConverter'
|
||||
export * from './Import/EvernoteConverter/EvernoteConverter'
|
||||
export * from './Import'
|
||||
|
||||
Reference in New Issue
Block a user