A tool to migrate notes from Bear to Zettlr
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

219 lines
6.9 KiB

package bearnotes
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
"golang.org/x/text/unicode/norm"
"gopkg.in/yaml.v2"
)
// MigrateNotes takes a source directory (from), a destination directory (to),
// a tag configuration file (tagFile) and performs a Bear to Zettlr migration.
func MigrateNotes(from string, to string, tagFile string) error {
var tags map[string]TagOptions = make(map[string]TagOptions)
fmt.Printf("Reading the tag file from %s...\n", tagFile)
fileContent, err := ioutil.ReadFile(tagFile)
if err != nil {
return err
}
err = yaml.Unmarshal(fileContent, &tags)
if err != nil {
return err
}
fmt.Printf("Migrating Bear notes from %s to %s...\n", from, to)
var success int = 0
var allNotes int = 0
err = filepath.Walk(from,
func(p string, info os.FileInfo, err error) error {
if err != nil {
log.Printf("stat: %s: %s\n", p, err)
return nil
}
// If it's not a markdown file, skip it.
if info.IsDir() || !strings.HasSuffix(info.Name(), ".md") {
return nil
}
log.Printf("Processing %s...\n", info.Name())
allNotes++
// Load the note
content, err := ioutil.ReadFile(p)
if err != nil {
log.Printf("open: %s: %s\n", p, err)
return nil
}
note := LoadNote(string(content))
// Iterate over the note's tags to compute the target directory & handling strategy.
// Since a note can have multiple tags, the first tag that defines a valid (non-empty)
// target directory and/or handling strategy sets the value.
// If another one specifies a different value, we issue a warning.
var targetDir string
var handlingStrategy string
for i, tag := range note.Tags {
// Normalize tag names to prevent file not found errors because of Unicode encoding.
tag.Name = norm.NFC.String(tag.Name)
// And make it lowercase since all tags are lower-case in Bear.
tagName := strings.ToLower(tag.Name)
tagOption, ok := tags[tagName]
if !ok {
log.Printf("ERROR: Unknown tag name '%s' in %s! Re-run the discover command!\n", tagName, info.Name())
return nil
}
if tagOption.Ignore {
continue
}
// Rewrite the tag name as instructed
note.Tags[i].Name = tagOption.TargetTagName
if tagOption.TargetDirectory != "" && targetDir != "" && targetDir != tagOption.TargetDirectory {
log.Printf("WARNING: Target directory '%s' for tag '%s' conflict with directives (%s) from another tag. Continuing with existing value.\n", tagOption.TargetDirectory, tagName, targetDir)
} else if targetDir == "" {
targetDir = tagOption.TargetDirectory
}
if tagOption.HandlingStrategy != "" && handlingStrategy != "" && handlingStrategy != tagOption.HandlingStrategy {
log.Printf("WARNING: Handling strategy '%s' for tag '%s' conflict with directives (%s) from another tag. Continuing with existing value.\n", tagOption.HandlingStrategy, tagName, handlingStrategy)
} else if handlingStrategy == "" {
if tagOption.HandlingStrategy == "same-folder" || tagOption.HandlingStrategy == "one-note-per-folder" || tagOption.HandlingStrategy == "" {
handlingStrategy = tagOption.HandlingStrategy
} else {
log.Printf("WARNING: Unknown handling strategy '%s' for tag '%s'.\n", tagOption.HandlingStrategy, tagName)
}
}
}
// Compute the final target directory, based on the handling strategy
noteName := strings.TrimSuffix(info.Name(), ".md")
if handlingStrategy == "one-note-per-folder" {
targetDir = path.Join(to, targetDir, noteName)
} else if handlingStrategy == "same-folder" {
targetDir = path.Join(to, targetDir)
} else {
// If no tag set an handling strategy or if the note has no tag,
// then it goes at the root of the target directory
targetDir = to
}
// Creates all the directory hierarchy
err = os.MkdirAll(targetDir, 0755)
if err != nil {
log.Printf("mkdir: %s: %s\n", targetDir, err)
return nil
}
// Migrate embedded images
for i, image := range note.Images {
// Normalize filenames to prevent 'file not found' errors
imageFileName := filepath.Base(norm.NFC.String(image.Location))
source := filepath.Join(from, norm.NFC.String(image.Location))
destination := filepath.Join(targetDir, imageFileName)
_, err := os.Stat(destination)
if os.IsNotExist(err) {
// Copy the image only if we don't overwrite an existing one
err = copyFile(source, destination)
if os.IsNotExist(err) {
log.Printf("WARNING: source image '%s' in note %s cannot be found!\n", imageFileName, noteName)
} else if err != nil {
log.Printf("copy: %s -> %s: %s\n", source, destination, err)
return nil
}
} else if err != nil {
log.Printf("stat: %s: %s\n", destination, err)
return nil
} else {
log.Printf("WARNING: embedded image '%s' of note %s already exists in the target directory %s!\n", imageFileName, noteName, destination)
}
note.Images[i].Location = imageFileName
}
// Migrate file attachments
for i, file := range note.Files {
// Normalize filenames to prevent 'file not found' errors
fileName := filepath.Base(norm.NFC.String(file.Location))
source := filepath.Join(from, noteName, norm.NFC.String(file.Location))
destination := filepath.Join(targetDir, fileName)
_, err := os.Stat(destination)
if os.IsNotExist(err) {
// Copy the file attachment if we don't overwrite an existing one
err = copyFile(source, destination)
if os.IsNotExist(err) {
log.Printf("WARNING: source file '%s' in note %s cannot be found!\n", fileName, noteName)
} else if err != nil {
log.Printf("copy: %s -> %s: %s\n", source, destination, err)
return nil
}
} else if err != nil {
log.Printf("stat: %s: %s\n", destination, err)
return nil
} else {
log.Printf("WARNING: file attachment '%s' of note %s already exists in the target directory %s!\n", fileName, noteName, destination)
}
note.Files[i].Location = fileName
}
// Write back the updated note
newNote := note.WriteNote()
targetNoteFileName := filepath.Join(targetDir, info.Name())
fd, err := os.Create(targetNoteFileName)
if err != nil {
log.Printf("open: %s: %s\n", targetNoteFileName, err)
return nil
}
defer fd.Close()
fd.WriteString(newNote)
success++
return nil
})
if err != nil {
return err
}
fmt.Println()
fmt.Printf("Processed %d notes with %d successes and %d failures\n", allNotes, success, allNotes-success)
return nil
}
// from https://opensource.com/article/18/6/copying-files-go
func copyFile(src string, dest string) error {
sourceFileStat, err := os.Stat(src)
if err != nil {
return err
}
if !sourceFileStat.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", src)
}
source, err := os.Open(src)
if err != nil {
return err
}
defer source.Close()
destination, err := os.Create(dest)
if err != nil {
return err
}
defer destination.Close()
_, err = io.Copy(destination, source)
return err
}