Skip to content

Commit

Permalink
Add backup and restore functionality and enhance chat format
Browse files Browse the repository at this point in the history
This commit introduces an automatic backup and restoration feature which will help in preserving user's data. It also modifies the chat format to improve readability and user experience. Additionally, minor changes have been made to improve code structure and maintainability.
  • Loading branch information
qianlifeng committed Dec 17, 2023
1 parent 760eaeb commit fa307d4
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 14 deletions.
22 changes: 17 additions & 5 deletions Wox.UI.Flutter/wox/lib/components/wox_preview_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,23 @@ class WoxPreviewView extends StatelessWidget {
cardColor: Colors.transparent,
);
contentWidget = Markdown(
data: woxPreview.previewData,
padding: EdgeInsets.zero,
selectable: true,
styleSheet: MarkdownStyleSheet.fromTheme(styleTheme),
);
data: woxPreview.previewData,
padding: EdgeInsets.zero,
selectable: true,
styleSheet: MarkdownStyleSheet.fromTheme(styleTheme).copyWith(
horizontalRuleDecoration: BoxDecoration(
border: Border(
top: BorderSide(
color: fromCssColor(woxTheme.previewFontColor).withOpacity(0.6),
width: 1,
),
bottom: const BorderSide(
color: Colors.transparent,
width: 10,
),
),
),
));
} else if (woxPreview.previewType == WoxPreviewTypeEnum.WOX_PREVIEW_TYPE_TEXT.code) {
contentWidget = SelectableText(woxPreview.previewData, style: TextStyle(color: fromCssColor(woxTheme.previewFontColor)));
} else if (woxPreview.previewType == WoxPreviewTypeEnum.WOX_PREVIEW_TYPE_IMAGE.code) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ class WoxLauncherController extends GetxController implements WoxLauncherInterfa
return;
}

