2020-03-20 21:57:16 +01:00
"use strict" ;
2024-07-18 21:35:17 +03:00
import BAttribute from "../../becca/entities/battribute.js" ;
2025-03-16 14:39:17 +02:00
import { removeTextFileExtension , newEntityId , getNoteTitle , processStringOrBuffer , unescapeHtml } from "../../services/utils.js" ;
2024-07-18 21:35:17 +03:00
import log from "../../services/log.js" ;
import noteService from "../../services/notes.js" ;
import attributeService from "../../services/attributes.js" ;
import BBranch from "../../becca/entities/bbranch.js" ;
2024-07-18 21:37:45 +03:00
import path from "path" ;
2024-07-18 21:35:17 +03:00
import protectedSessionService from "../protected_session.js" ;
import mimeService from "./mime.js" ;
import treeService from "../tree.js" ;
2024-07-18 21:37:45 +03:00
import yauzl from "yauzl" ;
2024-07-18 21:35:17 +03:00
import htmlSanitizer from "../html_sanitizer.js" ;
import becca from "../../becca/becca.js" ;
import BAttachment from "../../becca/entities/battachment.js" ;
import markdownService from "./markdown.js" ;
2025-01-13 23:18:10 +02:00
import type TaskContext from "../task_context.js" ;
import type BNote from "../../becca/entities/bnote.js" ;
2025-01-09 18:36:24 +02:00
import type NoteMeta from "../meta/note_meta.js" ;
import type AttributeMeta from "../meta/attribute_meta.js" ;
2025-01-13 23:18:10 +02:00
import type { Stream } from "stream" ;
2025-04-18 12:33:50 +03:00
import { ALLOWED_NOTE_TYPES , type NoteType } from "@triliumnext/commons" ;
2024-04-03 22:46:14 +03:00
interface MetaFile {
2025-01-09 18:07:02 +02:00
files : NoteMeta [ ] ;
2024-04-03 22:46:14 +03:00
}
2025-03-10 19:14:46 +02:00
interface ImportZipOpts {
preserveIds? : boolean ;
}
async function importZip ( taskContext : TaskContext , fileBuffer : Buffer , importRootNote : BNote , opts? : ImportZipOpts ) : Promise < BNote > {
2024-04-03 22:46:14 +03:00
/** maps from original noteId (in ZIP file) to newly generated noteId */
const noteIdMap : Record < string , string > = { } ;
/** type maps from original attachmentId (in ZIP file) to newly generated attachmentId */
const attachmentIdMap : Record < string , string > = { } ;
const attributes : AttributeMeta [ ] = [ ] ;
2020-03-21 15:14:44 +01:00
// path => noteId, used only when meta file is not available
2024-04-03 22:46:14 +03:00
/** path => noteId | attachmentId */
2025-01-09 18:07:02 +02:00
const createdPaths : Record < string , string > = { "/" : importRootNote . noteId , "\\" : importRootNote . noteId } ;
2024-04-13 17:30:48 +03:00
let metaFile : MetaFile | null = null ;
let firstNote : BNote | null = null ;
2025-06-20 20:56:25 +03:00
let topLevelPath = "" ;
2024-04-03 22:46:14 +03:00
const createdNoteIds = new Set < string > ( ) ;
function getNewNoteId ( origNoteId : string ) {
2020-03-20 21:57:16 +01:00
if ( ! origNoteId . trim ( ) ) {
2023-05-06 22:50:28 +02:00
// this probably shouldn't happen, but still good to have this precaution
return "empty_note_id" ;
2020-03-20 21:57:16 +01:00
}
2025-03-10 19:14:46 +02:00
if ( origNoteId === "root" || origNoteId . startsWith ( "_" ) || opts ? . preserveIds ) {
2022-12-23 23:08:30 +01:00
// these "named" noteIds don't differ between Trilium instances
return origNoteId ;
}
2020-03-20 21:57:16 +01:00
if ( ! noteIdMap [ origNoteId ] ) {
2025-01-02 13:47:44 +01:00
noteIdMap [ origNoteId ] = newEntityId ( ) ;
2020-03-20 21:57:16 +01:00
}
return noteIdMap [ origNoteId ] ;
}
2024-04-03 22:46:14 +03:00
function getNewAttachmentId ( origAttachmentId : string ) {
2025-03-10 20:50:57 +02:00
if ( opts ? . preserveIds ) {
return origAttachmentId ;
}
2023-05-06 22:50:28 +02:00
if ( ! origAttachmentId . trim ( ) ) {
// this probably shouldn't happen, but still good to have this precaution
return "empty_attachment_id" ;
}
if ( ! attachmentIdMap [ origAttachmentId ] ) {
2025-01-02 13:47:44 +01:00
attachmentIdMap [ origAttachmentId ] = newEntityId ( ) ;
2023-05-06 22:50:28 +02:00
}
return attachmentIdMap [ origAttachmentId ] ;
}
2024-04-03 22:46:14 +03:00
function getAttachmentMeta ( parentNoteMeta : NoteMeta , dataFileName : string ) {
for ( const noteMeta of parentNoteMeta . children || [ ] ) {
2023-05-06 22:50:28 +02:00
for ( const attachmentMeta of noteMeta . attachments || [ ] ) {
if ( attachmentMeta . dataFileName === dataFileName ) {
return {
parentNoteMeta ,
noteMeta ,
attachmentMeta
} ;
}
}
}
return { } ;
}
2024-04-03 22:46:14 +03:00
function getMeta ( filePath : string ) {
2020-03-20 21:57:16 +01:00
if ( ! metaFile ) {
return { } ;
}
const pathSegments = filePath . split ( /[\/\\]/g ) ;
2024-04-03 22:46:14 +03:00
let cursor : NoteMeta | undefined = {
2020-03-20 21:57:16 +01:00
isImportRoot : true ,
2024-04-03 22:46:14 +03:00
children : metaFile.files ,
dataFileName : ""
2020-03-20 21:57:16 +01:00
} ;
2024-04-13 17:30:48 +03:00
let parent : NoteMeta | undefined = undefined ;
2020-03-20 21:57:16 +01:00
2025-03-16 14:39:17 +02:00
for ( let segment of pathSegments ) {
2023-05-06 22:50:28 +02:00
if ( ! cursor ? . children ? . length ) {
2020-03-20 21:57:16 +01:00
return { } ;
}
2025-03-16 14:39:17 +02:00
segment = unescapeHtml ( segment ) ;
2020-03-20 21:57:16 +01:00
parent = cursor ;
2024-04-03 22:46:14 +03:00
if ( parent . children ) {
2025-01-09 18:07:02 +02:00
cursor = parent . children . find ( ( file ) = > file . dataFileName === segment || file . dirFileName === segment ) ;
2024-04-03 22:46:14 +03:00
}
2023-03-08 09:01:23 +01:00
if ( ! cursor ) {
2023-05-06 22:50:28 +02:00
return getAttachmentMeta ( parent , segment ) ;
2023-03-08 09:01:23 +01:00
}
2020-03-20 21:57:16 +01:00
}
return {
parentNoteMeta : parent ,
2024-04-03 22:46:14 +03:00
noteMeta : cursor ,
attachmentMeta : null
2020-03-20 21:57:16 +01:00
} ;
}
2024-04-03 22:46:14 +03:00
function getParentNoteId ( filePath : string , parentNoteMeta? : NoteMeta ) {
2020-03-20 21:57:16 +01:00
let parentNoteId ;
2024-04-03 22:46:14 +03:00
if ( parentNoteMeta ? . noteId ) {
2020-03-20 21:57:16 +01:00
parentNoteId = parentNoteMeta . isImportRoot ? importRootNote.noteId : getNewNoteId ( parentNoteMeta . noteId ) ;
2025-01-09 18:07:02 +02:00
} else {
2020-03-20 21:57:16 +01:00
const parentPath = path . dirname ( filePath ) ;
2025-01-09 18:07:02 +02:00
if ( parentPath === "." ) {
2020-03-20 21:57:16 +01:00
parentNoteId = importRootNote . noteId ;
2023-05-06 22:50:28 +02:00
} else if ( parentPath in createdPaths ) {
2020-03-20 21:57:16 +01:00
parentNoteId = createdPaths [ parentPath ] ;
2023-05-06 22:50:28 +02:00
} else {
2023-06-30 11:18:34 +02:00
// ZIP allows creating out of order records - i.e., file in a directory can appear in the ZIP stream before the actual directory
2020-06-20 12:31:38 +02:00
parentNoteId = saveDirectory ( parentPath ) ;
2020-03-20 21:57:16 +01:00
}
}
return parentNoteId ;
}
2024-04-03 22:46:14 +03:00
function getNoteId ( noteMeta : NoteMeta | undefined , filePath : string ) : string {
if ( noteMeta ? . noteId ) {
2020-03-21 15:14:44 +01:00
return getNewNoteId ( noteMeta . noteId ) ;
}
2023-05-06 22:50:28 +02:00
// in case we lack metadata, we treat e.g. "Programming.html" and "Programming" as the same note
// (one data file, the other directory for children)
2025-01-02 13:47:44 +01:00
const filePathNoExt = removeTextFileExtension ( filePath ) ;
2020-03-20 21:57:16 +01:00
if ( filePathNoExt in createdPaths ) {
return createdPaths [ filePathNoExt ] ;
}
2025-01-02 13:47:44 +01:00
const noteId = newEntityId ( ) ;
2020-03-20 21:57:16 +01:00
createdPaths [ filePathNoExt ] = noteId ;
return noteId ;
}
2024-04-03 22:46:14 +03:00
function detectFileTypeAndMime ( taskContext : TaskContext , filePath : string ) {
2020-03-20 21:57:16 +01:00
const mime = mimeService . getMime ( filePath ) || "application/octet-stream" ;
2024-04-03 22:46:14 +03:00
const type = mimeService . getType ( taskContext . data || { } , mime ) ;
2020-03-20 21:57:16 +01:00
return { mime , type } ;
}
2024-04-03 22:46:14 +03:00
function saveAttributes ( note : BNote , noteMeta : NoteMeta | undefined ) {
2020-03-20 21:57:16 +01:00
if ( ! noteMeta ) {
return ;
}
2024-04-03 22:46:14 +03:00
for ( const attr of noteMeta . attributes || [ ] ) {
2020-03-20 21:57:16 +01:00
attr . noteId = note . noteId ;
2025-01-09 18:07:02 +02:00
if ( attr . type === "label-definition" ) {
attr . type = "label" ;
2022-12-21 15:19:05 +01:00
attr . name = ` label: ${ attr . name } ` ;
2025-01-09 18:07:02 +02:00
} else if ( attr . type === "relation-definition" ) {
attr . type = "label" ;
2022-12-21 15:19:05 +01:00
attr . name = ` relation: ${ attr . name } ` ;
2020-06-27 00:40:35 +02:00
}
2020-03-20 21:57:16 +01:00
if ( ! attributeService . isAttributeType ( attr . type ) ) {
2022-12-21 15:19:05 +01:00
log . error ( ` Unrecognized attribute type ${ attr . type } ` ) ;
2020-03-20 21:57:16 +01:00
continue ;
}
2025-01-09 18:07:02 +02:00
if ( attr . type === "relation" && [ "internalLink" , "imageLink" , "relationMapLink" , "includeNoteLink" ] . includes ( attr . name ) ) {
2020-03-20 21:57:16 +01:00
// these relations are created automatically and as such don't need to be duplicated in the import
continue ;
}
2025-01-09 18:07:02 +02:00
if ( attr . type === "relation" ) {
2020-03-20 21:57:16 +01:00
attr . value = getNewNoteId ( attr . value ) ;
}
2024-04-03 22:46:14 +03:00
if ( taskContext . data ? . safeImport && attributeService . isAttributeDangerous ( attr . type , attr . name ) ) {
2022-12-21 15:19:05 +01:00
attr . name = ` disabled: ${ attr . name } ` ;
2020-03-20 21:57:16 +01:00
}
2024-04-03 22:46:14 +03:00
if ( taskContext . data ? . safeImport ) {
2022-07-06 23:09:16 +02:00
attr . name = htmlSanitizer . sanitize ( attr . name ) ;
attr . value = htmlSanitizer . sanitize ( attr . value ) ;
}
2020-03-20 21:57:16 +01:00
attributes . push ( attr ) ;
}
}
2024-04-03 22:46:14 +03:00
function saveDirectory ( filePath : string ) {
2020-03-20 21:57:16 +01:00
const { parentNoteMeta , noteMeta } = getMeta ( filePath ) ;
const noteId = getNoteId ( noteMeta , filePath ) ;
2023-05-06 22:50:28 +02:00
if ( becca . getNote ( noteId ) ) {
2020-03-20 21:57:16 +01:00
return ;
}
2025-01-02 13:47:44 +01:00
const noteTitle = getNoteTitle ( filePath , ! ! taskContext . data ? . replaceUnderscoresWithSpaces , noteMeta ) ;
2023-05-06 22:50:28 +02:00
const parentNoteId = getParentNoteId ( filePath , parentNoteMeta ) ;
2024-04-03 22:46:14 +03:00
if ( ! parentNoteId ) {
throw new Error ( "Missing parent note ID." ) ;
}
2024-07-13 16:55:20 +03:00
2025-01-09 18:07:02 +02:00
const { note } = noteService . createNewNote ( {
2020-03-20 21:57:16 +01:00
parentNoteId : parentNoteId ,
2024-04-03 22:46:14 +03:00
title : noteTitle || "" ,
2025-01-09 18:07:02 +02:00
content : "" ,
2020-03-20 21:57:16 +01:00
noteId : noteId ,
2022-12-25 14:10:12 +01:00
type : resolveNoteType ( noteMeta ? . type ) ,
2025-01-09 18:07:02 +02:00
mime : noteMeta ? noteMeta . mime : "text/html" ,
prefix : noteMeta?.prefix || "" ,
2024-04-10 19:26:08 +03:00
isExpanded : ! ! noteMeta ? . isExpanded ,
2025-01-09 18:07:02 +02:00
notePosition : noteMeta && firstNote ? noteMeta.notePosition : undefined ,
isProtected : importRootNote.isProtected && protectedSessionService . isProtectedSessionAvailable ( )
2023-05-06 22:50:28 +02:00
} ) ;
2020-03-20 21:57:16 +01:00
2023-05-06 15:07:38 +02:00
createdNoteIds . add ( note . noteId ) ;
2020-03-21 15:14:44 +01:00
2020-06-20 12:31:38 +02:00
saveAttributes ( note , noteMeta ) ;
2020-03-20 21:57:16 +01:00
2023-05-06 22:50:28 +02:00
firstNote = firstNote || note ;
2020-03-20 21:57:16 +01:00
return noteId ;
}
2024-04-03 22:46:14 +03:00
function getEntityIdFromRelativeUrl ( url : string , filePath : string ) {
2025-06-20 20:56:25 +03:00
let absUrl ;
if ( ! url . startsWith ( "/" ) ) {
while ( url . startsWith ( "./" ) ) {
url = url . substr ( 2 ) ;
}
2020-03-20 21:57:16 +01:00
2025-06-20 20:56:25 +03:00
let absUrl = path . dirname ( filePath ) ;
2020-03-20 21:57:16 +01:00
2025-06-20 20:56:25 +03:00
while ( url . startsWith ( "../" ) ) {
absUrl = path . dirname ( absUrl ) ;
2020-03-20 21:57:16 +01:00
2025-06-20 20:56:25 +03:00
url = url . substr ( 3 ) ;
}
2020-03-20 21:57:16 +01:00
2025-06-20 20:56:25 +03:00
if ( absUrl === "." ) {
absUrl = "" ;
}
2020-03-20 21:57:16 +01:00
2025-06-20 20:56:25 +03:00
absUrl += ` ${ absUrl . length > 0 ? "/" : "" } ${ url } ` ;
} else {
absUrl = topLevelPath + url ;
}
2020-03-20 21:57:16 +01:00
2023-05-06 22:50:28 +02:00
const { noteMeta , attachmentMeta } = getMeta ( absUrl ) ;
2022-12-29 10:25:49 +01:00
2024-04-03 22:46:14 +03:00
if ( attachmentMeta && attachmentMeta . attachmentId && noteMeta . noteId ) {
2023-05-06 22:50:28 +02:00
return {
2023-07-13 23:54:47 +02:00
attachmentId : getNewAttachmentId ( attachmentMeta . attachmentId ) ,
noteId : getNewNoteId ( noteMeta . noteId )
2023-05-06 22:50:28 +02:00
} ;
2025-01-09 18:07:02 +02:00
} else {
// don't check for noteMeta since it's not mandatory for notes
2023-05-06 22:50:28 +02:00
return {
noteId : getNoteId ( noteMeta , absUrl )
} ;
}
2020-03-20 21:57:16 +01:00
}
2024-04-03 22:46:14 +03:00
function processTextNoteContent ( content : string , noteTitle : string , filePath : string , noteMeta? : NoteMeta ) {
function isUrlAbsolute ( url : string ) {
2022-12-26 10:38:31 +01:00
return /^(?:[a-z]+:)?\/\//i . test ( url ) ;
}
2023-10-18 09:24:13 +02:00
content = removeTriliumTags ( content ) ;
2023-05-04 19:56:02 +03:00
2022-12-26 10:38:31 +01:00
content = content . replace ( /<h1>([^<]*)<\/h1>/gi , ( match , text ) = > {
if ( noteTitle . trim ( ) === text . trim ( ) ) {
return "" ; // remove whole H1 tag
} else {
return ` <h2> ${ text } </h2> ` ;
}
} ) ;
2024-04-03 22:46:14 +03:00
if ( taskContext . data ? . safeImport ) {
2023-10-21 17:54:07 +02:00
content = htmlSanitizer . sanitize ( content ) ;
}
2022-12-26 10:38:31 +01:00
content = content . replace ( / < h t m l . * < b o d y [ ^ > ] * > / g i s , " " ) ;
content = content . replace ( / < \ / b o d y > . * < \ / h t m l > / g i s , " " ) ;
content = content . replace ( /src="([^"]*)"/g , ( match , url ) = > {
2023-10-03 09:40:31 +02:00
if ( url . startsWith ( "data:image" ) ) {
// inline images are parsed and saved into attachments in the note service
return match ;
}
2022-12-26 10:38:31 +01:00
try {
2023-05-06 22:50:28 +02:00
url = decodeURIComponent ( url ) . trim ( ) ;
2024-04-03 22:46:14 +03:00
} catch ( e : any ) {
2023-05-06 22:50:28 +02:00
log . error ( ` Cannot parse image URL ' ${ url } ', keeping original. Error: ${ e . message } . ` ) ;
2022-12-26 10:38:31 +01:00
return ` src=" ${ url } " ` ;
}
2025-06-20 21:59:11 +03:00
if ( isUrlAbsolute ( url ) ) {
2022-12-26 10:38:31 +01:00
return match ;
}
2023-05-06 22:50:28 +02:00
const target = getEntityIdFromRelativeUrl ( url , filePath ) ;
2022-12-26 10:38:31 +01:00
2023-07-13 23:54:47 +02:00
if ( target . attachmentId ) {
2023-05-06 22:50:28 +02:00
return ` src="api/attachments/ ${ target . attachmentId } /image/ ${ path . basename ( url ) } " ` ;
2023-07-13 23:54:47 +02:00
} else if ( target . noteId ) {
return ` src="api/images/ ${ target . noteId } / ${ path . basename ( url ) } " ` ;
2023-05-06 22:50:28 +02:00
} else {
2022-12-29 10:27:23 +01:00
return match ;
}
2022-12-26 10:38:31 +01:00
} ) ;
content = content . replace ( /href="([^"]*)"/g , ( match , url ) = > {
try {
2023-05-06 22:50:28 +02:00
url = decodeURIComponent ( url ) . trim ( ) ;
2024-04-03 22:46:14 +03:00
} catch ( e : any ) {
2023-05-06 22:50:28 +02:00
log . error ( ` Cannot parse link URL ' ${ url } ', keeping original. Error: ${ e . message } . ` ) ;
2022-12-26 10:38:31 +01:00
return ` href=" ${ url } " ` ;
}
2025-01-09 18:07:02 +02:00
if (
url . startsWith ( "#" ) || // already a note path (probably)
isUrlAbsolute ( url )
) {
2022-12-26 10:38:31 +01:00
return match ;
}
2023-05-06 22:50:28 +02:00
const target = getEntityIdFromRelativeUrl ( url , filePath ) ;
2022-12-26 10:38:31 +01:00
2023-07-13 23:54:47 +02:00
if ( target . attachmentId ) {
return ` href="#root/ ${ target . noteId } ?viewMode=attachments&attachmentId= ${ target . attachmentId } " ` ;
} else if ( target . noteId ) {
return ` href="#root/ ${ target . noteId } " ` ;
} else {
2022-12-29 10:27:23 +01:00
return match ;
}
2022-12-26 10:38:31 +01:00
} ) ;
if ( noteMeta ) {
2025-01-09 18:07:02 +02:00
const includeNoteLinks = ( noteMeta . attributes || [ ] ) . filter ( ( attr ) = > attr . type === "relation" && attr . name === "includeNoteLink" ) ;
2022-12-26 10:38:31 +01:00
for ( const link of includeNoteLinks ) {
// no need to escape the regexp find string since it's a noteId which doesn't contain any special characters
content = content . replace ( new RegExp ( link . value , "g" ) , getNewNoteId ( link . value ) ) ;
}
}
2023-02-19 21:34:37 +01:00
content = content . trim ( ) ;
2022-12-26 10:38:31 +01:00
return content ;
}
2024-04-03 22:46:14 +03:00
function processNoteContent ( noteMeta : NoteMeta | undefined , type : string , mime : string , content : string | Buffer , noteTitle : string , filePath : string ) {
2025-02-20 20:25:42 +02:00
if ( ( noteMeta ? . format === "markdown" || ( ! noteMeta && taskContext . data ? . textImportedAsText && [ "text/markdown" , "text/x-markdown" , "text/mdx" ] . includes ( mime ) ) ) && typeof content === "string" ) {
2023-07-15 10:31:50 +02:00
content = markdownService . renderToHtml ( content , noteTitle ) ;
2022-12-26 22:51:16 +01:00
}
2025-01-09 18:07:02 +02:00
if ( type === "text" && typeof content === "string" ) {
2022-12-26 22:51:16 +01:00
content = processTextNoteContent ( content , noteTitle , filePath , noteMeta ) ;
}
2025-01-09 18:07:02 +02:00
if ( type === "relationMap" && noteMeta && typeof content === "string" ) {
const relationMapLinks = ( noteMeta . attributes || [ ] ) . filter ( ( attr ) = > attr . type === "relation" && attr . name === "relationMapLink" ) ;
2022-12-26 22:51:16 +01:00
// this will replace relation map links
for ( const link of relationMapLinks ) {
// no need to escape the regexp find string since it's a noteId which doesn't contain any special characters
content = content . replace ( new RegExp ( link . value , "g" ) , getNewNoteId ( link . value ) ) ;
}
}
return content ;
}
2024-04-03 22:46:14 +03:00
function saveNote ( filePath : string , content : string | Buffer ) {
2023-05-06 22:50:28 +02:00
const { parentNoteMeta , noteMeta , attachmentMeta } = getMeta ( filePath ) ;
2020-03-20 21:57:16 +01:00
2022-12-26 22:51:16 +01:00
if ( noteMeta ? . noImport ) {
2020-03-20 21:57:16 +01:00
return ;
}
const noteId = getNoteId ( noteMeta , filePath ) ;
2023-01-25 09:55:29 +01:00
2024-04-03 22:46:14 +03:00
if ( attachmentMeta && attachmentMeta . attachmentId ) {
2023-03-16 12:17:55 +01:00
const attachment = new BAttachment ( {
2023-05-06 22:50:28 +02:00
attachmentId : getNewAttachmentId ( attachmentMeta . attachmentId ) ,
2023-07-14 17:01:56 +02:00
ownerId : noteId ,
2023-03-16 12:11:00 +01:00
title : attachmentMeta.title ,
role : attachmentMeta.role ,
2023-05-06 22:50:28 +02:00
mime : attachmentMeta.mime ,
position : attachmentMeta.position
2023-03-08 09:01:23 +01:00
} ) ;
2023-09-28 00:24:53 +02:00
attachment . setContent ( content , { forceSave : true } ) ;
2023-03-08 09:01:23 +01:00
return ;
}
2020-06-20 12:31:38 +02:00
const parentNoteId = getParentNoteId ( filePath , parentNoteMeta ) ;
2020-03-20 21:57:16 +01:00
2020-03-21 15:14:44 +01:00
if ( ! parentNoteId ) {
2023-05-06 15:07:38 +02:00
throw new Error ( ` Cannot find parentNoteId for ' ${ filePath } ' ` ) ;
2020-03-21 15:14:44 +01:00
}
2022-12-26 22:51:16 +01:00
if ( noteMeta ? . isClone ) {
2021-12-27 13:37:51 +01:00
if ( ! becca . getBranchFromChildAndParent ( noteId , parentNoteId ) ) {
2023-01-03 13:52:37 +01:00
new BBranch ( {
2021-12-27 13:37:51 +01:00
noteId ,
parentNoteId ,
isExpanded : noteMeta.isExpanded ,
prefix : noteMeta.prefix ,
notePosition : noteMeta.notePosition
} ) . save ( ) ;
}
2020-03-20 21:57:16 +01:00
return ;
}
2024-10-10 20:19:59 +03:00
let { mime , type : detectedType } = noteMeta ? noteMeta : detectFileTypeAndMime ( taskContext , filePath ) ;
const type = resolveNoteType ( detectedType ) ;
2024-07-28 23:39:25 +03:00
if ( mime == null ) {
2024-04-03 22:46:14 +03:00
throw new Error ( "Unable to resolve mime type." ) ;
}
2025-01-09 18:07:02 +02:00
if ( type !== "file" && type !== "image" ) {
2025-02-22 01:37:02 +02:00
content = processStringOrBuffer ( content ) ;
2020-03-20 21:57:16 +01:00
}
2025-01-02 13:47:44 +01:00
const noteTitle = getNoteTitle ( filePath , taskContext . data ? . replaceUnderscoresWithSpaces || false , noteMeta ) ;
2024-07-13 16:55:20 +03:00
2024-04-03 22:46:14 +03:00
content = processNoteContent ( noteMeta , type , mime , content , noteTitle || "" , filePath ) ;
2020-03-20 21:57:16 +01:00
2021-05-02 11:23:58 +02:00
let note = becca . getNote ( noteId ) ;
2020-03-20 21:57:16 +01:00
2021-12-15 22:36:45 +01:00
const isProtected = importRootNote . isProtected && protectedSessionService . isProtectedSessionAvailable ( ) ;
2020-03-20 21:57:16 +01:00
if ( note ) {
2021-12-15 22:36:45 +01:00
// only skeleton was created because of altered order of cloned notes in ZIP, we need to update
// https://github.com/zadam/trilium/issues/2440
if ( note . type === undefined ) {
2024-10-10 20:19:59 +03:00
note . type = type ;
2021-12-15 22:36:45 +01:00
note . mime = mime ;
2024-04-03 22:46:14 +03:00
note . title = noteTitle || "" ;
2021-12-15 22:36:45 +01:00
note . isProtected = isProtected ;
note . save ( ) ;
}
2020-06-20 12:31:38 +02:00
note . setContent ( content ) ;
2021-12-26 23:31:54 +01:00
2021-12-27 13:37:51 +01:00
if ( ! becca . getBranchFromChildAndParent ( noteId , parentNoteId ) ) {
2023-01-03 13:52:37 +01:00
new BBranch ( {
2021-12-27 13:37:51 +01:00
noteId ,
parentNoteId ,
2024-04-03 22:46:14 +03:00
isExpanded : noteMeta?.isExpanded ,
prefix : noteMeta?.prefix ,
notePosition : noteMeta?.notePosition
2021-12-27 13:37:51 +01:00
} ) . save ( ) ;
}
2025-03-10 19:14:46 +02:00
if ( opts ? . preserveIds ) {
firstNote = firstNote || note ;
}
2025-01-09 18:07:02 +02:00
} else {
( { note } = noteService . createNewNote ( {
2020-03-20 21:57:16 +01:00
parentNoteId : parentNoteId ,
2024-04-03 22:46:14 +03:00
title : noteTitle || "" ,
2020-03-20 21:57:16 +01:00
content : content ,
noteId ,
2024-10-10 20:19:59 +03:00
type ,
2020-03-20 21:57:16 +01:00
mime ,
2025-01-09 18:07:02 +02:00
prefix : noteMeta?.prefix || "" ,
2024-04-10 19:26:08 +03:00
isExpanded : ! ! noteMeta ? . isExpanded ,
2023-05-06 22:50:28 +02:00
// root notePosition should be ignored since it relates to the original document
2021-02-16 23:38:05 +01:00
// now import root should be placed after existing notes into new parent
2025-01-09 18:07:02 +02:00
notePosition : noteMeta && firstNote ? noteMeta.notePosition : undefined ,
isProtected : isProtected
2020-03-20 21:57:16 +01:00
} ) ) ;
2023-05-06 15:07:38 +02:00
createdNoteIds . add ( note . noteId ) ;
2020-03-21 15:14:44 +01:00
2020-06-20 12:31:38 +02:00
saveAttributes ( note , noteMeta ) ;
2020-03-20 21:57:16 +01:00
2023-05-06 22:50:28 +02:00
firstNote = firstNote || note ;
2020-03-20 21:57:16 +01:00
}
2025-01-09 18:07:02 +02:00
if ( ! noteMeta && ( type === "file" || type === "image" ) ) {
2020-03-20 21:57:16 +01:00
attributes . push ( {
noteId ,
2025-01-09 18:07:02 +02:00
type : "label" ,
name : "originalFileName" ,
2020-03-20 21:57:16 +01:00
value : path.basename ( filePath )
} ) ;
}
}
2025-06-20 20:56:25 +03:00
// we're running two passes in order to obtain critical information first (meta file and root)
const topLevelItems = new Set < string > ( ) ;
2024-04-03 22:46:14 +03:00
await readZipFile ( fileBuffer , async ( zipfile : yauzl.ZipFile , entry : yauzl.Entry ) = > {
2020-03-20 22:13:29 +01:00
const filePath = normalizeFilePath ( entry . fileName ) ;
2025-06-20 20:56:25 +03:00
// make sure that the meta file is loaded before the rest of the files is processed.
2025-01-09 18:07:02 +02:00
if ( filePath === "!!!meta.json" ) {
2020-06-27 00:40:35 +02:00
const content = await readContent ( zipfile , entry ) ;
2020-03-20 22:13:29 +01:00
2023-05-06 22:50:28 +02:00
metaFile = JSON . parse ( content . toString ( "utf-8" ) ) ;
2020-03-20 22:13:29 +01:00
}
2025-06-20 20:56:25 +03:00
// determine the root of the .zip (i.e. if it has only one top-level folder then the root is that folder, or the root of the archive if there are multiple top-level folders).
const firstSlash = filePath . indexOf ( "/" ) ;
const topLevelPath = ( firstSlash !== - 1 ? filePath . substring ( 0 , firstSlash ) : filePath ) ;
topLevelItems . add ( topLevelPath ) ;
2020-03-20 22:13:29 +01:00
zipfile . readEntry ( ) ;
} ) ;
2025-06-20 20:56:25 +03:00
topLevelPath = ( topLevelItems . size > 1 ? "" : topLevelItems . values ( ) . next ( ) . value ? ? "" ) ;
2024-04-03 22:46:14 +03:00
await readZipFile ( fileBuffer , async ( zipfile : yauzl.ZipFile , entry : yauzl.Entry ) = > {
2020-03-20 22:13:29 +01:00
const filePath = normalizeFilePath ( entry . fileName ) ;
if ( /\/$/ . test ( entry . fileName ) ) {
2020-06-20 12:31:38 +02:00
saveDirectory ( filePath ) ;
2025-01-09 18:07:02 +02:00
} else if ( filePath !== "!!!meta.json" ) {
2020-06-27 00:40:35 +02:00
const content = await readContent ( zipfile , entry ) ;
2020-03-20 22:13:29 +01:00
2020-06-20 12:31:38 +02:00
saveNote ( filePath , content ) ;
2020-03-20 22:13:29 +01:00
}
taskContext . increaseProgressCount ( ) ;
zipfile . readEntry ( ) ;
} ) ;
2020-03-20 21:57:16 +01:00
2023-05-06 15:07:38 +02:00
for ( const noteId of createdNoteIds ) {
2023-01-26 20:32:27 +01:00
const note = becca . getNote ( noteId ) ;
2024-04-03 22:46:14 +03:00
if ( ! note ) continue ;
2023-01-26 20:32:27 +01:00
await noteService . asyncPostProcessContent ( note , note . getContent ( ) ) ;
2020-03-20 21:57:16 +01:00
if ( ! metaFile ) {
2023-05-06 15:07:38 +02:00
// if there's no meta file, then the notes are created based on the order in that zip file but that
2022-12-26 10:38:31 +01:00
// is usually quite random, so we sort the notes in the way they would appear in the file manager
2025-01-09 18:07:02 +02:00
treeService . sortNotes ( noteId , "title" , false , true ) ;
2020-03-20 21:57:16 +01:00
}
taskContext . increaseProgressCount ( ) ;
}
// we're saving attributes and links only now so that all relation and link target notes
// are already in the database (we don't want to have "broken" relations, not even transitionally)
for ( const attr of attributes ) {
2025-01-09 18:07:02 +02:00
if ( attr . type !== "relation" || attr . value in becca . notes ) {
2023-01-03 13:52:37 +01:00
new BAttribute ( attr ) . save ( ) ;
2025-01-09 18:07:02 +02:00
} else {
2022-12-26 10:38:31 +01:00
log . info ( ` Relation not imported since the target note doesn't exist: ${ JSON . stringify ( attr ) } ` ) ;
2020-03-20 21:57:16 +01:00
}
}
2024-04-13 17:30:48 +03:00
if ( ! firstNote ) {
throw new Error ( "Unable to determine first note." ) ;
}
2020-03-20 21:57:16 +01:00
return firstNote ;
}
2024-04-03 22:46:14 +03:00
/** @returns path without leading or trailing slash and backslashes converted to forward ones */
function normalizeFilePath ( filePath : string ) : string {
2022-12-26 22:51:16 +01:00
filePath = filePath . replace ( /\\/g , "/" ) ;
if ( filePath . startsWith ( "/" ) ) {
filePath = filePath . substr ( 1 ) ;
}
if ( filePath . endsWith ( "/" ) ) {
filePath = filePath . substr ( 0 , filePath . length - 1 ) ;
}
return filePath ;
}
2024-04-03 22:46:14 +03:00
function streamToBuffer ( stream : Stream ) : Promise < Buffer > {
const chunks : Uint8Array [ ] = [ ] ;
2025-01-09 18:07:02 +02:00
stream . on ( "data" , ( chunk ) = > chunks . push ( chunk ) ) ;
2022-12-26 22:51:16 +01:00
2025-01-09 18:07:02 +02:00
return new Promise ( ( res , rej ) = > stream . on ( "end" , ( ) = > res ( Buffer . concat ( chunks ) ) ) ) ;
2022-12-26 22:51:16 +01:00
}
2025-03-10 16:20:48 +02:00
export function readContent ( zipfile : yauzl.ZipFile , entry : yauzl.Entry ) : Promise < Buffer > {
2022-12-26 22:51:16 +01:00
return new Promise ( ( res , rej ) = > {
2025-01-09 18:07:02 +02:00
zipfile . openReadStream ( entry , function ( err , readStream ) {
2022-12-26 22:51:16 +01:00
if ( err ) rej ( err ) ;
2024-04-03 22:46:14 +03:00
if ( ! readStream ) throw new Error ( "Unable to read content." ) ;
2022-12-26 22:51:16 +01:00
streamToBuffer ( readStream ) . then ( res ) ;
} ) ;
} ) ;
}
2025-03-10 16:20:48 +02:00
export function readZipFile ( buffer : Buffer , processEntryCallback : ( zipfile : yauzl.ZipFile , entry : yauzl.Entry ) = > Promise < void > ) {
return new Promise < void > ( ( res , rej ) = > {
2025-01-09 18:07:02 +02:00
yauzl . fromBuffer ( buffer , { lazyEntries : true , validateEntrySizes : false } , function ( err , zipfile ) {
2024-07-28 23:29:38 +03:00
if ( err ) rej ( err ) ;
2024-04-03 22:46:14 +03:00
if ( ! zipfile ) throw new Error ( "Unable to read zip file." ) ;
2022-12-26 22:51:16 +01:00
zipfile . readEntry ( ) ;
2025-01-09 18:07:02 +02:00
zipfile . on ( "entry" , async ( entry ) = > {
2024-07-28 23:29:38 +03:00
try {
await processEntryCallback ( zipfile , entry ) ;
} catch ( e ) {
rej ( e ) ;
}
} ) ;
2022-12-26 22:51:16 +01:00
zipfile . on ( "end" , res ) ;
} ) ;
} ) ;
}
2024-04-03 22:46:14 +03:00
function resolveNoteType ( type : string | undefined ) : NoteType {
2025-03-10 17:04:17 +02:00
// BC for ZIPs created in Trilium 0.57 and older
2025-01-09 18:07:02 +02:00
if ( type === "relation-map" ) {
return "relationMap" ;
} else if ( type === "note-map" ) {
return "noteMap" ;
} else if ( type === "web-view" ) {
return "webView" ;
2022-12-07 23:37:40 +01:00
}
2024-07-13 16:43:33 +03:00
if ( type && ( ALLOWED_NOTE_TYPES as readonly string [ ] ) . includes ( type ) ) {
return type as NoteType ;
} else {
return "text" ;
}
2022-12-07 23:37:40 +01:00
}
2025-03-31 00:27:22 +03:00
export function removeTriliumTags ( content : string ) {
const tagsToRemove = [
"<h1 data-trilium-h1>([^<]*)<\/h1>" ,
2025-03-31 00:58:35 +03:00
"<title data-trilium-title>([^<]*)<\/title>"
] ;
2025-03-31 00:27:22 +03:00
for ( const tag of tagsToRemove ) {
let re = new RegExp ( tag , "gi" ) ;
content = content . replace ( re , "" ) ;
}
2025-03-31 00:58:35 +03:00
// Remove ckeditor tags
content = content . replace ( / < d i v c l a s s = " c k - c o n t e n t " > ( . * ) < \ / d i v > / g m s , " $ 1 " ) ;
content = content . replace ( / < d i v c l a s s = " c o n t e n t " > ( . * ) < \ / d i v > / g m s , " $ 1 " ) ;
2025-03-31 00:27:22 +03:00
return content ;
}
2024-07-18 21:42:44 +03:00
export default {
2020-03-20 21:57:16 +01:00
importZip
2020-06-07 23:55:55 +02:00
} ;