512 lines
13 KiB
Go
512 lines
13 KiB
Go
//
|
|
// Copyright ⓒ 2023 Chakib Ben Ziane <contact@blob42.xyz> and [`GoSuki` contributors]
|
|
// (https://github.com/blob42/gosuki/graphs/contributors).
|
|
//
|
|
// All rights reserved.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
//
|
|
// This file is part of GoSuki.
|
|
//
|
|
// GoSuki is free software: you can redistribute it and/or modify it under the terms of
|
|
// the GNU Affero General Public License as published by the Free Software Foundation,
|
|
// either version 3 of the License, or (at your option) any later version.
|
|
//
|
|
// GoSuki is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
// PURPOSE. See the GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License along with
|
|
// gosuki. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
// Chrome browser module.
|
|
//
|
|
// Chrome bookmarks are stored in a json file normally called Bookmarks.
|
|
// The bookmarks file is updated atomically by chrome for each change to the
|
|
// bookmark entries by the user.
|
|
//
|
|
// Changes are detected by watching the parent directory for fsnotify.Create
|
|
// events on the bookmark file. On linux this is done by using fsnotify.
|
|
package chrome
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/OneOfOne/xxhash"
|
|
"github.com/fsnotify/fsnotify"
|
|
|
|
"github.com/buger/jsonparser"
|
|
|
|
"git.blob42.xyz/gosuki/gosuki/hooks"
|
|
"git.blob42.xyz/gosuki/gosuki/internal/database"
|
|
"git.blob42.xyz/gosuki/gosuki/internal/logging"
|
|
"git.blob42.xyz/gosuki/gosuki/pkg/modules"
|
|
"git.blob42.xyz/gosuki/gosuki/pkg/profiles"
|
|
"git.blob42.xyz/gosuki/gosuki/pkg/tree"
|
|
"git.blob42.xyz/gosuki/gosuki/pkg/watch"
|
|
)
|
|
|
|
var (
|
|
log = logging.GetLogger("Chrome")
|
|
)
|
|
|
|
// Type of the func used recursively on each json node entry by [jsonparser.ArrayEach]
|
|
type ParseChildJSONFunc func([]byte, jsonparser.ValueType, int, error)
|
|
type RecursiveParseJSONFunc func([]byte, []byte, jsonparser.ValueType, int) error
|
|
|
|
var jsonNodeTypes = map[string]tree.NodeType{
|
|
"folder": tree.FolderNode,
|
|
"url": tree.URLNode,
|
|
}
|
|
|
|
var jsonNodePaths = struct {
|
|
Type, Children, URL string
|
|
}{"type", "children", "url"}
|
|
|
|
// type used to store json nodes in memory for parsing.
|
|
type RawNode struct {
|
|
name []byte
|
|
nType []byte
|
|
url []byte
|
|
children []byte
|
|
childrenType jsonparser.ValueType
|
|
}
|
|
|
|
func (rawNode *RawNode) parseItems(nodeData []byte) {
|
|
|
|
// Paths to lookup in node payload
|
|
paths := [][]string{
|
|
{"type"},
|
|
{"name"}, // Title of page
|
|
{"url"},
|
|
{"children"},
|
|
}
|
|
|
|
jsonparser.EachKey(nodeData, func(idx int, value []byte, vt jsonparser.ValueType, err error) {
|
|
if err != nil {
|
|
log.Critical("error parsing node items")
|
|
}
|
|
|
|
switch idx {
|
|
case 0:
|
|
rawNode.nType = value
|
|
|
|
case 1: // name or title
|
|
//currentNode.Name = s(value)
|
|
rawNode.name = value
|
|
case 2:
|
|
//currentNode.URL = s(value)
|
|
rawNode.url = value
|
|
case 3:
|
|
rawNode.children, rawNode.childrenType = value, vt
|
|
}
|
|
}, paths...)
|
|
}
|
|
|
|
// Returns *tree.Node from *RawNode
|
|
func (rawNode *RawNode) getNode() *tree.Node {
|
|
node := new(tree.Node)
|
|
nType, ok := jsonNodeTypes[string(rawNode.nType)]
|
|
if !ok {
|
|
log.Criticalf("unknown node type: %s", rawNode.nType)
|
|
}
|
|
node.Type = nType
|
|
|
|
node.Name = string(rawNode.name)
|
|
|
|
return node
|
|
}
|
|
|
|
// Chrome browser module
|
|
type Chrome struct {
|
|
// holds browsers.BrowserConfig
|
|
*ChromeConfig
|
|
}
|
|
|
|
func (ch *Chrome) Init(ctx *modules.Context, p *profiles.Profile) error {
|
|
// if called without profile setup default profile
|
|
if p == nil {
|
|
prof, err := ProfileManager.GetProfileByID(BrowserName, ch.Profile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bookmarkDir, err := prof.AbsolutePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ch.BkDir = bookmarkDir
|
|
return ch.init(ctx)
|
|
}
|
|
|
|
ch.ChromeConfig = NewChromeConfig()
|
|
ch.Profile = p.Id
|
|
|
|
if bookmarkDir, err := p.AbsolutePath(); err != nil {
|
|
return err
|
|
} else {
|
|
ch.BkDir = bookmarkDir
|
|
}
|
|
|
|
return ch.init(ctx)
|
|
}
|
|
|
|
// Init() is the first method called after a browser instance is created
|
|
// and registered.
|
|
// Return ok, error
|
|
func (ch *Chrome) init(_ *modules.Context) error {
|
|
log.Infof("initializing <%s>", ch.Name)
|
|
return ch.setupWatchers()
|
|
}
|
|
|
|
func (ch *Chrome) setupWatchers() error {
|
|
bookmarkDir := ch.BkDir
|
|
log.Debugf("Watching path: %s", bookmarkDir)
|
|
bookmarkPath := filepath.Join(bookmarkDir, ch.BkFile)
|
|
// Setup watcher
|
|
w := &watch.Watch{
|
|
Path: bookmarkDir,
|
|
EventTypes: []fsnotify.Op{fsnotify.Create},
|
|
EventNames: []string{bookmarkPath},
|
|
|
|
|
|
// NOTE: it used to be that chrome watcher would go stale after the
|
|
// first event, this is because the bookmark file is created after the
|
|
// browser is started, so we need to reset the watcher after the first
|
|
// event. This is not needed anymore as we are using the watcher to
|
|
// watch the bookmark dir and not the file itself.
|
|
ResetWatch: false,
|
|
}
|
|
|
|
ok, err := modules.SetupWatchers(ch.BrowserConfig, w)
|
|
if err != nil {
|
|
return fmt.Errorf("could not setup watcher: %s", err)
|
|
}
|
|
if !ok {
|
|
return errors.New("could not setup watcher")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ch *Chrome) ResetWatcher() error {
|
|
w := ch.GetWatcher()
|
|
if err := w.W.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := ch.setupWatchers(); err != nil {
|
|
return err
|
|
}
|
|
|
|
go watch.WatchLoop(ch)
|
|
return nil
|
|
}
|
|
|
|
// Returns a pointer to an initialized browser config
|
|
func (ch Chrome) Config() *modules.BrowserConfig {
|
|
return ch.BrowserConfig
|
|
}
|
|
|
|
func (ch Chrome) ModInfo() modules.ModInfo {
|
|
return modules.ModInfo{
|
|
ID: modules.ModID(ch.Name),
|
|
New: func() modules.Module {
|
|
return NewChrome()
|
|
},
|
|
}
|
|
}
|
|
|
|
func (ch *Chrome) Watch() *watch.WatchDescriptor {
|
|
// calls modules.BrowserConfig.GetWatcher()
|
|
return ch.GetWatcher()
|
|
}
|
|
|
|
func (ch *Chrome) Run() {
|
|
startRun := time.Now()
|
|
|
|
// Rebuild node tree
|
|
ch.NodeTree = &tree.Node{
|
|
Name: RootNodeName,
|
|
Parent: nil,
|
|
Type: tree.RootNode,
|
|
}
|
|
|
|
// Load bookmark file
|
|
//WIP: use builting path helpers
|
|
bookmarkPath, err := ch.BookmarkPath()
|
|
if err != nil {
|
|
log.Critical(err)
|
|
return
|
|
}
|
|
|
|
f, err := os.ReadFile(bookmarkPath)
|
|
if err != nil {
|
|
log.Critical(err)
|
|
return
|
|
}
|
|
|
|
var parseChildren ParseChildJSONFunc
|
|
var jsonParseRecursive RecursiveParseJSONFunc
|
|
|
|
parseChildren = func(childVal []byte,
|
|
dataType jsonparser.ValueType,
|
|
offset int,
|
|
err error) {
|
|
if err != nil {
|
|
log.Panic(err)
|
|
}
|
|
|
|
err = jsonParseRecursive(nil, childVal, dataType, offset)
|
|
if err != nil {
|
|
log.Critical(err)
|
|
}
|
|
|
|
}
|
|
|
|
// Needed to store the parent of each child node
|
|
var parentNodes []*tree.Node
|
|
|
|
jsonParseRoots := func(key []byte,
|
|
node []byte,
|
|
dataType jsonparser.ValueType,
|
|
offset int,
|
|
) error {
|
|
|
|
// If node type is string ignore (needed for sync_transaction_version)
|
|
if dataType == jsonparser.String {
|
|
return nil
|
|
}
|
|
|
|
ch.CurrentNodeCount++
|
|
rawNode := new(RawNode)
|
|
rawNode.parseItems(node)
|
|
|
|
//log.Debugf("Parsing root folder %s", rawNode.name)
|
|
|
|
currentNode := rawNode.getNode()
|
|
|
|
// Process this node as parent node later
|
|
parentNodes = append(parentNodes, currentNode)
|
|
|
|
// add the root node as parent to this node
|
|
currentNode.Parent = ch.NodeTree
|
|
|
|
// Add this root node as a child of the root node
|
|
ch.NodeTree.Children = append(ch.NodeTree.Children, currentNode)
|
|
|
|
// Call recursive parsing of this node which must
|
|
// a root folder node
|
|
jsonparser.ArrayEach(node, parseChildren, jsonNodePaths.Children)
|
|
|
|
// Finished parsing this root, it is not anymore a parent
|
|
_, parentNodes = parentNodes[len(parentNodes)-1],
|
|
parentNodes[:len(parentNodes)-1]
|
|
|
|
//log.Debugf("Parsed root %s folder", rawNode.name)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Main recursive parsing underneath each root folder
|
|
jsonParseRecursive = func(key []byte,
|
|
node []byte,
|
|
dataType jsonparser.ValueType,
|
|
offset int,
|
|
) error {
|
|
|
|
// If node type is string ignore (needed for sync_transaction_version)
|
|
if dataType == jsonparser.String {
|
|
return nil
|
|
}
|
|
|
|
ch.CurrentNodeCount++
|
|
|
|
rawNode := new(RawNode)
|
|
rawNode.parseItems(node)
|
|
|
|
currentNode := rawNode.getNode()
|
|
//log.Debugf("parsing node %s", currentNode.Name)
|
|
|
|
// if parents array is not empty
|
|
if len(parentNodes) != 0 {
|
|
parent := parentNodes[len(parentNodes)-1]
|
|
//log.Debugf("Adding current node to parent %s", parent.Name)
|
|
|
|
// Add current node to closest parent
|
|
currentNode.Parent = parent
|
|
|
|
// Add current node as child to parent
|
|
currentNode.Parent.Children = append(currentNode.Parent.Children, currentNode)
|
|
}
|
|
|
|
// if node is a folder with children
|
|
if rawNode.childrenType == jsonparser.Array && len(rawNode.children) > 2 { // if len(children) > len("[]")
|
|
|
|
//log.Debugf("Started folder %s", rawNode.name)
|
|
parentNodes = append(parentNodes, currentNode)
|
|
|
|
// Process recursively all child nodes of this folder node
|
|
jsonparser.ArrayEach(node, parseChildren, jsonNodePaths.Children)
|
|
|
|
//log.Debugf("Finished folder %s", rawNode.name)
|
|
_, parentNodes = parentNodes[len(parentNodes)-1], parentNodes[:len(parentNodes)-1]
|
|
|
|
}
|
|
|
|
// if node is url(leaf), handle the url
|
|
if currentNode.Type == tree.URLNode {
|
|
|
|
currentNode.URL = string(rawNode.url)
|
|
ch.CurrentURLCount++
|
|
|
|
// Check if url-node already in index
|
|
var nodeVal *tree.Node
|
|
iVal, found := ch.URLIndex.Get(currentNode.URL)
|
|
|
|
nameHash := xxhash.ChecksumString64(currentNode.Name)
|
|
|
|
// If node url not in index, add it to index
|
|
if !found {
|
|
//log.Debugf("Not found")
|
|
|
|
// store hash(name)
|
|
currentNode.NameHash = nameHash
|
|
|
|
// The value in the index will be a
|
|
// pointer to currentNode
|
|
log.Debugf("Inserting url %s to index", currentNode.URL)
|
|
ch.URLIndex.Insert(currentNode.URL, currentNode)
|
|
|
|
// Run registered bookmark parsing hooks
|
|
err = ch.CallHooks(currentNode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If we find the node already in index
|
|
// we check if the hash(name) changed meaning
|
|
// the data changed
|
|
} else {
|
|
// log.Debugf("URL Found in index")
|
|
nodeVal = iVal.(*tree.Node)
|
|
|
|
// hash(name) is different meaning new commands/tags could
|
|
// be added, we need to process the parsing hoos
|
|
if nodeVal.NameHash != nameHash {
|
|
log.Debugf("URL name changed !")
|
|
|
|
// Run parse hooks on node
|
|
ch.CallHooks(currentNode)
|
|
}
|
|
|
|
// Else we do nothing, the node will not
|
|
// change
|
|
}
|
|
|
|
//If parent is folder, add it as tag and add current node as child
|
|
//And add this link as child
|
|
if currentNode.Parent.Type == tree.FolderNode {
|
|
log.Debug("Parent is folder, parsing as tag ...")
|
|
currentNode.Tags = append(currentNode.Tags, currentNode.Parent.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// starts from the "roots" key of chrome json bookmark file
|
|
rootsData, _, _, _ := jsonparser.Get(f, "roots")
|
|
|
|
// Start a new node tree building job
|
|
jsonparser.ObjectEach(rootsData, jsonParseRoots)
|
|
ch.LastFullTreeParseTime = time.Since(startRun)
|
|
log.Debugf("<%s> parsed tree in %s", ch.Name, ch.LastFullTreeParseTime)
|
|
// Finished node tree building job
|
|
|
|
// Debug walk tree
|
|
//go PrintTree(ch.NodeTree)
|
|
|
|
// Reset the index to represent the nodetree
|
|
ch.RebuildIndex()
|
|
|
|
// Finished parsing
|
|
log.Debugf("<%s> parsed %d bookmarks and %d nodes", ch.Name, ch.CurrentURLCount, ch.CurrentNodeCount)
|
|
|
|
//Add nodeTree to Cache
|
|
//log.Debugf("<%s> buffer content", ch.Name)
|
|
//ch.BufferDB.Print()
|
|
|
|
log.Debugf("<%s> syncing to buffer", ch.Name)
|
|
database.SyncTreeToBuffer(ch.NodeTree, ch.BufferDB)
|
|
log.Debugf("<%s> tree synced to buffer", ch.Name)
|
|
|
|
//ch.BufferDB.Print()
|
|
|
|
// database.Cache represents bookmarks across all browsers
|
|
// From browsers it should support: add/update
|
|
// Delete method should only be possible through admin interface
|
|
// We could have an @ignore command to ignore a bookmark
|
|
|
|
// URLIndex is a hashmap index of all URLS representing current state
|
|
// of the browser
|
|
|
|
// nodeTree is current state of the browser as tree
|
|
|
|
// Buffer is the current state of the browser represetned by
|
|
// URLIndex and nodeTree
|
|
|
|
// If the cache is empty just copy buffer to cache
|
|
// until local db is already populated and preloaded
|
|
// debugPrint("%d", BufferDB.Count())
|
|
log.Debugf("checking if db is empty")
|
|
if empty, err := database.Cache.DB.IsEmpty(); empty {
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
log.Info("cache empty: loading buffer to CacheDB")
|
|
|
|
ch.BufferDB.CopyTo(database.Cache.DB)
|
|
|
|
log.Debugf("syncing <%s> to disk", database.Cache.DB.Name)
|
|
} else {
|
|
log.Debug("syncing to db")
|
|
ch.BufferDB.SyncTo(database.Cache.DB)
|
|
}
|
|
|
|
database.ScheduleSyncToDisk()
|
|
ch.LastWatchRunTime = time.Since(startRun)
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load() will be called right after a browser is initialized
|
|
func (ch *Chrome) Load() error {
|
|
go ch.Run()
|
|
return nil
|
|
}
|
|
|
|
func NewChrome() *Chrome {
|
|
return &Chrome{
|
|
ChromeConfig: ChromeCfg,
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
modules.RegisterBrowser(Chrome{ChromeConfig: ChromeCfg})
|
|
}
|
|
|
|
// interface guards
|
|
|
|
var _ modules.BrowserModule = (*Chrome)(nil)
|
|
var _ modules.ProfileInitializer = (*Chrome)(nil)
|
|
var _ modules.Loader = (*Chrome)(nil)
|
|
var _ watch.WatchRunner = (*Chrome)(nil)
|
|
var _ hooks.HookRunner = (*Chrome)(nil)
|
|
var _ watch.Stats = (*Chrome)(nil)
|
|
var _ profiles.ProfileManager = (*Chrome)(nil)
|