2018-03-09 19:49:35 +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-02-27 22:04:38 +02:00
constructor ( ) {
this . modules _ = null ;
}
modules ( ) {
if ( this . modules _ ) return this . modules _ ;
let importModules = [
{
2018-03-09 19:49:35 +02:00
format : "jex" ,
fileExtension : "jex" ,
sources : [ "file" ] ,
description : _ ( "Joplin Export File" ) ,
} ,
{
format : "md" ,
fileExtension : "md" ,
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 19:49:35 +02:00
description : _ ( "Markdown" ) ,
} ,
{
format : "raw" ,
sources : [ "directory" ] ,
description : _ ( "Joplin Export Directory" ) ,
} ,
{
format : "enex" ,
fileExtension : "enex" ,
sources : [ "file" ] ,
description : _ ( "Evernote Export File" ) ,
2018-02-27 22:04:38 +02:00
} ,
] ;
let exportModules = [
{
2018-03-09 19:49:35 +02:00
format : "jex" ,
fileExtension : "jex" ,
target : "file" ,
description : _ ( "Joplin Export File" ) ,
} ,
{
format : "raw" ,
target : "directory" ,
description : _ ( "Joplin Export Directory" ) ,
2018-02-27 22:04:38 +02:00
} ,
] ;
2018-03-09 19:49:35 +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 19:49:35 +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 ) ;
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 ) ;
return new ModuleClass ( ) ;
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 ;
if ( m . fileExtension === ext ) return m ;
}
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 19:49:35 +02:00
options = Object . assign (
{ } ,
{
format : "auto" ,
destinationFolderId : null ,
destinationFolder : null ,
} ,
options
) ;
2018-02-25 19:01:16 +02:00
2018-03-09 19:49:35 +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 19:49:35 +02:00
let result = { warnings : [ ] } ;
2018-02-25 19:01:16 +02:00
2018-03-09 19:49:35 +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 ;
const sourceFolderIds = options . sourceFolderIds ? options . sourceFolderIds : [ ] ;
const sourceNoteIds = options . sourceNoteIds ? options . sourceNoteIds : [ ] ;
2018-03-09 19:49:35 +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 19:49:35 +02:00
itemOrId : itemOrId ,
2018-02-23 21:32:19 +02:00
} ) ;
2018-03-09 19:49:35 +02:00
} ;
2018-02-23 21:32:19 +02:00
let exportedNoteIds = [ ] ;
let resourceIds = [ ] ;
const folderIds = await Folder . allIds ( ) ;
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 ) ;
const rids = Note . linkedResourceIds ( note . body ) ;
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 19:49:35 +02:00
const exporter = this . newModule _ ( "exporter" , exportFormat ) ;
2018-02-23 21:32:19 +02:00
await exporter . init ( exportPath ) ;
for ( let i = 0 ; i < itemsToExport . length ; i ++ ) {
const itemType = itemsToExport [ i ] . type ;
const ItemClass = BaseItem . getClassByItemType ( itemType ) ;
const itemOrId = itemsToExport [ i ] . itemOrId ;
2018-03-09 19:49:35 +02:00
const item = typeof itemOrId === "object" ? itemOrId : await ItemClass . load ( itemOrId ) ;
2018-02-23 21:32:19 +02:00
if ( ! item ) {
if ( itemType === BaseModel . TYPE _RESOURCE ) {
2018-03-09 19:49:35 +02:00
result . warnings . push ( sprintf ( "A resource that does not exist is referenced in a note. The resource was skipped. Resource ID: %s" , itemOrId ) ) ;
2018-02-23 21:32:19 +02:00
} else {
result . warnings . push ( sprintf ( 'Cannot find item with type "%s" and ID %s. Item was skipped.' , ItemClass . tableName ( ) , JSON . stringify ( itemOrId ) ) ) ;
}
continue ;
}
2018-03-09 19:49:35 +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-02-26 21:25:54 +02:00
try {
if ( itemType == BaseModel . TYPE _RESOURCE ) {
const resourcePath = Resource . fullPath ( item ) ;
await exporter . processResource ( item , resourcePath ) ;
}
2018-02-23 21:32:19 +02:00
2018-02-26 21:25:54 +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 19:49:35 +02:00
module . exports = InteropService ;