2018-03-09 22:59:12 +02:00
const BaseItem = require ( 'lib/models/BaseItem.js' ) ;
const BaseModel = require ( 'lib/BaseModel.js' ) ;
const Resource = require ( 'lib/models/Resource.js' ) ;
const Folder = require ( 'lib/models/Folder.js' ) ;
const NoteTag = require ( 'lib/models/NoteTag.js' ) ;
const Note = require ( 'lib/models/Note.js' ) ;
const Tag = require ( 'lib/models/Tag.js' ) ;
const { basename , filename } = require ( 'lib/path-utils.js' ) ;
const fs = require ( 'fs-extra' ) ;
const ArrayUtils = require ( 'lib/ArrayUtils' ) ;
const { sprintf } = require ( 'sprintf-js' ) ;
const { shim } = require ( 'lib/shim' ) ;
const { _ } = require ( 'lib/locale' ) ;
const { fileExtension } = require ( 'lib/path-utils' ) ;
const { uuid } = require ( 'lib/uuid.js' ) ;
const { toTitleCase } = require ( 'lib/string-utils' ) ;
2018-02-25 19:01:16 +02:00
2018-02-26 21:25:54 +02:00
class InteropService {
2018-03-09 22:59:12 +02:00
2018-02-27 22:04:38 +02:00
constructor ( ) {
this . modules _ = null ;
}
modules ( ) {
if ( this . modules _ ) return this . modules _ ;
let importModules = [
{
2018-03-09 22:59:12 +02:00
format : 'jex' ,
2018-06-26 01:07:53 +02:00
fileExtensions : [ 'jex' ] ,
2018-03-09 22:59:12 +02:00
sources : [ 'file' ] ,
description : _ ( 'Joplin Export File' ) ,
} , {
format : 'md' ,
2018-06-26 01:07:53 +02:00
fileExtensions : [ 'md' , 'markdown' ] ,
2018-03-09 22:59:12 +02:00
sources : [ 'file' , 'directory' ] ,
2018-02-27 22:04:38 +02:00
isNoteArchive : false , // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
2018-03-09 22:59:12 +02:00
description : _ ( 'Markdown' ) ,
} , {
format : 'raw' ,
sources : [ 'directory' ] ,
description : _ ( 'Joplin Export Directory' ) ,
} , {
format : 'enex' ,
2018-06-26 01:07:53 +02:00
fileExtensions : [ 'enex' ] ,
2018-03-09 22:59:12 +02:00
sources : [ 'file' ] ,
description : _ ( 'Evernote Export File' ) ,
2018-02-27 22:04:38 +02:00
} ,
] ;
let exportModules = [
{
2018-03-09 22:59:12 +02:00
format : 'jex' ,
2018-06-26 01:07:53 +02:00
fileExtensions : [ 'jex' ] ,
2018-03-09 22:59:12 +02:00
target : 'file' ,
description : _ ( 'Joplin Export File' ) ,
} , {
format : 'raw' ,
target : 'directory' ,
description : _ ( 'Joplin Export Directory' ) ,
2018-11-11 22:17:43 +02:00
} , {
format : 'json' ,
target : 'directory' ,
description : _ ( 'Json Export Directory' ) ,
2018-09-04 12:59:09 +02:00
} , {
format : 'md' ,
target : 'directory' ,
description : _ ( 'Markdown' ) ,
2018-02-27 22:04:38 +02:00
} ,
] ;
2018-03-09 22:59:12 +02:00
importModules = importModules . map ( ( a ) => {
const className = 'InteropService_Importer_' + toTitleCase ( a . format ) ;
const output = Object . assign ( { } , {
type : 'importer' ,
path : 'lib/services/' + className ,
} , a ) ;
if ( ! ( 'isNoteArchive' in output ) ) output . isNoteArchive = true ;
2018-02-27 22:04:38 +02:00
return output ;
} ) ;
2018-03-09 22:59:12 +02:00
exportModules = exportModules . map ( ( a ) => {
const className = 'InteropService_Exporter_' + toTitleCase ( a . format ) ;
return Object . assign ( { } , {
type : 'exporter' ,
path : 'lib/services/' + className ,
} , a ) ;
2018-02-27 22:04:38 +02:00
} ) ;
this . modules _ = importModules . concat ( exportModules ) ;
2018-03-12 20:01:47 +02:00
this . modules _ = this . modules _ . map ( ( a ) => {
a . fullLabel = function ( moduleSource = null ) {
const label = [ this . format . toUpperCase ( ) + ' - ' + this . description ] ;
if ( moduleSource && this . sources . length > 1 ) {
label . push ( '(' + ( moduleSource === 'file' ? _ ( 'File' ) : _ ( 'Directory' ) ) + ')' ) ;
}
return label . join ( ' ' ) ;
} ;
return a ;
} ) ;
2018-02-27 22:04:38 +02:00
return this . modules _ ;
}
moduleByFormat _ ( type , format ) {
const modules = this . modules ( ) ;
for ( let i = 0 ; i < modules . length ; i ++ ) {
const m = modules [ i ] ;
if ( m . format === format && m . type === type ) return modules [ i ] ;
2018-02-26 21:16:01 +02:00
}
2018-02-27 22:04:38 +02:00
return null ;
2018-02-25 19:01:16 +02:00
}
2018-02-27 22:04:38 +02:00
newModule _ ( type , format ) {
const module = this . moduleByFormat _ ( type , format ) ;
if ( ! module ) throw new Error ( _ ( 'Cannot load "%s" module for format "%s"' , type , format ) ) ;
const ModuleClass = require ( module . path ) ;
2018-06-26 01:07:53 +02:00
const output = new ModuleClass ( ) ;
output . setMetadata ( module ) ;
return output ;
2018-02-26 20:43:50 +02:00
}
2018-02-27 22:04:38 +02:00
moduleByFileExtension _ ( type , ext ) {
ext = ext . toLowerCase ( ) ;
const modules = this . modules ( ) ;
for ( let i = 0 ; i < modules . length ; i ++ ) {
const m = modules [ i ] ;
if ( type !== m . type ) continue ;
2019-07-13 18:26:47 +02:00
if ( m . fileExtensions && m . fileExtensions . indexOf ( ext ) >= 0 ) return m ;
2018-02-27 22:04:38 +02:00
}
return null ;
2018-02-26 21:16:01 +02:00
}
2018-02-23 21:32:19 +02:00
async import ( options ) {
2018-02-26 21:16:01 +02:00
if ( ! await shim . fsDriver ( ) . exists ( options . path ) ) throw new Error ( _ ( 'Cannot find "%s".' , options . path ) ) ;
2018-03-09 22:59:12 +02:00
options = Object . assign ( { } , {
format : 'auto' ,
destinationFolderId : null ,
destinationFolder : null ,
} , options ) ;
2018-02-25 19:01:16 +02:00
2018-03-09 22:59:12 +02:00
if ( options . format === 'auto' ) {
const module = this . moduleByFileExtension _ ( 'importer' , fileExtension ( options . path ) ) ;
if ( ! module ) throw new Error ( _ ( 'Please specify import format for %s' , options . path ) ) ;
2018-02-27 22:04:38 +02:00
options . format = module . format ;
2018-02-25 19:01:16 +02:00
}
if ( options . destinationFolderId ) {
const folder = await Folder . load ( options . destinationFolderId ) ;
2018-02-27 22:04:38 +02:00
if ( ! folder ) throw new Error ( _ ( 'Cannot find "%s".' , options . destinationFolderId ) ) ;
2018-02-26 20:43:50 +02:00
options . destinationFolder = folder ;
2018-02-25 19:01:16 +02:00
}
2018-03-09 22:59:12 +02:00
let result = { warnings : [ ] }
2018-02-25 19:01:16 +02:00
2018-03-09 22:59:12 +02:00
const importer = this . newModule _ ( 'importer' , options . format ) ;
2018-02-26 20:43:50 +02:00
await importer . init ( options . path , options ) ;
result = await importer . exec ( result ) ;
2018-02-25 19:01:16 +02:00
return result ;
2018-02-23 21:32:19 +02:00
}
async export ( options ) {
const exportPath = options . path ? options . path : null ;
2018-06-10 20:15:40 +02:00
let sourceFolderIds = options . sourceFolderIds ? options . sourceFolderIds : [ ] ;
2018-02-23 21:32:19 +02:00
const sourceNoteIds = options . sourceNoteIds ? options . sourceNoteIds : [ ] ;
2018-03-09 22:59:12 +02:00
const exportFormat = options . format ? options . format : 'jex' ;
const result = { warnings : [ ] }
2018-02-23 21:32:19 +02:00
const itemsToExport = [ ] ;
const queueExportItem = ( itemType , itemOrId ) => {
itemsToExport . push ( {
type : itemType ,
2018-03-09 22:59:12 +02:00
itemOrId : itemOrId
2018-02-23 21:32:19 +02:00
} ) ;
2018-03-09 22:59:12 +02:00
}
2018-02-23 21:32:19 +02:00
let exportedNoteIds = [ ] ;
let resourceIds = [ ] ;
const folderIds = await Folder . allIds ( ) ;
2018-06-10 20:15:40 +02:00
let fullSourceFolderIds = sourceFolderIds . slice ( ) ;
for ( let i = 0 ; i < sourceFolderIds . length ; i ++ ) {
const id = sourceFolderIds [ i ] ;
const childrenIds = await Folder . childrenIds ( id ) ;
fullSourceFolderIds = fullSourceFolderIds . concat ( childrenIds ) ;
}
sourceFolderIds = fullSourceFolderIds ;
2018-02-23 21:32:19 +02:00
for ( let folderIndex = 0 ; folderIndex < folderIds . length ; folderIndex ++ ) {
const folderId = folderIds [ folderIndex ] ;
if ( sourceFolderIds . length && sourceFolderIds . indexOf ( folderId ) < 0 ) continue ;
if ( ! sourceNoteIds . length ) await queueExportItem ( BaseModel . TYPE _FOLDER , folderId ) ;
const noteIds = await Folder . noteIds ( folderId ) ;
for ( let noteIndex = 0 ; noteIndex < noteIds . length ; noteIndex ++ ) {
const noteId = noteIds [ noteIndex ] ;
if ( sourceNoteIds . length && sourceNoteIds . indexOf ( noteId ) < 0 ) continue ;
const note = await Note . load ( noteId ) ;
await queueExportItem ( BaseModel . TYPE _NOTE , note ) ;
exportedNoteIds . push ( noteId ) ;
2018-05-03 14:11:45 +02:00
const rids = await Note . linkedResourceIds ( note . body ) ;
2018-02-23 21:32:19 +02:00
resourceIds = resourceIds . concat ( rids ) ;
}
}
2018-02-26 21:25:54 +02:00
resourceIds = ArrayUtils . unique ( resourceIds ) ;
2018-02-23 21:32:19 +02:00
for ( let i = 0 ; i < resourceIds . length ; i ++ ) {
await queueExportItem ( BaseModel . TYPE _RESOURCE , resourceIds [ i ] ) ;
}
const noteTags = await NoteTag . all ( ) ;
let exportedTagIds = [ ] ;
for ( let i = 0 ; i < noteTags . length ; i ++ ) {
const noteTag = noteTags [ i ] ;
if ( exportedNoteIds . indexOf ( noteTag . note _id ) < 0 ) continue ;
await queueExportItem ( BaseModel . TYPE _NOTE _TAG , noteTag . id ) ;
exportedTagIds . push ( noteTag . tag _id ) ;
}
for ( let i = 0 ; i < exportedTagIds . length ; i ++ ) {
await queueExportItem ( BaseModel . TYPE _TAG , exportedTagIds [ i ] ) ;
}
2018-03-09 22:59:12 +02:00
const exporter = this . newModule _ ( 'exporter' , exportFormat ) ;
2018-02-23 21:32:19 +02:00
await exporter . init ( exportPath ) ;
2018-11-21 02:36:23 +02:00
const typeOrder = [ BaseModel . TYPE _FOLDER , BaseModel . TYPE _RESOURCE , BaseModel . TYPE _NOTE , BaseModel . TYPE _TAG , BaseModel . TYPE _NOTE _TAG ] ;
const context = {
resourcePaths : { } ,
} ;
for ( let typeOrderIndex = 0 ; typeOrderIndex < typeOrder . length ; typeOrderIndex ++ ) {
const type = typeOrder [ typeOrderIndex ] ;
for ( let i = 0 ; i < itemsToExport . length ; i ++ ) {
const itemType = itemsToExport [ i ] . type ;
if ( itemType !== type ) continue ;
const ItemClass = BaseItem . getClassByItemType ( itemType ) ;
const itemOrId = itemsToExport [ i ] . itemOrId ;
const item = typeof itemOrId === 'object' ? itemOrId : await ItemClass . load ( itemOrId ) ;
if ( ! item ) {
if ( itemType === BaseModel . TYPE _RESOURCE ) {
result . warnings . push ( sprintf ( 'A resource that does not exist is referenced in a note. The resource was skipped. Resource ID: %s' , itemOrId ) ) ;
} else {
result . warnings . push ( sprintf ( 'Cannot find item with type "%s" and ID %s. Item was skipped.' , ItemClass . tableName ( ) , JSON . stringify ( itemOrId ) ) ) ;
}
continue ;
2018-02-23 21:32:19 +02:00
}
2018-11-21 02:36:23 +02:00
if ( item . encryption _applied || item . encryption _blob _encrypted ) throw new Error ( _ ( 'This item is currently encrypted: %s "%s". Please wait for all items to be decrypted and try again.' , BaseModel . modelTypeToName ( itemType ) , item . title ? item . title : item . id ) ) ;
2018-02-27 22:51:07 +02:00
2018-11-21 02:36:23 +02:00
try {
if ( itemType == BaseModel . TYPE _RESOURCE ) {
const resourcePath = Resource . fullPath ( item ) ;
context . resourcePaths [ item . id ] = resourcePath ;
exporter . updateContext ( context ) ;
await exporter . processResource ( item , resourcePath ) ;
}
2018-02-23 21:32:19 +02:00
2018-11-21 02:36:23 +02:00
await exporter . processItem ( ItemClass , item ) ;
} catch ( error ) {
result . warnings . push ( error . message ) ;
}
2018-02-23 21:32:19 +02:00
}
}
await exporter . close ( ) ;
return result ;
}
2018-03-09 22:59:12 +02:00
2018-02-23 21:32:19 +02:00
}
2018-03-09 22:59:12 +02:00
module . exports = InteropService ;