` + "\n" 862 | footer := "
\n" 863 | 864 | // Writing the header meta informations. 865 | w.Header().Set("Content-Disposition", "attachment; filename=gobkm.html") 866 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 867 | // Writing the HTML header 868 | if _, err := w.Write([]byte(header)); err != nil { 869 | // Just logging the error. 870 | log.WithFields(log.Fields{ 871 | "err": err, 872 | }).Error("ExportHandler") 873 | } 874 | // Exporting the bookmarks. 875 | env.exportTree(w, &exportBookmarksStruct{Fld: rootFolder}, 0) 876 | // Writing the HTML footer. 877 | if _, err := w.Write([]byte(footer)); err != nil { 878 | // Just logging the error. 879 | log.WithFields(log.Fields{ 880 | "err": err, 881 | }).Error("ExportHandler") 882 | } 883 | 884 | } 885 | 886 | // exportTree recursively exports in HTML the given bookmark struct. 887 | func (env *Env) exportTree(wr io.Writer, eb *exportBookmarksStruct, depth int) *exportBookmarksStruct { 888 | 889 | // Depth is just for cosmetics indent purposes. 890 | depth++ 891 | log.WithFields(log.Fields{ 892 | "*eb": *eb, 893 | }).Debug("ExportTree") 894 | 895 | // Writing the folder title. 896 | insertIndent(wr, depth) 897 | _, _ = wr.Write([]byte("
\n")) 900 | 901 | // For each children folder recursively building the bookmars tree. 902 | for _, child := range env.DB.GetFolderSubfolders(eb.Fld.Id) { 903 | eb.Sub = append(eb.Sub, env.exportTree(wr, &exportBookmarksStruct{Fld: child}, depth)) 904 | } 905 | 906 | // Getting the folder bookmarks. 907 | eb.Bkms = env.DB.GetFolderBookmarks(eb.Fld.Id) 908 | // Writing them. 909 | for _, bkm := range eb.Bkms { 910 | insertIndent(wr, depth) 911 | _, _ = wr.Write([]byte("
\n")) 915 | 916 | return eb 917 | 918 | } 919 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "flag" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/tbellembois/gobkm/handlers" 12 | "github.com/tbellembois/gobkm/models" 13 | 14 | "github.com/justinas/alice" 15 | "github.com/rs/cors" 16 | ) 17 | 18 | var ( 19 | datastore *models.SQLiteDataStore 20 | err error 21 | logf *os.File 22 | 23 | //go:embed static/wasm/* 24 | embedWasmBox embed.FS 25 | 26 | //go:embed static/index.html 27 | embedIndex string 28 | ) 29 | 30 | func main() { 31 | 32 | // Getting the program parameters. 33 | listenPort := flag.String("port", "8081", "the port to listen") 34 | proxyURL := flag.String("proxy", "http://localhost:"+*listenPort, "the proxy full URL if used") 35 | historySize := flag.Int("history", 3, "the folder history size") 36 | username := flag.String("username", "", "the default login username") 37 | dbPath := flag.String("db", "bkm.db", "the full sqlite db path") 38 | logfile := flag.String("logfile", "", "log to the given file") 39 | debug := flag.Bool("debug", false, "debug (verbose log), default is error") 40 | flag.Parse() 41 | 42 | // Logging to file if logfile parameter specified. 43 | if *logfile != "" { 44 | if logf, err = os.OpenFile(*logfile, os.O_WRONLY|os.O_CREATE, 0755); err != nil { 45 | log.Panic(err) 46 | } else { 47 | log.SetOutput(logf) 48 | } 49 | } 50 | // Setting the log level. 51 | if *debug { 52 | log.SetLevel(log.DebugLevel) 53 | } else { 54 | log.SetLevel(log.ErrorLevel) 55 | } 56 | log.WithFields(log.Fields{ 57 | "listenPort": *listenPort, 58 | "proxyURL": *proxyURL, 59 | "historySize": *historySize, 60 | "username": *username, 61 | "logfile": *logfile, 62 | "debug": *debug, 63 | }).Debug("main:flags") 64 | 65 | // Database initialization. 66 | if datastore, err = models.NewDBstore(*dbPath); err != nil { 67 | log.Panic(err) 68 | } 69 | // Database creation. 70 | datastore.CreateDatabase() 71 | datastore.PopulateDatabase() 72 | // Error check. 73 | if datastore.FlushErrors() != nil { 74 | log.Panic(err) 75 | } 76 | 77 | // Host from URL. 78 | u, err := url.Parse(*proxyURL) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | log.Debug(u) 83 | 84 | // Environment creation. 85 | env := handlers.Env{ 86 | DB: datastore, 87 | GoBkmProxyURL: *proxyURL, 88 | GoBkmProxyHost: u.Host, 89 | GoBkmHistorySize: *historySize, 90 | GoBkmUsername: *username, 91 | } 92 | 93 | env.TplMainData = embedIndex 94 | 95 | // CORS handler. 96 | c := cors.New(cors.Options{ 97 | Debug: true, 98 | AllowedOrigins: []string{"http://localhost:8081", *proxyURL}, 99 | AllowCredentials: true, 100 | AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodOptions}, 101 | AllowedHeaders: []string{"Authorization", "DNT", "User-Agent", "X-Requested-With", "If-Modified-Since", "Cache-Control", "Content-Type", "Range"}, 102 | }) 103 | 104 | mux := http.NewServeMux() 105 | 106 | // Handlers initialization. 107 | mux.Handle("/wasm/", http.StripPrefix("/wasm/", http.FileServer(http.FS(embedWasmBox)))) 108 | 109 | mux.HandleFunc("/addBookmark/", env.AddBookmarkHandler) 110 | mux.HandleFunc("/addFolder/", env.AddFolderHandler) 111 | mux.HandleFunc("/deleteBookmark/", env.DeleteBookmarkHandler) 112 | mux.HandleFunc("/deleteFolder/", env.DeleteFolderHandler) 113 | mux.HandleFunc("/getTags/", env.GetTagsHandler) 114 | mux.HandleFunc("/getStars/", env.GetStarsHandler) 115 | mux.HandleFunc("/getFolderChildren/", env.GetFolderChildrenHandler) 116 | mux.HandleFunc("/getTree/", env.GetTreeHandler) 117 | mux.HandleFunc("/import/", env.ImportHandler) 118 | mux.HandleFunc("/export/", env.ExportHandler) 119 | mux.HandleFunc("/updateFolder/", env.UpdateFolderHandler) 120 | mux.HandleFunc("/updateBookmark/", env.UpdateBookmarkHandler) 121 | mux.HandleFunc("/searchBookmarks/", env.SearchBookmarkHandler) 122 | mux.HandleFunc("/starBookmark/", env.StarBookmarkHandler) 123 | mux.HandleFunc("/", env.MainHandler) 124 | 125 | chain := alice.New(c.Handler).Then(mux) 126 | 127 | if err = http.ListenAndServe(":"+*listenPort, chain); err != nil { 128 | log.Fatal(err) 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /models/idatastore.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/tbellembois/gobkm/types" 5 | ) 6 | 7 | // Datastore is a folders and bookmarks storage interface. 8 | type Datastore interface { 9 | FlushErrors() error 10 | 11 | SearchBookmarks(string) []*types.Bookmark 12 | GetBookmark(int) *types.Bookmark 13 | GetBookmarkTags(int) []*types.Tag 14 | GetFolderBookmarks(int) types.Bookmarks 15 | SaveBookmark(*types.Bookmark) int64 16 | UpdateBookmark(*types.Bookmark) 17 | DeleteBookmark(*types.Bookmark) 18 | 19 | GetFolder(int) *types.Folder 20 | GetFolderSubfolders(int) []*types.Folder 21 | SaveFolder(*types.Folder) int64 22 | UpdateFolder(*types.Folder) 23 | DeleteFolder(*types.Folder) 24 | 25 | GetTags() []*types.Tag 26 | GetStars() []*types.Bookmark 27 | GetTag(int) *types.Tag 28 | SaveTag(*types.Tag) int64 29 | } 30 | -------------------------------------------------------------------------------- /models/sqlitedatastore.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql" 5 | 6 | _ "github.com/mattn/go-sqlite3" // register sqlite3 driver 7 | log "github.com/sirupsen/logrus" 8 | "github.com/tbellembois/gobkm/types" 9 | ) 10 | 11 | const ( 12 | dbdriver = "sqlite3" 13 | ) 14 | 15 | // SQLiteDataStore implements the Datastore interface 16 | // to store the folders and bookmarks in SQLite3. 17 | type SQLiteDataStore struct { 18 | *sql.DB 19 | err error 20 | } 21 | 22 | // NewDBstore returns a database connection to the given dataSourceName 23 | // ie. a path to the sqlite database file. 24 | func NewDBstore(dataSourceName string) (*SQLiteDataStore, error) { 25 | 26 | log.WithFields(log.Fields{ 27 | "dataSourceName": dataSourceName, 28 | }).Debug("NewDBstore:params") 29 | 30 | var ( 31 | db *sql.DB 32 | err error 33 | ) 34 | 35 | if db, err = sql.Open(dbdriver, dataSourceName); err != nil { 36 | log.WithFields(log.Fields{ 37 | "dataSourceName": dataSourceName, 38 | }).Error("NewDBstore:error opening the database") 39 | return nil, err 40 | } 41 | 42 | return &SQLiteDataStore{db, nil}, nil 43 | 44 | } 45 | 46 | // FlushErrors returns the last DB errors and flushes it. 47 | func (db *SQLiteDataStore) FlushErrors() error { 48 | 49 | // Saving the last thrown error. 50 | lastError := db.err 51 | // Resetting the error. 52 | db.err = nil 53 | // Returning the last error. 54 | return lastError 55 | 56 | } 57 | 58 | // CreateDatabase creates the database tables. 59 | func (db *SQLiteDataStore) CreateDatabase() { 60 | 61 | log.Info("Creating database") 62 | 63 | // Activate the foreign keys feature. 64 | if _, db.err = db.Exec("PRAGMA foreign_keys = ON"); db.err != nil { 65 | log.Error("CreateDatabase: error executing the PRAGMA request:" + db.err.Error()) 66 | panic(db.err) 67 | } 68 | 69 | // Tables creation if needed. 70 | if _, db.err = db.Exec(`CREATE TABLE IF NOT EXISTS folder ( id integer PRIMARY KEY, title string NOT NULL, parentFolderId integer, nbChildrenFolders integer, 71 | FOREIGN KEY (parentFolderId) references folder(id) 72 | ON DELETE CASCADE)`); db.err != nil { 73 | log.Error("CreateDatabase: error executing the CREATE TABLE request for table bookmark") 74 | panic(db.err) 75 | } 76 | if _, db.err = db.Exec("CREATE TABLE IF NOT EXISTS tag ( id integer PRIMARY KEY, name string NOT NULL)"); db.err != nil { 77 | log.Error("CreateDatabase: error executing the CREATE TABLE request for table bookmark") 78 | panic(db.err) 79 | } 80 | if _, db.err = db.Exec(`CREATE TABLE IF NOT EXISTS bookmarktag ( id integer PRIMARY KEY, 81 | bookmarkId integer, 82 | tagId integer, 83 | FOREIGN KEY (bookmarkId) references bookmark(id), 84 | FOREIGN KEY (tagId) references tag(id))`); db.err != nil { 85 | log.Error("CreateDatabase: error executing the CREATE TABLE request for table bookmarktag") 86 | panic(db.err) 87 | } 88 | if _, db.err = db.Exec(`CREATE TABLE IF NOT EXISTS bookmark ( id integer PRIMARY KEY, title string NOT NULL, url string NOT NULL, favicon string, starred integer, folderId integer, 89 | FOREIGN KEY (folderId) references folder(id) 90 | ON DELETE CASCADE)`); db.err != nil { 91 | log.Error("CreateDatabase: error executing the CREATE TABLE request for table bookmark") 92 | panic(db.err) 93 | } 94 | 95 | // Looking for folders. 96 | var count int 97 | if db.err = db.QueryRow("SELECT COUNT(*) as count FROM folder").Scan(&count); db.err != nil { 98 | log.Error("CreateDatabase: error executing the SELECT COUNT(*) request for table folder") 99 | panic(db.err) 100 | } 101 | // Inserting the / folder if not present. 102 | if count > 0 { 103 | log.Info("CreateDatabase: folder table not empty, leaving") 104 | return 105 | } 106 | if _, db.err = db.Exec("INSERT INTO folder(id, title) values(\"1\", \"/\")"); db.err != nil { 107 | log.Error("CreateDatabase: error inserting the root folder") 108 | panic(db.err) 109 | } 110 | 111 | } 112 | 113 | // PopulateDatabase populate the database with sample folders and bookmarks. 114 | func (db *SQLiteDataStore) PopulateDatabase() { 115 | 116 | log.Info("Populating database") 117 | 118 | // Leaving on past errors. 119 | if db.err != nil { 120 | panic(db.err) 121 | } 122 | 123 | var ( 124 | folders []*types.Folder 125 | bookmarks []*types.Bookmark 126 | count int 127 | ) 128 | 129 | // Leaving if database is already populated. 130 | if db.err = db.QueryRow("SELECT COUNT(*) as count FROM folder").Scan(&count); db.err != nil || count > 1 { 131 | log.Info("Database not empty, leaving") 132 | return 133 | } 134 | 135 | // Getting the root folder. 136 | folderRoot := db.GetFolder(1) 137 | // Creating new sample folders. 138 | folder1 := types.Folder{Id: 1, Title: "IT", Parent: folderRoot} 139 | folder2 := types.Folder{Id: 2, Title: "Development", Parent: &folder1} 140 | // Creating new sample tags. 141 | tag1 := []*types.Tag{{Id: 1, Name: "mytag1"}} 142 | tag2 := []*types.Tag{{Id: 1, Name: "mytag1"}, {Id: 2, Name: "mytag2"}} 143 | // Creating new sample bookmarks. 144 | bookmark1 := types.Bookmark{Id: 1, Title: "GoLang", Tags: tag1, Starred: true, URL: "https://golang.org/", Favicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAb9JREFUOI3tkj9oU1EUh797c3lgjA4xL61FX0yhMQqmW5QgFim4+GcyQ3Hp1MlBqFIyOGUobRScnYoQikNA0Ao6WJS2UIdiK7SUVGtfIZg0iMSA+Iy5Dg9fGnyLu2e6nHPu9zv3/K7QWuMXjfqebjQbOM5PIuEjHI6Ywq9P/TlUdm09+3KeNxtlAHbLWzTrNeTBQxjhHuLHohrgwqkBRi5dpO+4JQDEh80NfePOXaIDJ3FigximBUAyk+5SOvFphR/tNovvyzg769TKmxQLecS5a9d1dOQ2zp7N6bjF1PAZlJKMv1hFpVxIa+0t96+cBWD82TLr2zaGaVGbvYcEqLx+gmFajKZiqANBeo/2MZcb89RHUzEAeiNh5nJjGKZF9VUJAFks5FGVrc7IuuW7VH518slMGlHdpljII/sTSW+7j5ohEIrP9S9cnnxIaShOaSjOzNoOBNz81ceLHqg/kRRqv0ggGGLCdm3t+fqRmZtZ15HKEhN2Go1ABUO06VjfBdDSLQS0IFNd4fytSQAWHuR4B8gW7lWJP8B7rtA8zU7zfH4V8f0brew0ou37j/wBHigx2D2d/LvHJ/Vv8R8AvwHjjZMncK4ImgAAAABJRU5ErkJggg==", Folder: &folder2} 145 | bookmark2 := types.Bookmark{Id: 2, Title: "GoBkm Github", Tags: tag2, Starred: false, URL: "https://github.com/tbellembois/gobkm", Favicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAilJREFUOI2FUUFrE0EYfTuzNgaxtmlmd9ON3WiS0gQ3Fs+BPXmVCBb03EMPUhBELzmEQGjv3nsJSiRLLvkDnrxJo5XUnCRirWiaGmIhBLP5PNSpSbDxg4GZ4X3ve+99wESVs1lz1baLZihUNzTtxNC0EzMUqq/adrGczZqTeEVeKJdDynVL31ut+0QERVHGgPJPE+Ll3traAyWf/0tAuRwiOzt7vX7fxj+ax4gA+H2+98319ZSSz4MBQMp1S71+376gqh8cx3mkcP55MBiAM9bmjLU77TY451/j8fhjn6q+7fX7dsp1S2eedSFIF4Li0agrpxDRmHxZy7FYSeLL2azJtqrVbQmg4fCS9DVqY8KST5JuVavbihkK1QeDQRIAvrVa55uXarpdGNEoAYCqqvvM8zwLALqdznBU6nmlzM6CM/YTADzPsxgADgBX5ufZtPTPFBBhSHT5z5MxlfMmAPw4OgIR3fkfwe10Oi2Vcs4/sblAYBcA5hYW4OP8+TQbRIR3jUYFON2U8Pt32cNM5ikAxCxr87ppvtE1jcK6/rpWq92TjY7j3L22tPQqGAgQY0wDTje1ubHxBABgJ5NlPRgkIsKyZVXDi4v7dHw8NlnuXh47mSxLIhARLNNs/PI8cdU0M7FI5MuLSuXjaKiGppG0d3Fmpt48OLgxFvohEZKJRMXQNNKFoMksDCHIEIISKyvu4bR1P8vlwrfi8WJvAnTTtouFQsGaxP8GGFLoTDqN+p0AAAAASUVORK5CYII=", Folder: &folder2} 146 | 147 | folders = append(folders, &folder1, &folder2) 148 | bookmarks = append(bookmarks, &bookmark1, &bookmark2) 149 | 150 | // DB save. 151 | for _, fld := range folders { 152 | db.SaveFolder(fld) 153 | } 154 | for _, bkm := range bookmarks { 155 | db.SaveBookmark(bkm) 156 | } 157 | 158 | } 159 | 160 | // GetBookmark returns the full tags list 161 | func (db *SQLiteDataStore) GetTags() []*types.Tag { 162 | 163 | // Leaving silently on past errors... 164 | if db.err != nil { 165 | return nil 166 | } 167 | 168 | // Querying the tags. 169 | var ( 170 | rows *sql.Rows 171 | tags []*types.Tag 172 | ) 173 | rows, db.err = db.Query("SELECT * FROM tag") 174 | defer func() { 175 | if db.err = rows.Close(); db.err != nil { 176 | log.WithFields(log.Fields{ 177 | "err": db.err, 178 | }).Error("GetTags:error closing rows") 179 | } 180 | }() 181 | 182 | switch { 183 | case db.err == sql.ErrNoRows: 184 | log.Debug("GetTags:no bookmarks") 185 | db.err = nil 186 | return nil 187 | case db.err != nil: 188 | log.WithFields(log.Fields{ 189 | "err": db.err, 190 | }).Error("GetTags:SELECT query error") 191 | return nil 192 | default: 193 | for rows.Next() { 194 | // Building a new Tag instance with each row. 195 | tag := new(types.Tag) 196 | db.err = rows.Scan(&tag.Id, &tag.Name) 197 | if db.err != nil { 198 | log.WithFields(log.Fields{ 199 | "err": db.err, 200 | }).Error("GetTags:error scanning the query result row") 201 | return nil 202 | } 203 | tags = append(tags, tag) 204 | } 205 | if db.err = rows.Err(); db.err != nil { 206 | log.WithFields(log.Fields{ 207 | "err": db.err, 208 | }).Error("GetTags:error looping rows") 209 | return nil 210 | } 211 | } 212 | return tags 213 | 214 | } 215 | 216 | // GetTag returns a Tag instance with the given id. 217 | func (db *SQLiteDataStore) GetTag(id int) *types.Tag { 218 | 219 | log.WithFields(log.Fields{ 220 | "id": id, 221 | }).Debug("GetTag") 222 | 223 | // Leaving silently on past errors... 224 | if db.err != nil { 225 | return nil 226 | } 227 | 228 | // Querying the Tag. 229 | tag := new(types.Tag) 230 | db.err = db.QueryRow("SELECT * FROM tag WHERE id=?", id).Scan(&tag.Id, &tag.Name) 231 | switch { 232 | case db.err == sql.ErrNoRows: 233 | log.WithFields(log.Fields{ 234 | "id": id, 235 | }).Debug("GetTag:no tag with that ID") 236 | db.err = nil 237 | return nil 238 | case db.err != nil: 239 | log.WithFields(log.Fields{ 240 | "err": db.err, 241 | }).Error("GetTag:SELECT query error") 242 | return nil 243 | default: 244 | log.WithFields(log.Fields{ 245 | "Id": tag.Id, 246 | "Name": tag.Name, 247 | }).Debug("GetTag:tag found") 248 | } 249 | return tag 250 | 251 | } 252 | 253 | // GetBookmark returns a Bookmark instance with the given id. 254 | func (db *SQLiteDataStore) GetBookmark(id int) *types.Bookmark { 255 | 256 | log.WithFields(log.Fields{ 257 | "id": id, 258 | }).Debug("GetBookmark") 259 | 260 | // Leaving silently on past errors... 261 | if db.err != nil { 262 | return nil 263 | } 264 | 265 | var ( 266 | folderID sql.NullInt64 267 | starred sql.NullInt64 268 | ) 269 | 270 | // Querying the bookmark. 271 | bkm := new(types.Bookmark) 272 | db.err = db.QueryRow("SELECT id, title, url, favicon, starred, folderId FROM bookmark WHERE id=?", id).Scan(&bkm.Id, &bkm.Title, &bkm.URL, &bkm.Favicon, &starred, &folderID) 273 | switch { 274 | case db.err == sql.ErrNoRows: 275 | log.WithFields(log.Fields{ 276 | "id": id, 277 | }).Debug("GetBookmark:no bookmark with that ID") 278 | db.err = nil 279 | return nil 280 | case db.err != nil: 281 | log.WithFields(log.Fields{ 282 | "err": db.err, 283 | }).Error("GetBookmark:SELECT query error") 284 | return nil 285 | default: 286 | log.WithFields(log.Fields{ 287 | "Id": bkm.Id, 288 | "Title": bkm.Title, 289 | "folderId": folderID, 290 | "Favicon": bkm.Favicon, 291 | }).Debug("GetBookmark:bookmark found") 292 | // Starred bookmark ? 293 | if int(starred.Int64) != 0 { 294 | bkm.Starred = true 295 | } 296 | // Retrieving the parent folder if it is not the root (/). 297 | if folderID.Int64 != 0 { 298 | bkm.Folder = db.GetFolder(int(folderID.Int64)) 299 | if db.err != nil { 300 | log.WithFields(log.Fields{ 301 | "err": db.err, 302 | }).Error("GetBookmark:parent Folder retrieving error") 303 | return nil 304 | } 305 | } 306 | } 307 | return bkm 308 | 309 | } 310 | 311 | // GetFolder returns a Folder instance with the given id. 312 | func (db *SQLiteDataStore) GetFolder(id int) *types.Folder { 313 | 314 | log.WithFields(log.Fields{ 315 | "id": id, 316 | }).Debug("GetFolder") 317 | 318 | // Leaving silently on past errors... 319 | if db.err != nil || id == 0 { 320 | return nil 321 | } 322 | 323 | // Querying the folder. 324 | var parentFldID sql.NullInt64 325 | fld := new(types.Folder) 326 | db.err = db.QueryRow("SELECT id, title, parentFolderId FROM folder WHERE id=?", id).Scan(&fld.Id, &fld.Title, &parentFldID) 327 | switch { 328 | case db.err == sql.ErrNoRows: 329 | log.WithFields(log.Fields{ 330 | "id": id, 331 | }).Debug("GetFolder:no folder with that ID") 332 | db.err = nil 333 | return nil 334 | case db.err != nil: 335 | log.WithFields(log.Fields{ 336 | "err": db.err, 337 | }).Error("GetFolder:SELECT query error") 338 | return nil 339 | default: 340 | log.WithFields(log.Fields{ 341 | "Id": fld.Id, 342 | "Title": fld.Title, 343 | "parentFldId": parentFldID, 344 | }).Debug("GetFolder:folder found") 345 | // recursively retrieving the parents 346 | if parentFldID.Int64 != 0 { 347 | fld.Parent = db.GetFolder(int(parentFldID.Int64)) 348 | } 349 | } 350 | 351 | // Recursively getting the parents 352 | if parentFldID.Valid { 353 | fld.Parent = db.GetFolder(int(parentFldID.Int64)) 354 | } 355 | 356 | return fld 357 | 358 | } 359 | 360 | // GetStars returns the starred bookmarks. 361 | func (db *SQLiteDataStore) GetStars() []*types.Bookmark { 362 | 363 | // Leaving silently on past errors... 364 | if db.err != nil { 365 | return nil 366 | } 367 | 368 | // Querying the bookmarks. 369 | var ( 370 | rows *sql.Rows 371 | bkms []*types.Bookmark 372 | ) 373 | rows, db.err = db.Query("SELECT id, title, url, favicon, starred, folderId FROM bookmark WHERE starred ORDER BY title") 374 | defer func() { 375 | if db.err = rows.Close(); db.err != nil { 376 | log.WithFields(log.Fields{ 377 | "err": db.err, 378 | }).Error("GetStarredBookmarks:error closing rows") 379 | } 380 | }() 381 | 382 | switch { 383 | case db.err == sql.ErrNoRows: 384 | log.Debug("GetStarredBookmarks:no bookmarks") 385 | db.err = nil 386 | return nil 387 | case db.err != nil: 388 | log.WithFields(log.Fields{ 389 | "err": db.err, 390 | }).Error("GetStarredBookmarks:SELECT query error") 391 | return nil 392 | default: 393 | for rows.Next() { 394 | // Building a new Bookmark instance with each row. 395 | bkm := new(types.Bookmark) 396 | var fldID sql.NullInt64 397 | db.err = rows.Scan(&bkm.Id, &bkm.Title, &bkm.URL, &bkm.Favicon, &bkm.Starred, &fldID) 398 | if db.err != nil { 399 | log.WithFields(log.Fields{ 400 | "err": db.err, 401 | }).Error("GetStarredBookmarks:error scanning the query result row") 402 | return nil 403 | } 404 | // Retrieving the bookmark folder. 405 | bkm.Folder = db.GetFolder(int(fldID.Int64)) 406 | bkms = append(bkms, bkm) 407 | } 408 | if db.err = rows.Err(); db.err != nil { 409 | log.WithFields(log.Fields{ 410 | "err": db.err, 411 | }).Error("GetStarredBookmarks:error looping rows") 412 | return nil 413 | } 414 | return bkms 415 | } 416 | 417 | } 418 | 419 | // SearchBookmarks returns the bookmarks with the title containing the given string. 420 | func (db *SQLiteDataStore) SearchBookmarks(s string) []*types.Bookmark { 421 | 422 | log.WithFields(log.Fields{ 423 | "s": s, 424 | }).Debug("SearchBookmarks") 425 | 426 | // Leaving silently on past errors... 427 | if db.err != nil { 428 | return nil 429 | } 430 | var ( 431 | rows *sql.Rows 432 | bkms []*types.Bookmark 433 | ) 434 | 435 | // Querying the bookmarks. 436 | rows, db.err = db.Query(`SELECT bookmark.id, bookmark.title, bookmark.url, bookmark.favicon, bookmark.starred, bookmark.folderId 437 | FROM bookmark 438 | LEFT JOIN bookmarktag ON bookmarktag.bookmarkId = bookmark.Id 439 | LEFT JOIN tag ON bookmarktag.tagId = tag.Id 440 | WHERE bookmark.title LIKE ? OR 441 | tag.name LIKE ? 442 | GROUP BY bookmark.id 443 | ORDER BY bookmark.title`, "%"+s+"%", "%"+s+"%") 444 | defer func() { 445 | if db.err = rows.Close(); db.err != nil { 446 | log.WithFields(log.Fields{ 447 | "err": db.err, 448 | }).Error("SearchBookmarks:error closing rows") 449 | } 450 | }() 451 | switch { 452 | case db.err == sql.ErrNoRows: 453 | log.Debug("SearchBookmarks:no bookmarks") 454 | db.err = nil 455 | return nil 456 | case db.err != nil: 457 | log.WithFields(log.Fields{ 458 | "err": db.err, 459 | }).Error("SearchBookmarks:SELECT query error") 460 | return nil 461 | default: 462 | for rows.Next() { 463 | // Building a new Bookmark instance with each row. 464 | bkm := new(types.Bookmark) 465 | var parentFldID sql.NullInt64 466 | var starred sql.NullInt64 467 | db.err = rows.Scan(&bkm.Id, &bkm.Title, &bkm.URL, &bkm.Favicon, &starred, &parentFldID) 468 | 469 | // Getting the folder 470 | bkm.Folder = db.GetFolder(int(parentFldID.Int64)) 471 | 472 | // Starred bookmark ? 473 | if int(starred.Int64) != 0 { 474 | bkm.Starred = true 475 | } 476 | if db.err != nil { 477 | log.WithFields(log.Fields{ 478 | "err": db.err, 479 | }).Error("SearchBookmarks:error scanning the query result row") 480 | return nil 481 | } 482 | log.WithFields(log.Fields{ 483 | "bkm": bkm, 484 | }).Debug("SearchBookmarks:bookmark found") 485 | bkms = append(bkms, bkm) 486 | } 487 | if db.err = rows.Err(); db.err != nil { 488 | log.WithFields(log.Fields{ 489 | "err": db.err, 490 | }).Error("SearchBookmarks:error looping rows") 491 | return nil 492 | } 493 | return bkms 494 | } 495 | 496 | } 497 | 498 | // GetFolderBookmarks returns the bookmarks of the given folder id. 499 | func (db *SQLiteDataStore) GetFolderBookmarks(id int) types.Bookmarks { 500 | 501 | log.WithFields(log.Fields{ 502 | "id": id, 503 | }).Debug("GetFolderBookmarks") 504 | 505 | // Leaving silently on past errors... 506 | if db.err != nil { 507 | return nil 508 | } 509 | var ( 510 | rows *sql.Rows 511 | bkms types.Bookmarks 512 | ) 513 | 514 | // Querying the bookmarks. 515 | rows, db.err = db.Query("SELECT id, title, url, favicon, starred, folderId FROM bookmark WHERE folderId is ? ORDER BY title", id) 516 | defer func() { 517 | if db.err = rows.Close(); db.err != nil { 518 | log.WithFields(log.Fields{ 519 | "err": db.err, 520 | }).Error("GetFolderBookmarks:error closing rows") 521 | } 522 | }() 523 | 524 | switch { 525 | case db.err == sql.ErrNoRows: 526 | log.Debug("GetFolderBookmarks:no bookmarks") 527 | db.err = nil 528 | return nil 529 | case db.err != nil: 530 | log.WithFields(log.Fields{ 531 | "err": db.err, 532 | }).Error("GetFolderBookmarks:SELECT query error") 533 | return nil 534 | default: 535 | for rows.Next() { 536 | // Building a new Bookmark instance with each row. 537 | bkm := new(types.Bookmark) 538 | var parentFldID sql.NullInt64 539 | var starred sql.NullInt64 540 | db.err = rows.Scan(&bkm.Id, &bkm.Title, &bkm.URL, &bkm.Favicon, &starred, &parentFldID) 541 | // Starred bookmark ? 542 | if int(starred.Int64) != 0 { 543 | bkm.Starred = true 544 | } 545 | if db.err != nil { 546 | log.WithFields(log.Fields{ 547 | "err": db.err, 548 | }).Error("GetFolderBookmarks:error scanning the query result row") 549 | return nil 550 | } 551 | 552 | // Getting the bookmark tags 553 | bkm.Tags = db.GetBookmarkTags(bkm.Id) 554 | 555 | bkm.Folder = &types.Folder{Id: int(parentFldID.Int64)} 556 | bkms = append(bkms, bkm) 557 | log.WithFields(log.Fields{ 558 | "bkm": bkm, 559 | }).Debug("GetFolderBookmarks:bookmark found") 560 | } 561 | if db.err = rows.Err(); db.err != nil { 562 | log.WithFields(log.Fields{ 563 | "err": db.err, 564 | }).Error("GetFolderBookmarks:error looping rows") 565 | return nil 566 | } 567 | 568 | return bkms 569 | } 570 | 571 | } 572 | 573 | // GetBookmarkTags returns the tags of the bookmark 574 | func (db *SQLiteDataStore) GetBookmarkTags(id int) []*types.Tag { 575 | 576 | log.WithFields(log.Fields{ 577 | "id": id, 578 | }).Debug("GetBookmarkTags") 579 | 580 | // Leaving silently on past errors... 581 | if db.err != nil { 582 | return nil 583 | } 584 | 585 | var ( 586 | row *sql.Row 587 | rows *sql.Rows 588 | tagids []int 589 | tags []*types.Tag 590 | ) 591 | // Querying the tags ids. 592 | rows, db.err = db.Query("SELECT tagId FROM bookmarktag WHERE bookmarkId is ?", id) 593 | defer func() { 594 | if db.err = rows.Close(); db.err != nil { 595 | log.WithFields(log.Fields{ 596 | "err": db.err, 597 | }).Error("GetBookmarkTags:error closing rows") 598 | } 599 | }() 600 | switch { 601 | case db.err == sql.ErrNoRows: 602 | log.Debug("GetBookmarkTags:no tags") 603 | db.err = nil 604 | return nil 605 | case db.err != nil: 606 | log.WithFields(log.Fields{ 607 | "err": db.err, 608 | }).Error("GetBookmarkTags:SELECT query error") 609 | return nil 610 | default: 611 | for rows.Next() { 612 | var tagid int 613 | db.err = rows.Scan(&tagid) 614 | if db.err != nil { 615 | log.WithFields(log.Fields{ 616 | "err": db.err, 617 | }).Error("GetBookmarkTags:error scanning the query result row - tagid") 618 | return nil 619 | } 620 | tagids = append(tagids, tagid) 621 | } 622 | if db.err = rows.Err(); db.err != nil { 623 | log.WithFields(log.Fields{ 624 | "err": db.err, 625 | }).Error("GetBookmarkTags:error looping rows") 626 | return nil 627 | } 628 | } 629 | log.WithFields(log.Fields{"tagids": tagids}).Debug("GetBookmarkTags") 630 | 631 | // Querying the tags. 632 | for _, tid := range tagids { 633 | row = db.QueryRow("SELECT id, name FROM tag WHERE id is ?", tid) 634 | defer func() { 635 | if db.err = rows.Close(); db.err != nil { 636 | log.WithFields(log.Fields{ 637 | "err": db.err, 638 | }).Error("GetBookmarkTags:error closing rows") 639 | } 640 | }() 641 | var tag types.Tag 642 | db.err = row.Scan(&tag.Id, &tag.Name) 643 | if db.err != nil { 644 | log.WithFields(log.Fields{ 645 | "err": db.err, 646 | }).Error("GetBookmarkTags:error scanning the query result row - tag") 647 | return nil 648 | } 649 | log.WithFields(log.Fields{"tag": tag}).Debug("GetBookmarkTags") 650 | tags = append(tags, &tag) 651 | } 652 | 653 | return tags 654 | 655 | } 656 | 657 | // GetFolderSubfolders returns the children folders as an array of *Folder 658 | func (db *SQLiteDataStore) GetFolderSubfolders(id int) []*types.Folder { 659 | 660 | log.WithFields(log.Fields{ 661 | "id": id, 662 | }).Debug("GetChildrenFolders") 663 | 664 | // Leaving silently on past errors... 665 | if db.err != nil { 666 | return nil 667 | } 668 | 669 | var ( 670 | rows *sql.Rows 671 | flds []*types.Folder 672 | ) 673 | // Querying the folders. 674 | rows, db.err = db.Query("SELECT id, title, parentFolderId, nbChildrenFolders FROM folder WHERE parentFolderId is ? ORDER BY title", id) 675 | defer func() { 676 | if db.err = rows.Close(); db.err != nil { 677 | log.WithFields(log.Fields{ 678 | "err": db.err, 679 | }).Error("GetFolderSubfolders:error closing rows") 680 | } 681 | }() 682 | switch { 683 | case db.err == sql.ErrNoRows: 684 | log.Debug("GetChildrenFolders:no folders") 685 | db.err = nil 686 | return nil 687 | case db.err != nil: 688 | log.WithFields(log.Fields{ 689 | "err": db.err, 690 | }).Error("GetChildrenFolders:SELECT query error") 691 | return nil 692 | default: 693 | for rows.Next() { 694 | // Building a new Folder instance with each row. 695 | fld := new(types.Folder) 696 | var parentFldID sql.NullInt64 697 | db.err = rows.Scan(&fld.Id, &fld.Title, &parentFldID, &fld.NbChildrenFolders) 698 | if db.err != nil { 699 | log.WithFields(log.Fields{ 700 | "err": db.err, 701 | }).Error("GetChildrenFolders:error scanning the query result row") 702 | return nil 703 | } 704 | fld.Parent = &types.Folder{Id: int(parentFldID.Int64)} 705 | flds = append(flds, fld) 706 | } 707 | if db.err = rows.Err(); db.err != nil { 708 | log.WithFields(log.Fields{ 709 | "err": db.err, 710 | }).Error("GetChildrenFolders:error looping rows") 711 | return nil 712 | } 713 | return flds 714 | } 715 | 716 | } 717 | 718 | // SaveFolder saves the given new Folder into the db and returns the folder id. 719 | // Called only on folder creation or rename 720 | // so only the Title has to be set. 721 | func (db *SQLiteDataStore) SaveFolder(f *types.Folder) int64 { 722 | 723 | log.WithFields(log.Fields{ 724 | "f": f, 725 | }).Debug("SaveFolder") 726 | 727 | // Leaving silently on past errors... 728 | if db.err != nil { 729 | return 0 730 | } 731 | var stmt *sql.Stmt 732 | 733 | // Preparing the query. 734 | // id will be auto incremented 735 | if stmt, db.err = db.Prepare("INSERT INTO folder(title, parentFolderId, nbChildrenFolders) values(?,?,?)"); db.err != nil { 736 | log.WithFields(log.Fields{ 737 | "err": db.err, 738 | }).Error("SaveFolder:SELECT request prepare error") 739 | return 0 740 | } 741 | defer func() { 742 | if db.err = stmt.Close(); db.err != nil { 743 | log.WithFields(log.Fields{ 744 | "err": db.err, 745 | }).Error("SaveFolder:error closing stmt") 746 | } 747 | }() 748 | 749 | // Executing the query. 750 | var res sql.Result 751 | if f.Parent != nil { 752 | res, db.err = stmt.Exec(f.Title, f.Parent.Id, f.NbChildrenFolders) 753 | } else { 754 | res, db.err = stmt.Exec(f.Title, 1, f.NbChildrenFolders) 755 | } 756 | id, _ := res.LastInsertId() // we should check the error here too... 757 | if db.err != nil { 758 | log.WithFields(log.Fields{ 759 | "err": db.err, 760 | }).Error("SaveFolder:INSERT query error") 761 | return 0 762 | } 763 | return id 764 | 765 | } 766 | 767 | // UpdateBookmark updates the given bookmark. 768 | func (db *SQLiteDataStore) UpdateBookmark(b *types.Bookmark) { 769 | 770 | log.WithFields(log.Fields{ 771 | "b": b, 772 | }).Debug("UpdateBookmark") 773 | 774 | // Leaving silently on past errors... 775 | if db.err != nil { 776 | return 777 | } 778 | 779 | var ( 780 | stmt *sql.Stmt 781 | tx *sql.Tx 782 | ) 783 | 784 | // Beginning a new transaction. 785 | // TODO: is a transaction needed here? 786 | tx, db.err = db.Begin() 787 | if db.err != nil { 788 | log.WithFields(log.Fields{ 789 | "err": db.err, 790 | }).Error("Update bookmark:transaction begin failed") 791 | return 792 | } 793 | 794 | // Preparing the update request. 795 | stmt, db.err = tx.Prepare("UPDATE bookmark SET title=?, url=?, folderId=?, starred=?, favicon=? WHERE id=?") 796 | if db.err != nil { 797 | log.WithFields(log.Fields{ 798 | "err": db.err, 799 | }).Error("Update bookmark:UPDATE request prepare error") 800 | return 801 | } 802 | defer func() { 803 | if db.err = stmt.Close(); db.err != nil { 804 | log.WithFields(log.Fields{ 805 | "err": db.err, 806 | }).Error("UpdateBookmark:error closing stmt") 807 | } 808 | }() 809 | 810 | // Executing the query. 811 | if b.Folder != nil { 812 | _, db.err = stmt.Exec(b.Title, b.URL, b.Folder.Id, b.Starred, b.Favicon, b.Id) 813 | } else { 814 | _, db.err = stmt.Exec(b.Title, b.URL, 1, b.Starred, b.Favicon, b.Id) 815 | } 816 | // Rolling back on errors, or commit. 817 | if db.err != nil { 818 | log.WithFields(log.Fields{ 819 | "err": db.err, 820 | }).Error("UpdateBookmark: UPDATE bookmark error") 821 | if db.err = tx.Rollback(); db.err != nil { 822 | // Just logging the error. 823 | log.WithFields(log.Fields{ 824 | "err": db.err, 825 | }).Error("UpdateBookmark: UPDATE query transaction rollback error") 826 | return 827 | } 828 | return 829 | } 830 | if db.err = tx.Commit(); db.err != nil { 831 | // Just logging the error. 832 | log.WithFields(log.Fields{ 833 | "err": db.err, 834 | }).Error("UpdateBookmark: UPDATE bookmark transaction commit error") 835 | } 836 | 837 | // 838 | // Tags 839 | // 840 | // lazily deleting current tags 841 | _, db.err = db.Exec("DELETE from bookmarktag WHERE bookmarkId IS ?", b.Id) 842 | if db.err != nil { 843 | log.WithFields(log.Fields{ 844 | "err": db.err, 845 | }).Error("UpdateBookmark: DELETE bookmarktag query error") 846 | return 847 | } 848 | // inserting new tags 849 | for _, t := range b.Tags { 850 | log.WithFields(log.Fields{"t": t}).Debug("UpdateBookmark") 851 | // new tag id 852 | var ntid int 853 | // getting new tag from db 854 | nt := db.GetTag(t.Id) 855 | if nt == nil { 856 | // inserting the new tag into the db if it does not exist 857 | ntid = int(db.SaveTag(t)) 858 | } else { 859 | ntid = nt.Id 860 | } 861 | 862 | // linking the new tag to the bookmark 863 | log.WithFields(log.Fields{"b.Id": b.Id, "ntid": ntid}).Debug("UpdateBookmark") 864 | _, db.err = db.Exec("INSERT INTO bookmarktag(bookmarkId, tagId) values(?,?)", b.Id, ntid) 865 | if db.err != nil { 866 | log.WithFields(log.Fields{ 867 | "err": db.err, 868 | }).Error("UpdateBookmark: INSERT bookmarktag query error") 869 | return 870 | } 871 | } 872 | // cleaning orphan tags 873 | _, db.err = db.Exec("DELETE FROM tag WHERE tag.id NOT IN (SELECT tagId FROM bookmarktag)") 874 | if db.err != nil { 875 | log.WithFields(log.Fields{ 876 | "err": db.err, 877 | }).Error("UpdateBookmark: DELETE tag query error") 878 | return 879 | } 880 | 881 | } 882 | 883 | // SaveTag saves the new given Tag into the db 884 | func (db *SQLiteDataStore) SaveTag(t *types.Tag) int64 { 885 | 886 | log.WithFields(log.Fields{ 887 | "t": t, 888 | }).Debug("SaveTag") 889 | 890 | // Leaving silently on past errors... 891 | if db.err != nil { 892 | return 0 893 | } 894 | 895 | // Preparing the query. 896 | var stmt *sql.Stmt 897 | stmt, db.err = db.Prepare("INSERT INTO tag(name) values(?)") 898 | if db.err != nil { 899 | log.WithFields(log.Fields{ 900 | "err": db.err, 901 | }).Error("SaveTag:INSERT request prepare error") 902 | return 0 903 | } 904 | defer func() { 905 | if db.err = stmt.Close(); db.err != nil { 906 | log.WithFields(log.Fields{ 907 | "err": db.err, 908 | }).Error("SaveTag:error closing stmt") 909 | } 910 | }() 911 | 912 | // Executing the query. 913 | var res sql.Result 914 | res, db.err = stmt.Exec(t.Name) 915 | if db.err != nil { 916 | log.WithFields(log.Fields{ 917 | "err": db.err, 918 | }).Error("SaveTag:INSERT query error") 919 | return 0 920 | } 921 | 922 | id, _ := res.LastInsertId() 923 | return id 924 | 925 | } 926 | 927 | // SaveBookmark saves the new given Bookmark into the db 928 | func (db *SQLiteDataStore) SaveBookmark(b *types.Bookmark) int64 { 929 | 930 | log.WithFields(log.Fields{ 931 | "b": b, 932 | }).Debug("SaveBookmark") 933 | 934 | // Leaving silently on past errors... 935 | if db.err != nil { 936 | return 0 937 | } 938 | 939 | // 940 | // Bookmark 941 | // 942 | // Preparing the query. 943 | var stmt *sql.Stmt 944 | stmt, db.err = db.Prepare("INSERT INTO bookmark(title, url, folderId, favicon) values(?,?,?,?)") 945 | if db.err != nil { 946 | log.WithFields(log.Fields{ 947 | "err": db.err, 948 | }).Error("SaveBookmark:INSERT request prepare error") 949 | return 0 950 | } 951 | defer func() { 952 | if db.err = stmt.Close(); db.err != nil { 953 | log.WithFields(log.Fields{ 954 | "err": db.err, 955 | }).Error("SaveBookmark:error closing stmt") 956 | } 957 | }() 958 | 959 | // Executing the query. 960 | var res sql.Result 961 | if b.Folder != nil { 962 | res, db.err = stmt.Exec(b.Title, b.URL, b.Folder.Id, b.Favicon) 963 | } else { 964 | res, db.err = stmt.Exec(b.Title, b.URL, 1, b.Favicon) 965 | } 966 | if db.err != nil { 967 | log.WithFields(log.Fields{ 968 | "err": db.err, 969 | }).Error("SaveBookmark:INSERT query error") 970 | return 0 971 | } 972 | id, _ := res.LastInsertId() 973 | 974 | // 975 | // Tags 976 | // 977 | // inserting new tags 978 | for _, t := range b.Tags { 979 | log.WithFields(log.Fields{"t": t}).Debug("SaveBookmark") 980 | // new tag id 981 | var ntid int 982 | // getting new tag from db 983 | nt := db.GetTag(t.Id) 984 | if nt == nil { 985 | // inserting the new tag into the db if it does not exist 986 | ntid = int(db.SaveTag(t)) 987 | } else { 988 | ntid = nt.Id 989 | } 990 | 991 | // linking the new tag to the bookmark 992 | log.WithFields(log.Fields{"b.Id": b.Id, "ntid": ntid}).Debug("SaveBookmark") 993 | if _, db.err = db.Exec("INSERT INTO bookmarktag(bookmarkId, tagId) values(?,?)", b.Id, ntid); db.err != nil { 994 | return 0 995 | } 996 | } 997 | 998 | return id 999 | 1000 | } 1001 | 1002 | // DeleteBookmark delete the given Bookmark from the db 1003 | func (db *SQLiteDataStore) DeleteBookmark(b *types.Bookmark) { 1004 | 1005 | log.WithFields(log.Fields{ 1006 | "b": b, 1007 | }).Debug("DeleteBookmark") 1008 | 1009 | // Leaving silently on past errors... 1010 | if db.err != nil { 1011 | return 1012 | } 1013 | 1014 | // Executing the query. 1015 | _, db.err = db.Exec("DELETE from bookmark WHERE id=?", b.Id) 1016 | if db.err != nil { 1017 | log.WithFields(log.Fields{ 1018 | "err": db.err, 1019 | }).Error("DeleteBookmark:DELETE query error") 1020 | return 1021 | } 1022 | 1023 | } 1024 | 1025 | // UpdateFolder updates the given folder. 1026 | func (db *SQLiteDataStore) UpdateFolder(f *types.Folder) { 1027 | 1028 | log.WithFields(log.Fields{ 1029 | "f": f, 1030 | }).Debug("UpdateFolder") 1031 | 1032 | // Leaving silently on past errors... 1033 | if db.err != nil { 1034 | return 1035 | } 1036 | 1037 | var oldParentFolderID sql.NullInt64 1038 | // Retrieving the parentFolderId of the folder to be updated. 1039 | if db.err = db.QueryRow("SELECT parentFolderId from folder WHERE id=?", f.Id).Scan(&oldParentFolderID); db.err != nil { 1040 | log.WithFields(log.Fields{ 1041 | "err": db.err, 1042 | }).Error("UpdateFolder:SELECT query error") 1043 | return 1044 | } 1045 | log.WithFields(log.Fields{ 1046 | "oldParentFolderId": oldParentFolderID, 1047 | "f.Parent": f.Parent, 1048 | }).Debug("UpdateFolder") 1049 | 1050 | // Preparing the update request for the folder. 1051 | var stmt *sql.Stmt 1052 | stmt, db.err = db.Prepare("UPDATE folder SET title=?, parentFolderId=?, nbChildrenFolders=(SELECT count(*) from folder WHERE parentFolderId=?) WHERE id=?") 1053 | if db.err != nil { 1054 | log.WithFields(log.Fields{ 1055 | "err": db.err, 1056 | }).Error("UpdateFolder:UPDATE request prepare error") 1057 | return 1058 | } 1059 | defer func() { 1060 | if db.err = stmt.Close(); db.err != nil { 1061 | log.WithFields(log.Fields{ 1062 | "err": db.err, 1063 | }).Error("UpdateFolder:error closing stmt") 1064 | } 1065 | }() 1066 | 1067 | // Executing the query. 1068 | if f.Parent != nil { 1069 | _, db.err = stmt.Exec(f.Title, f.Parent.Id, f.Id, f.Id) 1070 | } else { 1071 | _, db.err = stmt.Exec(f.Title, 1, f.Id, f.Id) 1072 | } 1073 | if db.err != nil { 1074 | log.WithFields(log.Fields{ 1075 | "err": db.err, 1076 | }).Error("UpdateFolder:UPDATE query error") 1077 | return 1078 | } 1079 | 1080 | // Preparing the update request for the old and new parent folders (to update the nbChildrenFolders). 1081 | stmt, db.err = db.Prepare("UPDATE folder SET nbChildrenFolders=(SELECT count(*) from folder WHERE parentFolderId=?) WHERE id=?") 1082 | if db.err != nil { 1083 | log.WithFields(log.Fields{ 1084 | "err": db.err, 1085 | }).Error("UpdateFolder:UPDATE old parent request prepare error") 1086 | return 1087 | } 1088 | defer func() { 1089 | if db.err = stmt.Close(); db.err != nil { 1090 | log.WithFields(log.Fields{ 1091 | "err": db.err, 1092 | }).Error("UpdateFolder:error closing stmt") 1093 | } 1094 | }() 1095 | 1096 | // Executing the query for the old parent. 1097 | if _, db.err = stmt.Exec(oldParentFolderID, oldParentFolderID); db.err != nil { 1098 | log.WithFields(log.Fields{ 1099 | "err": db.err, 1100 | }).Error("UpdateFolder:UPDATE old parent request error") 1101 | return 1102 | } 1103 | // And the new. 1104 | if f.Parent != nil { 1105 | if _, db.err = stmt.Exec(f.Parent.Id, f.Parent.Id); db.err != nil { 1106 | log.WithFields(log.Fields{ 1107 | "err": db.err, 1108 | }).Error("UpdateFolder:UPDATE new parent request error") 1109 | return 1110 | } 1111 | } 1112 | 1113 | } 1114 | 1115 | // DeleteFolder delete the given Folder from the db. 1116 | func (db *SQLiteDataStore) DeleteFolder(f *types.Folder) { 1117 | 1118 | log.WithFields(log.Fields{ 1119 | "f": f, 1120 | }).Debug("DeleteFolder") 1121 | 1122 | // Leaving silently on past errors... 1123 | if db.err != nil { 1124 | return 1125 | } 1126 | 1127 | // Executing the query. 1128 | _, db.err = db.Exec("DELETE from folder WHERE id=?", f.Id) 1129 | if db.err != nil { 1130 | log.WithFields(log.Fields{ 1131 | "err": db.err, 1132 | }).Error("DeleteFolder:DELETE query error") 1133 | return 1134 | } 1135 | 1136 | } 1137 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbellembois/gobkm/7f373d50cb801a247ea25e6ce03a528089ff5261/screenshot.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /static/wasm/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbellembois/gobkm/7f373d50cb801a247ea25e6ce03a528089ff5261/static/wasm/main.wasm -------------------------------------------------------------------------------- /static/wasm/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | "use strict"; 6 | 7 | (() => { 8 | const enosys = () => { 9 | const err = new Error("not implemented"); 10 | err.code = "ENOSYS"; 11 | return err; 12 | }; 13 | 14 | if (!globalThis.fs) { 15 | let outputBuf = ""; 16 | globalThis.fs = { 17 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 18 | writeSync(fd, buf) { 19 | outputBuf += decoder.decode(buf); 20 | const nl = outputBuf.lastIndexOf("\n"); 21 | if (nl != -1) { 22 | console.log(outputBuf.substr(0, nl)); 23 | outputBuf = outputBuf.substr(nl + 1); 24 | } 25 | return buf.length; 26 | }, 27 | write(fd, buf, offset, length, position, callback) { 28 | if (offset !== 0 || length !== buf.length || position !== null) { 29 | callback(enosys()); 30 | return; 31 | } 32 | const n = this.writeSync(fd, buf); 33 | callback(null, n); 34 | }, 35 | chmod(path, mode, callback) { callback(enosys()); }, 36 | chown(path, uid, gid, callback) { callback(enosys()); }, 37 | close(fd, callback) { callback(enosys()); }, 38 | fchmod(fd, mode, callback) { callback(enosys()); }, 39 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 40 | fstat(fd, callback) { callback(enosys()); }, 41 | fsync(fd, callback) { callback(null); }, 42 | ftruncate(fd, length, callback) { callback(enosys()); }, 43 | lchown(path, uid, gid, callback) { callback(enosys()); }, 44 | link(path, link, callback) { callback(enosys()); }, 45 | lstat(path, callback) { callback(enosys()); }, 46 | mkdir(path, perm, callback) { callback(enosys()); }, 47 | open(path, flags, mode, callback) { callback(enosys()); }, 48 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 49 | readdir(path, callback) { callback(enosys()); }, 50 | readlink(path, callback) { callback(enosys()); }, 51 | rename(from, to, callback) { callback(enosys()); }, 52 | rmdir(path, callback) { callback(enosys()); }, 53 | stat(path, callback) { callback(enosys()); }, 54 | symlink(path, link, callback) { callback(enosys()); }, 55 | truncate(path, length, callback) { callback(enosys()); }, 56 | unlink(path, callback) { callback(enosys()); }, 57 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 58 | }; 59 | } 60 | 61 | if (!globalThis.process) { 62 | globalThis.process = { 63 | getuid() { return -1; }, 64 | getgid() { return -1; }, 65 | geteuid() { return -1; }, 66 | getegid() { return -1; }, 67 | getgroups() { throw enosys(); }, 68 | pid: -1, 69 | ppid: -1, 70 | umask() { throw enosys(); }, 71 | cwd() { throw enosys(); }, 72 | chdir() { throw enosys(); }, 73 | } 74 | } 75 | 76 | if (!globalThis.crypto) { 77 | throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); 78 | } 79 | 80 | if (!globalThis.performance) { 81 | throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); 82 | } 83 | 84 | if (!globalThis.TextEncoder) { 85 | throw new Error("globalThis.TextEncoder is not available, polyfill required"); 86 | } 87 | 88 | if (!globalThis.TextDecoder) { 89 | throw new Error("globalThis.TextDecoder is not available, polyfill required"); 90 | } 91 | 92 | const encoder = new TextEncoder("utf-8"); 93 | const decoder = new TextDecoder("utf-8"); 94 | 95 | globalThis.Go = class { 96 | constructor() { 97 | this.argv = ["js"]; 98 | this.env = {}; 99 | this.exit = (code) => { 100 | if (code !== 0) { 101 | console.warn("exit code:", code); 102 | } 103 | }; 104 | this._exitPromise = new Promise((resolve) => { 105 | this._resolveExitPromise = resolve; 106 | }); 107 | this._pendingEvent = null; 108 | this._scheduledTimeouts = new Map(); 109 | this._nextCallbackTimeoutID = 1; 110 | 111 | const setInt64 = (addr, v) => { 112 | this.mem.setUint32(addr + 0, v, true); 113 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 114 | } 115 | 116 | const getInt64 = (addr) => { 117 | const low = this.mem.getUint32(addr + 0, true); 118 | const high = this.mem.getInt32(addr + 4, true); 119 | return low + high * 4294967296; 120 | } 121 | 122 | const loadValue = (addr) => { 123 | const f = this.mem.getFloat64(addr, true); 124 | if (f === 0) { 125 | return undefined; 126 | } 127 | if (!isNaN(f)) { 128 | return f; 129 | } 130 | 131 | const id = this.mem.getUint32(addr, true); 132 | return this._values[id]; 133 | } 134 | 135 | const storeValue = (addr, v) => { 136 | const nanHead = 0x7FF80000; 137 | 138 | if (typeof v === "number" && v !== 0) { 139 | if (isNaN(v)) { 140 | this.mem.setUint32(addr + 4, nanHead, true); 141 | this.mem.setUint32(addr, 0, true); 142 | return; 143 | } 144 | this.mem.setFloat64(addr, v, true); 145 | return; 146 | } 147 | 148 | if (v === undefined) { 149 | this.mem.setFloat64(addr, 0, true); 150 | return; 151 | } 152 | 153 | let id = this._ids.get(v); 154 | if (id === undefined) { 155 | id = this._idPool.pop(); 156 | if (id === undefined) { 157 | id = this._values.length; 158 | } 159 | this._values[id] = v; 160 | this._goRefCounts[id] = 0; 161 | this._ids.set(v, id); 162 | } 163 | this._goRefCounts[id]++; 164 | let typeFlag = 0; 165 | switch (typeof v) { 166 | case "object": 167 | if (v !== null) { 168 | typeFlag = 1; 169 | } 170 | break; 171 | case "string": 172 | typeFlag = 2; 173 | break; 174 | case "symbol": 175 | typeFlag = 3; 176 | break; 177 | case "function": 178 | typeFlag = 4; 179 | break; 180 | } 181 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 182 | this.mem.setUint32(addr, id, true); 183 | } 184 | 185 | const loadSlice = (addr) => { 186 | const array = getInt64(addr + 0); 187 | const len = getInt64(addr + 8); 188 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 189 | } 190 | 191 | const loadSliceOfValues = (addr) => { 192 | const array = getInt64(addr + 0); 193 | const len = getInt64(addr + 8); 194 | const a = new Array(len); 195 | for (let i = 0; i < len; i++) { 196 | a[i] = loadValue(array + i * 8); 197 | } 198 | return a; 199 | } 200 | 201 | const loadString = (addr) => { 202 | const saddr = getInt64(addr + 0); 203 | const len = getInt64(addr + 8); 204 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 205 | } 206 | 207 | const timeOrigin = Date.now() - performance.now(); 208 | this.importObject = { 209 | go: { 210 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 211 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 212 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 213 | // This changes the SP, thus we have to update the SP used by the imported function. 214 | 215 | // func wasmExit(code int32) 216 | "runtime.wasmExit": (sp) => { 217 | sp >>>= 0; 218 | const code = this.mem.getInt32(sp + 8, true); 219 | this.exited = true; 220 | delete this._inst; 221 | delete this._values; 222 | delete this._goRefCounts; 223 | delete this._ids; 224 | delete this._idPool; 225 | this.exit(code); 226 | }, 227 | 228 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 229 | "runtime.wasmWrite": (sp) => { 230 | sp >>>= 0; 231 | const fd = getInt64(sp + 8); 232 | const p = getInt64(sp + 16); 233 | const n = this.mem.getInt32(sp + 24, true); 234 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 235 | }, 236 | 237 | // func resetMemoryDataView() 238 | "runtime.resetMemoryDataView": (sp) => { 239 | sp >>>= 0; 240 | this.mem = new DataView(this._inst.exports.mem.buffer); 241 | }, 242 | 243 | // func nanotime1() int64 244 | "runtime.nanotime1": (sp) => { 245 | sp >>>= 0; 246 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 247 | }, 248 | 249 | // func walltime() (sec int64, nsec int32) 250 | "runtime.walltime": (sp) => { 251 | sp >>>= 0; 252 | const msec = (new Date).getTime(); 253 | setInt64(sp + 8, msec / 1000); 254 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 255 | }, 256 | 257 | // func scheduleTimeoutEvent(delay int64) int32 258 | "runtime.scheduleTimeoutEvent": (sp) => { 259 | sp >>>= 0; 260 | const id = this._nextCallbackTimeoutID; 261 | this._nextCallbackTimeoutID++; 262 | this._scheduledTimeouts.set(id, setTimeout( 263 | () => { 264 | this._resume(); 265 | while (this._scheduledTimeouts.has(id)) { 266 | // for some reason Go failed to register the timeout event, log and try again 267 | // (temporary workaround for https://github.com/golang/go/issues/28975) 268 | console.warn("scheduleTimeoutEvent: missed timeout event"); 269 | this._resume(); 270 | } 271 | }, 272 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 273 | )); 274 | this.mem.setInt32(sp + 16, id, true); 275 | }, 276 | 277 | // func clearTimeoutEvent(id int32) 278 | "runtime.clearTimeoutEvent": (sp) => { 279 | sp >>>= 0; 280 | const id = this.mem.getInt32(sp + 8, true); 281 | clearTimeout(this._scheduledTimeouts.get(id)); 282 | this._scheduledTimeouts.delete(id); 283 | }, 284 | 285 | // func getRandomData(r []byte) 286 | "runtime.getRandomData": (sp) => { 287 | sp >>>= 0; 288 | crypto.getRandomValues(loadSlice(sp + 8)); 289 | }, 290 | 291 | // func finalizeRef(v ref) 292 | "syscall/js.finalizeRef": (sp) => { 293 | sp >>>= 0; 294 | const id = this.mem.getUint32(sp + 8, true); 295 | this._goRefCounts[id]--; 296 | if (this._goRefCounts[id] === 0) { 297 | const v = this._values[id]; 298 | this._values[id] = null; 299 | this._ids.delete(v); 300 | this._idPool.push(id); 301 | } 302 | }, 303 | 304 | // func stringVal(value string) ref 305 | "syscall/js.stringVal": (sp) => { 306 | sp >>>= 0; 307 | storeValue(sp + 24, loadString(sp + 8)); 308 | }, 309 | 310 | // func valueGet(v ref, p string) ref 311 | "syscall/js.valueGet": (sp) => { 312 | sp >>>= 0; 313 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 314 | sp = this._inst.exports.getsp() >>> 0; // see comment above 315 | storeValue(sp + 32, result); 316 | }, 317 | 318 | // func valueSet(v ref, p string, x ref) 319 | "syscall/js.valueSet": (sp) => { 320 | sp >>>= 0; 321 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 322 | }, 323 | 324 | // func valueDelete(v ref, p string) 325 | "syscall/js.valueDelete": (sp) => { 326 | sp >>>= 0; 327 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 328 | }, 329 | 330 | // func valueIndex(v ref, i int) ref 331 | "syscall/js.valueIndex": (sp) => { 332 | sp >>>= 0; 333 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 334 | }, 335 | 336 | // valueSetIndex(v ref, i int, x ref) 337 | "syscall/js.valueSetIndex": (sp) => { 338 | sp >>>= 0; 339 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 340 | }, 341 | 342 | // func valueCall(v ref, m string, args []ref) (ref, bool) 343 | "syscall/js.valueCall": (sp) => { 344 | sp >>>= 0; 345 | try { 346 | const v = loadValue(sp + 8); 347 | const m = Reflect.get(v, loadString(sp + 16)); 348 | const args = loadSliceOfValues(sp + 32); 349 | const result = Reflect.apply(m, v, args); 350 | sp = this._inst.exports.getsp() >>> 0; // see comment above 351 | storeValue(sp + 56, result); 352 | this.mem.setUint8(sp + 64, 1); 353 | } catch (err) { 354 | sp = this._inst.exports.getsp() >>> 0; // see comment above 355 | storeValue(sp + 56, err); 356 | this.mem.setUint8(sp + 64, 0); 357 | } 358 | }, 359 | 360 | // func valueInvoke(v ref, args []ref) (ref, bool) 361 | "syscall/js.valueInvoke": (sp) => { 362 | sp >>>= 0; 363 | try { 364 | const v = loadValue(sp + 8); 365 | const args = loadSliceOfValues(sp + 16); 366 | const result = Reflect.apply(v, undefined, args); 367 | sp = this._inst.exports.getsp() >>> 0; // see comment above 368 | storeValue(sp + 40, result); 369 | this.mem.setUint8(sp + 48, 1); 370 | } catch (err) { 371 | sp = this._inst.exports.getsp() >>> 0; // see comment above 372 | storeValue(sp + 40, err); 373 | this.mem.setUint8(sp + 48, 0); 374 | } 375 | }, 376 | 377 | // func valueNew(v ref, args []ref) (ref, bool) 378 | "syscall/js.valueNew": (sp) => { 379 | sp >>>= 0; 380 | try { 381 | const v = loadValue(sp + 8); 382 | const args = loadSliceOfValues(sp + 16); 383 | const result = Reflect.construct(v, args); 384 | sp = this._inst.exports.getsp() >>> 0; // see comment above 385 | storeValue(sp + 40, result); 386 | this.mem.setUint8(sp + 48, 1); 387 | } catch (err) { 388 | sp = this._inst.exports.getsp() >>> 0; // see comment above 389 | storeValue(sp + 40, err); 390 | this.mem.setUint8(sp + 48, 0); 391 | } 392 | }, 393 | 394 | // func valueLength(v ref) int 395 | "syscall/js.valueLength": (sp) => { 396 | sp >>>= 0; 397 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 398 | }, 399 | 400 | // valuePrepareString(v ref) (ref, int) 401 | "syscall/js.valuePrepareString": (sp) => { 402 | sp >>>= 0; 403 | const str = encoder.encode(String(loadValue(sp + 8))); 404 | storeValue(sp + 16, str); 405 | setInt64(sp + 24, str.length); 406 | }, 407 | 408 | // valueLoadString(v ref, b []byte) 409 | "syscall/js.valueLoadString": (sp) => { 410 | sp >>>= 0; 411 | const str = loadValue(sp + 8); 412 | loadSlice(sp + 16).set(str); 413 | }, 414 | 415 | // func valueInstanceOf(v ref, t ref) bool 416 | "syscall/js.valueInstanceOf": (sp) => { 417 | sp >>>= 0; 418 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 419 | }, 420 | 421 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 422 | "syscall/js.copyBytesToGo": (sp) => { 423 | sp >>>= 0; 424 | const dst = loadSlice(sp + 8); 425 | const src = loadValue(sp + 32); 426 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 427 | this.mem.setUint8(sp + 48, 0); 428 | return; 429 | } 430 | const toCopy = src.subarray(0, dst.length); 431 | dst.set(toCopy); 432 | setInt64(sp + 40, toCopy.length); 433 | this.mem.setUint8(sp + 48, 1); 434 | }, 435 | 436 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 437 | "syscall/js.copyBytesToJS": (sp) => { 438 | sp >>>= 0; 439 | const dst = loadValue(sp + 8); 440 | const src = loadSlice(sp + 16); 441 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 442 | this.mem.setUint8(sp + 48, 0); 443 | return; 444 | } 445 | const toCopy = src.subarray(0, dst.length); 446 | dst.set(toCopy); 447 | setInt64(sp + 40, toCopy.length); 448 | this.mem.setUint8(sp + 48, 1); 449 | }, 450 | 451 | "debug": (value) => { 452 | console.log(value); 453 | }, 454 | } 455 | }; 456 | } 457 | 458 | async run(instance) { 459 | if (!(instance instanceof WebAssembly.Instance)) { 460 | throw new Error("Go.run: WebAssembly.Instance expected"); 461 | } 462 | this._inst = instance; 463 | this.mem = new DataView(this._inst.exports.mem.buffer); 464 | this._values = [ // JS values that Go currently has references to, indexed by reference id 465 | NaN, 466 | 0, 467 | null, 468 | true, 469 | false, 470 | globalThis, 471 | this, 472 | ]; 473 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 474 | this._ids = new Map([ // mapping from JS values to reference ids 475 | [0, 1], 476 | [null, 2], 477 | [true, 3], 478 | [false, 4], 479 | [globalThis, 5], 480 | [this, 6], 481 | ]); 482 | this._idPool = []; // unused ids that have been garbage collected 483 | this.exited = false; // whether the Go program has exited 484 | 485 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 486 | let offset = 4096; 487 | 488 | const strPtr = (str) => { 489 | const ptr = offset; 490 | const bytes = encoder.encode(str + "\0"); 491 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 492 | offset += bytes.length; 493 | if (offset % 8 !== 0) { 494 | offset += 8 - (offset % 8); 495 | } 496 | return ptr; 497 | }; 498 | 499 | const argc = this.argv.length; 500 | 501 | const argvPtrs = []; 502 | this.argv.forEach((arg) => { 503 | argvPtrs.push(strPtr(arg)); 504 | }); 505 | argvPtrs.push(0); 506 | 507 | const keys = Object.keys(this.env).sort(); 508 | keys.forEach((key) => { 509 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 510 | }); 511 | argvPtrs.push(0); 512 | 513 | const argv = offset; 514 | argvPtrs.forEach((ptr) => { 515 | this.mem.setUint32(offset, ptr, true); 516 | this.mem.setUint32(offset + 4, 0, true); 517 | offset += 8; 518 | }); 519 | 520 | // The linker guarantees global data starts from at least wasmMinDataAddr. 521 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 522 | const wasmMinDataAddr = 4096 + 8192; 523 | if (offset >= wasmMinDataAddr) { 524 | throw new Error("total length of command line and environment variables exceeds limit"); 525 | } 526 | 527 | this._inst.exports.run(argc, argv); 528 | if (this.exited) { 529 | this._resolveExitPromise(); 530 | } 531 | await this._exitPromise; 532 | } 533 | 534 | _resume() { 535 | if (this.exited) { 536 | throw new Error("Go program has already exited"); 537 | } 538 | this._inst.exports.resume(); 539 | if (this.exited) { 540 | this._resolveExitPromise(); 541 | } 542 | } 543 | 544 | _makeFuncWrapper(id) { 545 | const go = this; 546 | return function () { 547 | const event = { id: id, this: this, args: arguments }; 548 | go._pendingEvent = event; 549 | go._resume(); 550 | return event.result; 551 | }; 552 | } 553 | } 554 | })(); 555 | -------------------------------------------------------------------------------- /types/bookmark.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | // Folder containing the bookmarks 9 | type Folder struct { 10 | Id int `json:"id"` 11 | Title string `json:"title"` 12 | Parent *Folder `json:"parent"` 13 | Folders []*Folder `json:"folders"` 14 | Bookmarks []*Bookmark `json:"bookmarks"` 15 | NbChildrenFolders int `json:"nbchildrenfolders"` 16 | } 17 | 18 | // Bookmark 19 | type Bookmark struct { 20 | Id int `json:"id"` 21 | Title string `json:"title"` 22 | URL string `json:"url"` 23 | Favicon string `json:"favicon"` // base64 encoded image 24 | Starred bool `json:"starred"` 25 | Folder *Folder `json:"folder"` // reference to the folder to help 26 | Tags []*Tag `json:"tags"` 27 | } 28 | 29 | // Tag represents a bookmark tag 30 | type Tag struct { 31 | Id int `json:"id"` 32 | Name string `json:"name"` 33 | } 34 | 35 | // Bookmarks implements the sort interface 36 | type Bookmarks []*Bookmark 37 | 38 | func (b Bookmarks) Len() int { 39 | return len(b) 40 | } 41 | 42 | func (b Bookmarks) Swap(i, j int) { 43 | b[i], b[j] = b[j], b[i] 44 | } 45 | 46 | func (b Bookmarks) Less(i, j int) bool { 47 | url1 := b[i].Title 48 | url2 := b[j].Title 49 | title1 := url1[strings.Index(url1, "//")+2:] 50 | title2 := url2[strings.Index(url2, "//")+2:] 51 | return title1 < title2 52 | } 53 | 54 | func (bk *Bookmark) String() string { 55 | var out []byte 56 | var err error 57 | 58 | if out, err = json.Marshal(bk); err != nil { 59 | return "" 60 | } 61 | return string(out) 62 | } 63 | 64 | // PathString returns the bookmark full path as a string 65 | func (bk *Bookmark) PathString() string { 66 | var ( 67 | p *Folder 68 | r string 69 | ) 70 | for p = bk.Folder; p != nil; p = p.Parent { 71 | r += "/" + p.Title 72 | } 73 | return r 74 | } 75 | 76 | func (fd *Folder) String() string { 77 | var out []byte 78 | var err error 79 | 80 | if out, err = json.Marshal(fd); err != nil { 81 | return "" 82 | } 83 | return string(out) 84 | } 85 | 86 | // IsRootFolder returns true if the given Folder has no parent 87 | func (fd *Folder) IsRootFolder() bool { 88 | return fd.Parent == nil 89 | } 90 | 91 | // HasChildrenFolders returns true if the given Folder has children 92 | func (fd *Folder) HasChildrenFolders() bool { 93 | return fd.NbChildrenFolders > 0 94 | } 95 | --------------------------------------------------------------------------------