Logger.instance.info("Received message: ${msg.toJson()}");
Logger.instance.info("Received message: ${msg.method}");
if (msg.method == "ToggleApp") {
toggleApp(ShowAppParams.fromJson(msg.data));
} else if (msg.method == "HideApp") {
Expand Down
3 changes: 2 additions & 1 deletion Wox/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ require (
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/otiai10/copy v1.14.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/saracen/zipextra v0.0.0-20220303013732-0187cb0159ea // indirect
Expand All @@ -64,7 +65,7 @@ require (
golang.design/x/mainthread v0.3.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.26.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions Wox/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E=
github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/parsiya/golnk v0.0.0-20221103095132-740a4c27c4ff h1:japdIZgV4tJIgn7NqUD7mAkLiPRsPK5LXVgjNwFtDA4=
Expand Down Expand Up @@ -162,6 +164,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
26 changes: 19 additions & 7 deletions Wox/plugin/system/chatgpt.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,18 @@ type Chat struct {
func (c *Chat) Format() string {
var result string
for _, conversation := range c.Conversations {
if strings.HasPrefix(conversation.Text, "GPT:") {
conversation.Text = strings.TrimPrefix(conversation.Text, "GPT:")
}

lines := "\n\n"
nick := "You"
if conversation.Role == openai.ChatMessageRoleSystem {
nick = "ChatGPT"
nick = "GPT"
lines = "\n\n***\n\n"
}
result += fmt.Sprintf("%s: %s\n\n", nick, conversation.Text)

result += fmt.Sprintf("**%s**: %s %s", nick, conversation.Text, lines)
}

return result
Expand Down Expand Up @@ -145,7 +152,7 @@ func (c *ChatgptPlugin) queryConversation(ctx context.Context, query plugin.Quer
chatHistory += fmt.Sprintf("You: %s\n", query.Search)
}
c.activeChatResult.Preview = plugin.WoxPreview{
PreviewType: plugin.WoxPreviewTypeText,
PreviewType: plugin.WoxPreviewTypeMarkdown,
PreviewData: chatHistory,
}
c.api.Log(ctx, fmt.Sprintf("active chat refresh interval: %d", c.activeChatResult.RefreshInterval))
Expand Down Expand Up @@ -175,12 +182,17 @@ func (c *ChatgptPlugin) queryConversation(ctx context.Context, query plugin.Quer
}

onAnswering := func(current plugin.RefreshableResult, deltaAnswer string) plugin.RefreshableResult {
if deltaAnswer == "" {
return current
}

if c.activeChatAnswer == "" {
//first response
deltaAnswer = fmt.Sprintf("ChatGPT: %s", deltaAnswer)
current.Preview.PreviewData += fmt.Sprintf("GPT: %s", deltaAnswer)
} else {
current.Preview.PreviewData += deltaAnswer
}

current.Preview.PreviewData += deltaAnswer
c.activeChatAnswer += deltaAnswer
return current
}
Expand Down Expand Up @@ -239,7 +251,7 @@ func (c *ChatgptPlugin) queryConversation(ctx context.Context, query plugin.Quer
results = append(results, plugin.QueryResult{
Title: "Start a new chat",
Preview: plugin.WoxPreview{
PreviewType: plugin.WoxPreviewTypeText,
PreviewType: plugin.WoxPreviewTypeMarkdown,
PreviewData: newChatPreviewData,
},
Icon: chatgptIcon,
Expand Down Expand Up @@ -369,7 +381,7 @@ func (c *ChatgptPlugin) queryCommand(ctx context.Context, query plugin.Query) []

return []plugin.QueryResult{{
Title: fmt.Sprintf("Chat with %s", query.Command),
Preview: plugin.WoxPreview{PreviewType: plugin.WoxPreviewTypeText, PreviewData: ""},
Preview: plugin.WoxPreview{PreviewType: plugin.WoxPreviewTypeMarkdown, PreviewData: ""},
Icon: chatgptIcon,
RefreshInterval: 100,
OnRefresh: c.generateGptResultRefresh(ctx, chatMessages, onAnswering, onAnswerErr, onAnswerFinished),
Expand Down
235 changes: 235 additions & 0 deletions Wox/setting/backup_restore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package setting

import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
cp "github.com/otiai10/copy"
"os"
"path"
"slices"
"strings"
"time"
"wox/util"
)

type BackupType string

const (
BackupTypeAuto BackupType = "auto"
BackupTypeManual BackupType = "manual"
BackupTypeUpdate BackupType = "update" // backup before update Wox
)

type Backup struct {
Id string
Name string // backup folder name
Timestamp int64
Type BackupType
}

func (m *Manager) StartAutoBackup(ctx context.Context) {
util.Go(ctx, "backup", func() {
for range time.NewTimer(24 * time.Hour).C {
backupErr := m.Backup(ctx, BackupTypeAuto)
if backupErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to backup data: %s", backupErr.Error()))
}
}
})
}

func (m *Manager) Backup(ctx context.Context, backupType BackupType) error {
logger.Info(ctx, fmt.Sprintf("backing up data: %s", backupType))

ts := util.GetSystemTimestamp()
backupName := fmt.Sprintf("%d", ts)
backupPath := path.Join(util.GetLocation().GetBackupDirectory(), backupName)
logger.Info(ctx, fmt.Sprintf("backup path: %s", backupPath))

err := cp.Copy(util.GetLocation().GetUserDataDirectory(), backupPath)
if err != nil {
logger.Error(ctx, fmt.Sprintf("failed to backup data: %s", err.Error()))
return err
}

backup := Backup{
Id: uuid.New().String(),
Name: backupName,
Timestamp: ts,
Type: backupType,
}
marshal, marshalErr := json.Marshal(backup)
if marshalErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to marshal backup data: %s", marshalErr.Error()))
// remove backup data
rmErr := os.RemoveAll(backupPath)
if rmErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to remove backup data: %s", rmErr.Error()))
}
return marshalErr
}

backupInfoPath := path.Join(backupPath, "backup.json")
writeErr := os.WriteFile(backupInfoPath, marshal, 0644)
if writeErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to write backup info: %s", writeErr.Error()))
// remove backup data
rmErr := os.RemoveAll(backupPath)
if rmErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to remove backup data: %s", rmErr.Error()))
}
return writeErr
}

logger.Info(ctx, "backup data saved successfully")

util.Go(ctx, "clean backups", func() {
m.cleanBackups(ctx)
})

return nil
}

func (m *Manager) Restore(ctx context.Context, backupId string) error {
logger.Info(ctx, fmt.Sprintf("restoring backup data: %s", backupId))
backups, getErr := m.FindAllBackups(ctx)
if getErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to get all backups: %s", getErr.Error()))
return getErr
}

var backupName string
for _, backup := range backups {
if backup.Id == backupId {
backupName = backup.Name
break
}
}
if backupName == "" {
logger.Error(ctx, fmt.Sprintf("backup not found: %s", backupId))
return fmt.Errorf("backup not found: %s", backupId)
}

// backup current data to temp directory
tempBackupName := fmt.Sprintf("temp_%d", util.GetSystemTimestamp())
tempBackupPath := path.Join(util.GetLocation().GetWoxDataDirectory(), tempBackupName)
cpErr := cp.Copy(util.GetLocation().GetUserDataDirectory(), tempBackupPath)
if cpErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to backup current data to temp directory: %s", cpErr.Error()))
return cpErr
}

// first remove current data
rmErr := os.Remove(util.GetLocation().GetUserDataDirectory())
if rmErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to remove user data directory: %s", rmErr.Error()))
return rmErr
}

backupPath := path.Join(util.GetLocation().GetBackupDirectory(), backupName)
cpErr = cp.Copy(backupPath, util.GetLocation().GetUserDataDirectory())
if cpErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to restore backup data to user data directory: %s", cpErr.Error()))
return cpErr
}

// remove backup info if exists
backupInfoPath := path.Join(backupPath, "backup.json")
if _, statErr := os.Stat(backupInfoPath); !os.IsNotExist(statErr) {
rmErr = os.Remove(backupInfoPath)
if rmErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to remove backup info: %s", rmErr.Error()))
return rmErr
}
}

