mirror of
https://github.com/dnote/dnote
synced 2026-03-14 14:35:50 +01:00
985 lines
24 KiB
Go
985 lines
24 KiB
Go
/* Copyright 2025 Dnote Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
// Package migrate provides migration logic for both sqlite and
|
|
// legacy JSON-based notes used until v0.4.x releases
|
|
package migrate
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/dnote/dnote/pkg/cli/context"
|
|
"github.com/dnote/dnote/pkg/cli/log"
|
|
"github.com/dnote/dnote/pkg/cli/utils"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
var (
|
|
schemaFilename = "schema"
|
|
backupDirName = ".dnote-bak"
|
|
)
|
|
|
|
// migration IDs
|
|
const (
|
|
_ = iota
|
|
legacyMigrationV1
|
|
legacyMigrationV2
|
|
legacyMigrationV3
|
|
legacyMigrationV4
|
|
legacyMigrationV5
|
|
legacyMigrationV6
|
|
legacyMigrationV7
|
|
legacyMigrationV8
|
|
)
|
|
|
|
var migrationSequence = []int{
|
|
legacyMigrationV1,
|
|
legacyMigrationV2,
|
|
legacyMigrationV3,
|
|
legacyMigrationV4,
|
|
legacyMigrationV5,
|
|
legacyMigrationV6,
|
|
legacyMigrationV7,
|
|
legacyMigrationV8,
|
|
}
|
|
|
|
type schema struct {
|
|
CurrentVersion int `yaml:"current_version"`
|
|
}
|
|
|
|
func makeSchema(complete bool) schema {
|
|
s := schema{}
|
|
|
|
var CurrentVersion int
|
|
if complete {
|
|
CurrentVersion = len(migrationSequence)
|
|
}
|
|
|
|
s.CurrentVersion = CurrentVersion
|
|
|
|
return s
|
|
}
|
|
|
|
func genUUID() (string, error) {
|
|
res, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "generating a random uuid")
|
|
}
|
|
|
|
return res.String(), nil
|
|
|
|
}
|
|
|
|
// Legacy performs migration on JSON-based dnote if necessary
|
|
func Legacy(ctx context.DnoteCtx) error {
|
|
// If schema does not exist, no need run a legacy migration
|
|
schemaPath := getSchemaPath(ctx)
|
|
ok, err := utils.FileExists(schemaPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "checking if schema exists")
|
|
}
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
unrunMigrations, err := getUnrunMigrations(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to get unrun migrations")
|
|
}
|
|
|
|
for _, mig := range unrunMigrations {
|
|
log.Debug("running legacy migration %d\n", mig)
|
|
if err := performMigration(ctx, mig); err != nil {
|
|
return errors.Wrapf(err, "running migration #%d", mig)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// performMigration backs up current .dnote data, performs migration, and
|
|
// restores or cleans backups depending on if there is an error
|
|
func performMigration(ctx context.DnoteCtx, migrationID int) error {
|
|
// legacyMigrationV8 is the final migration of the legacy JSON Dnote migration
|
|
// migrate to sqlite and return
|
|
if migrationID == legacyMigrationV8 {
|
|
if err := migrateToV8(ctx); err != nil {
|
|
return errors.Wrap(err, "migrating to sqlite")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := backupDnoteDir(ctx); err != nil {
|
|
return errors.Wrap(err, "Failed to back up dnote directory")
|
|
}
|
|
|
|
var migrationError error
|
|
|
|
switch migrationID {
|
|
case legacyMigrationV1:
|
|
migrationError = migrateToV1(ctx)
|
|
case legacyMigrationV2:
|
|
migrationError = migrateToV2(ctx)
|
|
case legacyMigrationV3:
|
|
migrationError = migrateToV3(ctx)
|
|
case legacyMigrationV4:
|
|
migrationError = migrateToV4(ctx)
|
|
case legacyMigrationV5:
|
|
migrationError = migrateToV5(ctx)
|
|
case legacyMigrationV6:
|
|
migrationError = migrateToV6(ctx)
|
|
case legacyMigrationV7:
|
|
migrationError = migrateToV7(ctx)
|
|
default:
|
|
return errors.Errorf("Unrecognized migration id %d", migrationID)
|
|
}
|
|
|
|
if migrationError != nil {
|
|
if err := restoreBackup(ctx); err != nil {
|
|
panic(errors.Wrap(err, "Failed to restore backup for a failed migration"))
|
|
}
|
|
|
|
return errors.Wrapf(migrationError, "Failed to perform migration #%d", migrationID)
|
|
}
|
|
|
|
if err := clearBackup(ctx); err != nil {
|
|
return errors.Wrap(err, "Failed to clear backup")
|
|
}
|
|
|
|
if err := updateSchemaVersion(ctx, migrationID); err != nil {
|
|
return errors.Wrap(err, "Failed to update schema version")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// backupDnoteDir backs up the dnote directory to a temporary backup directory
|
|
func backupDnoteDir(ctx context.DnoteCtx) error {
|
|
srcPath := fmt.Sprintf("%s/.dnote", ctx.Paths.Home)
|
|
tmpPath := fmt.Sprintf("%s/%s", ctx.Paths.Home, backupDirName)
|
|
|
|
if err := utils.CopyDir(srcPath, tmpPath); err != nil {
|
|
return errors.Wrap(err, "Failed to copy the .dnote directory")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func restoreBackup(ctx context.DnoteCtx) error {
|
|
var err error
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
log.Printf(`Failed to restore backup for a failed migration.
|
|
Don't worry. Your data is still intact in the backup directory.
|
|
Get help on https://github.com/dnote/dnote/pkg/cli/issues`)
|
|
}
|
|
}()
|
|
|
|
srcPath := fmt.Sprintf("%s/.dnote", ctx.Paths.Home)
|
|
backupPath := fmt.Sprintf("%s/%s", ctx.Paths.Home, backupDirName)
|
|
|
|
if err = os.RemoveAll(srcPath); err != nil {
|
|
return errors.Wrapf(err, "Failed to clear current dnote data at %s", backupPath)
|
|
}
|
|
|
|
if err = os.Rename(backupPath, srcPath); err != nil {
|
|
return errors.Wrap(err, `Failed to copy backup data to the original directory.`)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func clearBackup(ctx context.DnoteCtx) error {
|
|
backupPath := fmt.Sprintf("%s/%s", ctx.Paths.Home, backupDirName)
|
|
|
|
if err := os.RemoveAll(backupPath); err != nil {
|
|
return errors.Wrapf(err, "Failed to remove backup at %s", backupPath)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getSchemaPath returns the path to the file containing schema info
|
|
func getSchemaPath(ctx context.DnoteCtx) string {
|
|
return fmt.Sprintf("%s/%s", ctx.Paths.LegacyDnote, schemaFilename)
|
|
}
|
|
|
|
func readSchema(ctx context.DnoteCtx) (schema, error) {
|
|
var ret schema
|
|
|
|
path := getSchemaPath(ctx)
|
|
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ret, errors.Wrap(err, "Failed to read schema file")
|
|
}
|
|
|
|
err = yaml.Unmarshal(b, &ret)
|
|
if err != nil {
|
|
return ret, errors.Wrap(err, "Failed to unmarshal the schema JSON")
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func writeSchema(ctx context.DnoteCtx, s schema) error {
|
|
path := getSchemaPath(ctx)
|
|
d, err := yaml.Marshal(&s)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to marshal schema into yaml")
|
|
}
|
|
|
|
if err := os.WriteFile(path, d, 0644); err != nil {
|
|
return errors.Wrap(err, "Failed to write schema file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getUnrunMigrations(ctx context.DnoteCtx) ([]int, error) {
|
|
var ret []int
|
|
|
|
schema, err := readSchema(ctx)
|
|
if err != nil {
|
|
return ret, errors.Wrap(err, "Failed to read schema")
|
|
}
|
|
|
|
log.Debug("current legacy schema: %d\n", schema.CurrentVersion)
|
|
|
|
if schema.CurrentVersion == len(migrationSequence) {
|
|
return ret, nil
|
|
}
|
|
|
|
nextVersion := schema.CurrentVersion
|
|
ret = migrationSequence[nextVersion:]
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func updateSchemaVersion(ctx context.DnoteCtx, mID int) error {
|
|
s, err := readSchema(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to read schema")
|
|
}
|
|
|
|
s.CurrentVersion = mID
|
|
|
|
err = writeSchema(ctx, s)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to write schema")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/***** snapshots **/
|
|
|
|
// v2
|
|
type migrateToV2PreNote struct {
|
|
UID string
|
|
Content string
|
|
AddedOn int64
|
|
}
|
|
type migrateToV2PostNote struct {
|
|
UUID string `json:"uuid"`
|
|
Content string `json:"content"`
|
|
AddedOn int64 `json:"added_on"`
|
|
EditedOn int64 `json:"editd_on"`
|
|
}
|
|
type migrateToV2PreBook []migrateToV2PreNote
|
|
type migrateToV2PostBook struct {
|
|
Name string `json:"name"`
|
|
Notes []migrateToV2PostNote `json:"notes"`
|
|
}
|
|
type migrateToV2PreDnote map[string]migrateToV2PreBook
|
|
type migrateToV2PostDnote map[string]migrateToV2PostBook
|
|
|
|
//v3
|
|
var (
|
|
migrateToV3ActionAddNote = "add_note"
|
|
migrateToV3ActionAddBook = "add_book"
|
|
)
|
|
|
|
type migrateToV3Note struct {
|
|
UUID string `json:"uuid"`
|
|
Content string `json:"content"`
|
|
AddedOn int64 `json:"added_on"`
|
|
EditedOn int64 `json:"edited_on"`
|
|
}
|
|
type migrateToV3Book struct {
|
|
UUID string `json:"uuid"`
|
|
Name string `json:"name"`
|
|
Notes []migrateToV3Note `json:"notes"`
|
|
}
|
|
type migrateToV3Dnote map[string]migrateToV3Book
|
|
type migrateToV3Action struct {
|
|
Type string `json:"type"`
|
|
Data map[string]interface{} `json:"data"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
// v4
|
|
type migrateToV4PreConfig struct {
|
|
Book string
|
|
APIKey string
|
|
}
|
|
type migrateToV4PostConfig struct {
|
|
Editor string
|
|
APIKey string
|
|
}
|
|
|
|
// v5
|
|
type migrateToV5AddNoteData struct {
|
|
NoteUUID string `json:"note_uuid"`
|
|
BookName string `json:"book_name"`
|
|
Content string `json:"content"`
|
|
}
|
|
type migrateToV5RemoveNoteData struct {
|
|
NoteUUID string `json:"note_uuid"`
|
|
BookName string `json:"book_name"`
|
|
}
|
|
type migrateToV5AddBookData struct {
|
|
BookName string `json:"book_name"`
|
|
}
|
|
type migrateToV5RemoveBookData struct {
|
|
BookName string `json:"book_name"`
|
|
}
|
|
type migrateToV5PreEditNoteData struct {
|
|
NoteUUID string `json:"note_uuid"`
|
|
BookName string `json:"book_name"`
|
|
Content string `json:"content"`
|
|
}
|
|
type migrateToV5PostEditNoteData struct {
|
|
NoteUUID string `json:"note_uuid"`
|
|
FromBook string `json:"from_book"`
|
|
ToBook string `json:"to_book"`
|
|
Content string `json:"content"`
|
|
}
|
|
type migrateToV5PreAction struct {
|
|
ID int `json:"id"`
|
|
Type string `json:"type"`
|
|
Data json.RawMessage `json:"data"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
type migrateToV5PostAction struct {
|
|
UUID string `json:"uuid"`
|
|
Schema int `json:"schema"`
|
|
Type string `json:"type"`
|
|
Data json.RawMessage `json:"data"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
var (
|
|
migrateToV5ActionAddNote = "add_note"
|
|
migrateToV5ActionRemoveNote = "remove_note"
|
|
migrateToV5ActionEditNote = "edit_note"
|
|
migrateToV5ActionAddBook = "add_book"
|
|
migrateToV5ActionRemoveBook = "remove_book"
|
|
)
|
|
|
|
// v6
|
|
type migrateToV6PreNote struct {
|
|
UUID string `json:"uuid"`
|
|
Content string `json:"content"`
|
|
AddedOn int64 `json:"added_on"`
|
|
EditedOn int64 `json:"edited_on"`
|
|
}
|
|
type migrateToV6PostNote struct {
|
|
UUID string `json:"uuid"`
|
|
Content string `json:"content"`
|
|
AddedOn int64 `json:"added_on"`
|
|
EditedOn int64 `json:"edited_on"`
|
|
// Make a pointer to test absent values
|
|
Public *bool `json:"public"`
|
|
}
|
|
type migrateToV6PreBook struct {
|
|
Name string `json:"name"`
|
|
Notes []migrateToV6PreNote `json:"notes"`
|
|
}
|
|
type migrateToV6PostBook struct {
|
|
Name string `json:"name"`
|
|
Notes []migrateToV6PostNote `json:"notes"`
|
|
}
|
|
type migrateToV6PreDnote map[string]migrateToV6PreBook
|
|
type migrateToV6PostDnote map[string]migrateToV6PostBook
|
|
|
|
// v7
|
|
var migrateToV7ActionTypeEditNote = "edit_note"
|
|
|
|
type migrateToV7Action struct {
|
|
UUID string `json:"uuid"`
|
|
Schema int `json:"schema"`
|
|
Type string `json:"type"`
|
|
Data json.RawMessage `json:"data"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
type migrateToV7EditNoteDataV1 struct {
|
|
NoteUUID string `json:"note_uuid"`
|
|
FromBook string `json:"from_book"`
|
|
ToBook string `json:"to_book"`
|
|
Content string `json:"content"`
|
|
}
|
|
type migrateToV7EditNoteDataV2 struct {
|
|
NoteUUID string `json:"note_uuid"`
|
|
FromBook string `json:"from_book"`
|
|
ToBook *string `json:"to_book"`
|
|
Content *string `json:"content"`
|
|
Public *bool `json:"public"`
|
|
}
|
|
|
|
// v8
|
|
type migrateToV8Action struct {
|
|
UUID string `json:"uuid"`
|
|
Schema int `json:"schema"`
|
|
Type string `json:"type"`
|
|
Data json.RawMessage `json:"data"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
type migrateToV8Note struct {
|
|
UUID string `json:"uuid"`
|
|
Content string `json:"content"`
|
|
AddedOn int64 `json:"added_on"`
|
|
EditedOn int64 `json:"edited_on"`
|
|
// Make a pointer to test absent values
|
|
Public *bool `json:"public"`
|
|
}
|
|
type migrateToV8Book struct {
|
|
Name string `json:"name"`
|
|
Notes []migrateToV8Note `json:"notes"`
|
|
}
|
|
type migrateToV8Dnote map[string]migrateToV8Book
|
|
type migrateToV8Timestamp struct {
|
|
LastUpgrade int64 `yaml:"last_upgrade"`
|
|
Bookmark int `yaml:"bookmark"`
|
|
LastAction int64 `yaml:"last_action"`
|
|
}
|
|
|
|
var migrateToV8SystemKeyLastUpgrade = "last_upgrade"
|
|
var migrateToV8SystemKeyLastAction = "last_action"
|
|
var migrateToV8SystemKeyBookMark = "bookmark"
|
|
|
|
/***** migrations **/
|
|
|
|
// migrateToV1 deletes YAML archive if exists
|
|
func migrateToV1(ctx context.DnoteCtx) error {
|
|
yamlPath := fmt.Sprintf("%s/%s", ctx.Paths.Home, ".dnote-yaml-archived")
|
|
ok, err := utils.FileExists(yamlPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "checking if yaml file exists")
|
|
}
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if err := os.Remove(yamlPath); err != nil {
|
|
return errors.Wrap(err, "Failed to delete .dnote archive")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func migrateToV2(ctx context.DnoteCtx) error {
|
|
notePath := fmt.Sprintf("%s/dnote", ctx.Paths.LegacyDnote)
|
|
|
|
b, err := os.ReadFile(notePath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to read the note file")
|
|
}
|
|
|
|
var preDnote migrateToV2PreDnote
|
|
postDnote := migrateToV2PostDnote{}
|
|
|
|
err = json.Unmarshal(b, &preDnote)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
|
|
}
|
|
|
|
for bookName, book := range preDnote {
|
|
var notes = make([]migrateToV2PostNote, 0, len(book))
|
|
for _, note := range book {
|
|
noteUUID, err := genUUID()
|
|
if err != nil {
|
|
return errors.Wrap(err, "generating uuid")
|
|
}
|
|
|
|
newNote := migrateToV2PostNote{
|
|
UUID: noteUUID,
|
|
Content: note.Content,
|
|
AddedOn: note.AddedOn,
|
|
EditedOn: 0,
|
|
}
|
|
|
|
notes = append(notes, newNote)
|
|
}
|
|
|
|
b := migrateToV2PostBook{
|
|
Name: bookName,
|
|
Notes: notes,
|
|
}
|
|
|
|
postDnote[bookName] = b
|
|
}
|
|
|
|
d, err := json.MarshalIndent(postDnote, "", " ")
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
|
|
}
|
|
|
|
err = os.WriteFile(notePath, d, 0644)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to write the new dnote into the file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// migrateToV3 generates actions for existing dnote
|
|
func migrateToV3(ctx context.DnoteCtx) error {
|
|
notePath := fmt.Sprintf("%s/dnote", ctx.Paths.LegacyDnote)
|
|
actionsPath := fmt.Sprintf("%s/actions", ctx.Paths.LegacyDnote)
|
|
|
|
b, err := os.ReadFile(notePath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to read the note file")
|
|
}
|
|
|
|
var dnote migrateToV3Dnote
|
|
|
|
err = json.Unmarshal(b, &dnote)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
|
|
}
|
|
|
|
var actions []migrateToV3Action
|
|
|
|
for bookName, book := range dnote {
|
|
// Find the minimum added_on timestamp from the notes that belong to the book
|
|
// to give timstamp to the add_book action.
|
|
// Logically add_book must have happened no later than the first add_note
|
|
// to the book in order for sync to work.
|
|
minTs := time.Now().Unix()
|
|
for _, note := range book.Notes {
|
|
if note.AddedOn < minTs {
|
|
minTs = note.AddedOn
|
|
}
|
|
}
|
|
|
|
action := migrateToV3Action{
|
|
Type: migrateToV3ActionAddBook,
|
|
Data: map[string]interface{}{
|
|
"book_name": bookName,
|
|
},
|
|
Timestamp: minTs,
|
|
}
|
|
actions = append(actions, action)
|
|
|
|
for _, note := range book.Notes {
|
|
action := migrateToV3Action{
|
|
Type: migrateToV3ActionAddNote,
|
|
Data: map[string]interface{}{
|
|
"note_uuid": note.UUID,
|
|
"book_name": book.Name,
|
|
"content": note.Content,
|
|
},
|
|
Timestamp: note.AddedOn,
|
|
}
|
|
actions = append(actions, action)
|
|
}
|
|
}
|
|
|
|
a, err := json.Marshal(actions)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to marshal actions into JSON")
|
|
}
|
|
|
|
err = os.WriteFile(actionsPath, a, 0644)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to write the actions into a file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getEditorCommand() string {
|
|
editor := os.Getenv("EDITOR")
|
|
|
|
switch editor {
|
|
case "atom":
|
|
return "atom -w"
|
|
case "subl":
|
|
return "subl -n -w"
|
|
case "mate":
|
|
return "mate -w"
|
|
case "vim":
|
|
return "vim"
|
|
case "nano":
|
|
return "nano"
|
|
case "emacs":
|
|
return "emacs"
|
|
default:
|
|
return "vi"
|
|
}
|
|
}
|
|
|
|
func migrateToV4(ctx context.DnoteCtx) error {
|
|
configPath := fmt.Sprintf("%s/dnoterc", ctx.Paths.LegacyDnote)
|
|
|
|
b, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to read the config file")
|
|
}
|
|
|
|
var preConfig migrateToV4PreConfig
|
|
err = yaml.Unmarshal(b, &preConfig)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to unmarshal existing config into JSON")
|
|
}
|
|
|
|
postConfig := migrateToV4PostConfig{
|
|
APIKey: preConfig.APIKey,
|
|
Editor: getEditorCommand(),
|
|
}
|
|
|
|
data, err := yaml.Marshal(postConfig)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to marshal config into JSON")
|
|
}
|
|
|
|
err = os.WriteFile(configPath, data, 0644)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to write the config into a file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// migrateToV5 migrates actions
|
|
func migrateToV5(ctx context.DnoteCtx) error {
|
|
actionsPath := fmt.Sprintf("%s/actions", ctx.Paths.LegacyDnote)
|
|
|
|
b, err := os.ReadFile(actionsPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading the actions file")
|
|
}
|
|
|
|
var actions []migrateToV5PreAction
|
|
err = json.Unmarshal(b, &actions)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unmarshalling actions from JSON")
|
|
}
|
|
|
|
result := []migrateToV5PostAction{}
|
|
|
|
for _, action := range actions {
|
|
var data json.RawMessage
|
|
|
|
switch action.Type {
|
|
case migrateToV5ActionEditNote:
|
|
var oldData migrateToV5PreEditNoteData
|
|
if err = json.Unmarshal(action.Data, &oldData); err != nil {
|
|
return errors.Wrapf(err, "unmarshalling old data of an edit note action %d", action.ID)
|
|
}
|
|
|
|
migratedData := migrateToV5PostEditNoteData{
|
|
NoteUUID: oldData.NoteUUID,
|
|
FromBook: oldData.BookName,
|
|
Content: oldData.Content,
|
|
}
|
|
b, err = json.Marshal(migratedData)
|
|
if err != nil {
|
|
return errors.Wrap(err, "marshalling data")
|
|
}
|
|
|
|
data = b
|
|
default:
|
|
data = action.Data
|
|
}
|
|
|
|
actionUUID, err := genUUID()
|
|
if err != nil {
|
|
return errors.Wrap(err, "generating UUID")
|
|
}
|
|
|
|
migrated := migrateToV5PostAction{
|
|
UUID: actionUUID,
|
|
Schema: 1,
|
|
Type: action.Type,
|
|
Data: data,
|
|
Timestamp: action.Timestamp,
|
|
}
|
|
|
|
result = append(result, migrated)
|
|
}
|
|
|
|
a, err := json.Marshal(result)
|
|
if err != nil {
|
|
return errors.Wrap(err, "marshalling result into JSON")
|
|
}
|
|
err = os.WriteFile(actionsPath, a, 0644)
|
|
if err != nil {
|
|
return errors.Wrap(err, "writing the result into a file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// migrateToV6 adds a 'public' field to notes
|
|
func migrateToV6(ctx context.DnoteCtx) error {
|
|
notePath := fmt.Sprintf("%s/dnote", ctx.Paths.LegacyDnote)
|
|
|
|
b, err := os.ReadFile(notePath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to read the note file")
|
|
}
|
|
|
|
var preDnote migrateToV6PreDnote
|
|
postDnote := migrateToV6PostDnote{}
|
|
|
|
err = json.Unmarshal(b, &preDnote)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to unmarshal existing dnote into JSON")
|
|
}
|
|
|
|
for bookName, book := range preDnote {
|
|
var notes = make([]migrateToV6PostNote, 0, len(book.Notes))
|
|
public := false
|
|
for _, note := range book.Notes {
|
|
newNote := migrateToV6PostNote{
|
|
UUID: note.UUID,
|
|
Content: note.Content,
|
|
AddedOn: note.AddedOn,
|
|
EditedOn: note.EditedOn,
|
|
Public: &public,
|
|
}
|
|
|
|
notes = append(notes, newNote)
|
|
}
|
|
|
|
b := migrateToV6PostBook{
|
|
Name: bookName,
|
|
Notes: notes,
|
|
}
|
|
|
|
postDnote[bookName] = b
|
|
}
|
|
|
|
d, err := json.MarshalIndent(postDnote, "", " ")
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to marshal new dnote into JSON")
|
|
}
|
|
|
|
err = os.WriteFile(notePath, d, 0644)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Failed to write the new dnote into the file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// migrateToV7 migrates data of edit_note action to the proper version which is
|
|
// EditNoteDataV2. Due to a bug, edit logged actions with schema version '2'
|
|
// but with a data of EditNoteDataV1. https://github.com/dnote/dnote/pkg/cli/issues/107
|
|
func migrateToV7(ctx context.DnoteCtx) error {
|
|
actionPath := fmt.Sprintf("%s/actions", ctx.Paths.LegacyDnote)
|
|
|
|
b, err := os.ReadFile(actionPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading actions file")
|
|
}
|
|
|
|
var preActions []migrateToV7Action
|
|
postActions := []migrateToV7Action{}
|
|
err = json.Unmarshal(b, &preActions)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unmarshalling existing actions")
|
|
}
|
|
|
|
for _, action := range preActions {
|
|
var newAction migrateToV7Action
|
|
|
|
if action.Type == migrateToV7ActionTypeEditNote {
|
|
var oldData migrateToV7EditNoteDataV1
|
|
if e := json.Unmarshal(action.Data, &oldData); e != nil {
|
|
return errors.Wrapf(e, "unmarshalling data of action with uuid %s", action.Data)
|
|
}
|
|
|
|
newData := migrateToV7EditNoteDataV2{
|
|
NoteUUID: oldData.NoteUUID,
|
|
FromBook: oldData.FromBook,
|
|
ToBook: nil,
|
|
Content: &oldData.Content,
|
|
Public: nil,
|
|
}
|
|
d, e := json.Marshal(newData)
|
|
if e != nil {
|
|
return errors.Wrapf(e, "marshalling new data of action with uuid %s", action.Data)
|
|
}
|
|
|
|
newAction = migrateToV7Action{
|
|
UUID: action.UUID,
|
|
Schema: action.Schema,
|
|
Type: action.Type,
|
|
Timestamp: action.Timestamp,
|
|
Data: d,
|
|
}
|
|
} else {
|
|
newAction = action
|
|
}
|
|
|
|
postActions = append(postActions, newAction)
|
|
}
|
|
|
|
d, err := json.Marshal(postActions)
|
|
if err != nil {
|
|
return errors.Wrap(err, "marshalling new actions")
|
|
}
|
|
|
|
err = os.WriteFile(actionPath, d, 0644)
|
|
if err != nil {
|
|
return errors.Wrap(err, "writing new actions to a file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// migrateToV8 migrates dnote data to sqlite database
|
|
func migrateToV8(ctx context.DnoteCtx) error {
|
|
tx, err := ctx.DB.Begin()
|
|
if err != nil {
|
|
return errors.Wrap(err, "beginning a transaction")
|
|
}
|
|
|
|
// 1. Migrate the the dnote file
|
|
dnoteFilePath := fmt.Sprintf("%s/dnote", ctx.Paths.LegacyDnote)
|
|
b, err := os.ReadFile(dnoteFilePath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading the notes")
|
|
}
|
|
|
|
var dnote migrateToV8Dnote
|
|
err = json.Unmarshal(b, &dnote)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unmarshalling notes to JSON")
|
|
}
|
|
|
|
for bookName, book := range dnote {
|
|
bookUUIDResult, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return errors.Wrap(err, "generating uuid")
|
|
}
|
|
|
|
bookUUID := bookUUIDResult.String()
|
|
|
|
_, err = tx.Exec(`INSERT INTO books (uuid, label) VALUES (?, ?)`, bookUUID, bookName)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return errors.Wrapf(err, "inserting book %s", book.Name)
|
|
}
|
|
|
|
for _, note := range book.Notes {
|
|
_, err = tx.Exec(`INSERT INTO notes
|
|
(uuid, book_uuid, content, added_on, edited_on, public)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`, note.UUID, bookUUID, note.Content, note.AddedOn, note.EditedOn, note.Public)
|
|
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return errors.Wrapf(err, "inserting the note %s", note.UUID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Migrate the actions file
|
|
actionsPath := fmt.Sprintf("%s/actions", ctx.Paths.LegacyDnote)
|
|
b, err = os.ReadFile(actionsPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading the actions")
|
|
}
|
|
|
|
var actions []migrateToV8Action
|
|
err = json.Unmarshal(b, &actions)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unmarshalling actions from JSON")
|
|
}
|
|
|
|
for _, action := range actions {
|
|
_, err = tx.Exec(`INSERT INTO actions
|
|
(uuid, schema, type, data, timestamp)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`, action.UUID, action.Schema, action.Type, action.Data, action.Timestamp)
|
|
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return errors.Wrapf(err, "inserting the action %s", action.UUID)
|
|
}
|
|
}
|
|
|
|
// 3. Migrate the timestamps file
|
|
timestampsPath := fmt.Sprintf("%s/timestamps", ctx.Paths.LegacyDnote)
|
|
b, err = os.ReadFile(timestampsPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading the timestamps")
|
|
}
|
|
|
|
var timestamp migrateToV8Timestamp
|
|
err = yaml.Unmarshal(b, ×tamp)
|
|
if err != nil {
|
|
return errors.Wrap(err, "unmarshalling timestamps from YAML")
|
|
}
|
|
|
|
_, err = tx.Exec(`INSERT INTO system (key, value) VALUES (?, ?)`,
|
|
migrateToV8SystemKeyLastUpgrade, timestamp.LastUpgrade)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return errors.Wrap(err, "inserting the last_upgrade value")
|
|
}
|
|
_, err = tx.Exec(`INSERT INTO system (key, value) VALUES (?, ?)`,
|
|
migrateToV8SystemKeyLastAction, timestamp.LastAction)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return errors.Wrap(err, "inserting the last_action value")
|
|
}
|
|
_, err = tx.Exec(`INSERT INTO system (key, value) VALUES (?, ?)`,
|
|
migrateToV8SystemKeyBookMark, timestamp.Bookmark)
|
|
if err != nil {
|
|
tx.Rollback()
|
|
return errors.Wrap(err, "inserting the bookmark value")
|
|
}
|
|
|
|
tx.Commit()
|
|
|
|
if err := os.RemoveAll(dnoteFilePath); err != nil {
|
|
return errors.Wrap(err, "removing the old dnote file")
|
|
}
|
|
if err := os.RemoveAll(actionsPath); err != nil {
|
|
return errors.Wrap(err, "removing the actions file")
|
|
}
|
|
if err := os.RemoveAll(timestampsPath); err != nil {
|
|
return errors.Wrap(err, "removing the timestamps file")
|
|
}
|
|
schemaPath := fmt.Sprintf("%s/schema", ctx.Paths.LegacyDnote)
|
|
if err := os.RemoveAll(schemaPath); err != nil {
|
|
return errors.Wrap(err, "removing the schema file")
|
|
}
|
|
|
|
return nil
|
|
}
|