// remove temp backup data
rmErr = os.RemoveAll(tempBackupPath)
if rmErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to remove temp backup data: %s", rmErr.Error()))
return rmErr
}

logger.Info(ctx, "backup data restored successfully")

//TODO: reload data / plugins

return nil
}

func (m *Manager) FindAllBackups(ctx context.Context) ([]Backup, error) {
var backupList []Backup

backupDir := util.GetLocation().GetBackupDirectory()
backupDirEntries, readDirErr := os.ReadDir(backupDir)
if readDirErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to read backup directory: %s", readDirErr.Error()))
return nil, readDirErr
}

for _, entry := range backupDirEntries {
if strings.HasPrefix(entry.Name(), "temp_") {
continue
}

// read backup info file
backupInfoPath := path.Join(backupDir, entry.Name(), "backup.json")
file, readErr := os.ReadFile(backupInfoPath)
if readErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to read backup info file: %s", readErr.Error()))
continue
}

var backupInfo Backup
decodeErr := json.Unmarshal(file, &backupInfo)
if decodeErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to unmarshal backup info: %s", decodeErr.Error()))
continue
}

backupList = append(backupList, backupInfo)
}

return backupList, nil
}

func (m *Manager) cleanBackups(ctx context.Context) error {
logger.Info(ctx, "cleaning backups")
maxBackups := 5

backups, getErr := m.FindAllBackups(ctx)
if getErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to get all backups: %s", getErr.Error()))
return getErr
}

// keep 5 backups
if len(backups) <= maxBackups {
return nil
}

// sort backups by timestamp
slices.SortFunc(backups, func(i, j Backup) int {
return int(i.Timestamp - j.Timestamp)
})

// remove old backups
removedCount := 0
for i := 0; i < len(backups)-maxBackups; i++ {
backup := backups[i]
backupPath := path.Join(util.GetLocation().GetBackupDirectory(), backup.Name)
rmErr := os.RemoveAll(backupPath)
if rmErr != nil {
logger.Error(ctx, fmt.Sprintf("failed to remove backup: %s", rmErr.Error()))
continue
} else {
removedCount++
logger.Info(ctx, fmt.Sprintf("backup removed: %s, date: %s", backup.Id, util.FormatTimestamp(backup.Timestamp)))
}
}

logger.Info(ctx, fmt.Sprintf("backups cleaned successfully, removed count: %d", removedCount))
return nil
}
2 changes: 2 additions & 0 deletions Wox/setting/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func (m *Manager) Init(ctx context.Context) error {
m.woxAppData = &defaultWoxAppData
}

m.StartAutoBackup(ctx)

return nil
}

Expand Down
Loading

0 comments on commit fa307d4

Please sign in to comment.