github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/advisor/backend.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package advisor 21 22 import ( 23 "encoding/json" 24 "os" 25 "path/filepath" 26 "time" 27 28 "github.com/snapcore/bolt" 29 30 "github.com/snapcore/snapd/dirs" 31 "github.com/snapcore/snapd/osutil" 32 "github.com/snapcore/snapd/randutil" 33 ) 34 35 var ( 36 cmdBucketKey = []byte("Commands") 37 pkgBucketKey = []byte("Snaps") 38 ) 39 40 type writer struct { 41 fn string 42 db *bolt.DB 43 tx *bolt.Tx 44 cmdBucket *bolt.Bucket 45 pkgBucket *bolt.Bucket 46 } 47 48 type CommandDB interface { 49 // AddSnap adds the entries for commands pointing to the given 50 // snap name to the commands database. 51 AddSnap(snapName, version, summary string, commands []string) error 52 // Commit persist the changes, and closes the database. If the 53 // database has already been committed/rollbacked, does nothing. 54 Commit() error 55 // Rollback aborts the changes, and closes the database. If the 56 // database has already been committed/rollbacked, does nothing. 57 Rollback() error 58 } 59 60 // Create opens the commands database for writing, and starts a 61 // transaction that drops and recreates the buckets. You should then 62 // call AddSnap with each snap you wish to add, and them Commit the 63 // results to make the changes live, or Rollback to abort; either of 64 // these closes the database again. 65 func Create() (CommandDB, error) { 66 var err error 67 t := &writer{ 68 fn: dirs.SnapCommandsDB + "." + randutil.RandomString(12) + "~", 69 } 70 71 t.db, err = bolt.Open(t.fn, 0644, &bolt.Options{Timeout: 1 * time.Second}) 72 if err != nil { 73 return nil, err 74 } 75 76 t.tx, err = t.db.Begin(true) 77 if err == nil { 78 t.cmdBucket, err = t.tx.CreateBucket(cmdBucketKey) 79 if err == nil { 80 t.pkgBucket, err = t.tx.CreateBucket(pkgBucketKey) 81 } 82 83 if err != nil { 84 t.tx.Rollback() 85 } 86 } 87 88 if err != nil { 89 t.db.Close() 90 return nil, err 91 } 92 93 return t, nil 94 } 95 96 func (t *writer) AddSnap(snapName, version, summary string, commands []string) error { 97 for _, cmd := range commands { 98 var sil []Package 99 100 bcmd := []byte(cmd) 101 row := t.cmdBucket.Get(bcmd) 102 if row != nil { 103 if err := json.Unmarshal(row, &sil); err != nil { 104 return err 105 } 106 } 107 // For the mapping of command->snap we do not need the summary, nothing is using that. 108 sil = append(sil, Package{Snap: snapName, Version: version}) 109 row, err := json.Marshal(sil) 110 if err != nil { 111 return err 112 } 113 if err := t.cmdBucket.Put(bcmd, row); err != nil { 114 return err 115 } 116 } 117 118 // TODO: use json here as well and put the version information here 119 bj, err := json.Marshal(Package{ 120 Snap: snapName, 121 Version: version, 122 Summary: summary, 123 }) 124 if err != nil { 125 return err 126 } 127 if err := t.pkgBucket.Put([]byte(snapName), bj); err != nil { 128 return err 129 } 130 131 return nil 132 } 133 134 func (t *writer) Commit() error { 135 // either everything worked, and therefore this will fail, or something 136 // will fail, and that error is more important than this one if this one 137 // then fails as well. So, ignore the error. 138 defer os.Remove(t.fn) 139 140 if err := t.done(true); err != nil { 141 return err 142 } 143 144 dir, err := os.Open(filepath.Dir(dirs.SnapCommandsDB)) 145 if err != nil { 146 return err 147 } 148 defer dir.Close() 149 150 if err := os.Rename(t.fn, dirs.SnapCommandsDB); err != nil { 151 return err 152 } 153 154 return dir.Sync() 155 } 156 157 func (t *writer) Rollback() error { 158 e1 := t.done(false) 159 e2 := os.Remove(t.fn) 160 if e1 == nil { 161 return e2 162 } 163 return e1 164 } 165 166 func (t *writer) done(commit bool) error { 167 var e1, e2 error 168 169 t.cmdBucket = nil 170 t.pkgBucket = nil 171 if t.tx != nil { 172 if commit { 173 e1 = t.tx.Commit() 174 } else { 175 e1 = t.tx.Rollback() 176 } 177 t.tx = nil 178 } 179 if t.db != nil { 180 e2 = t.db.Close() 181 t.db = nil 182 } 183 if e1 == nil { 184 return e2 185 } 186 return e1 187 } 188 189 // DumpCommands returns the whole database as a map. For use in 190 // testing and debugging. 191 func DumpCommands() (map[string]string, error) { 192 db, err := bolt.Open(dirs.SnapCommandsDB, 0644, &bolt.Options{ 193 ReadOnly: true, 194 Timeout: 1 * time.Second, 195 }) 196 if err != nil { 197 return nil, err 198 } 199 defer db.Close() 200 201 tx, err := db.Begin(false) 202 if err != nil { 203 return nil, err 204 } 205 defer tx.Rollback() 206 207 b := tx.Bucket(cmdBucketKey) 208 if b == nil { 209 return nil, nil 210 } 211 212 m := map[string]string{} 213 c := b.Cursor() 214 for k, v := c.First(); k != nil; k, v = c.Next() { 215 m[string(k)] = string(v) 216 } 217 218 return m, nil 219 } 220 221 type boltFinder struct { 222 *bolt.DB 223 } 224 225 // Open the database for reading. 226 func Open() (Finder, error) { 227 // Check for missing file manually to workaround bug in bolt. 228 // bolt.Open() is using os.OpenFile(.., os.O_RDONLY | 229 // os.O_CREATE) even if ReadOnly mode is used. So we would get 230 // a misleading "permission denied" error without this check. 231 if !osutil.FileExists(dirs.SnapCommandsDB) { 232 return nil, os.ErrNotExist 233 } 234 db, err := bolt.Open(dirs.SnapCommandsDB, 0644, &bolt.Options{ 235 ReadOnly: true, 236 Timeout: 1 * time.Second, 237 }) 238 if err != nil { 239 return nil, err 240 } 241 242 return &boltFinder{db}, nil 243 } 244 245 func (f *boltFinder) FindCommand(command string) ([]Command, error) { 246 tx, err := f.Begin(false) 247 if err != nil { 248 return nil, err 249 } 250 defer tx.Rollback() 251 252 b := tx.Bucket(cmdBucketKey) 253 if b == nil { 254 return nil, nil 255 } 256 257 buf := b.Get([]byte(command)) 258 if buf == nil { 259 return nil, nil 260 } 261 var sil []Package 262 if err := json.Unmarshal(buf, &sil); err != nil { 263 return nil, err 264 } 265 cmds := make([]Command, len(sil)) 266 for i, si := range sil { 267 cmds[i] = Command{ 268 Snap: si.Snap, 269 Version: si.Version, 270 Command: command, 271 } 272 } 273 274 return cmds, nil 275 } 276 277 func (f *boltFinder) FindPackage(pkgName string) (*Package, error) { 278 tx, err := f.Begin(false) 279 if err != nil { 280 return nil, err 281 } 282 defer tx.Rollback() 283 284 b := tx.Bucket(pkgBucketKey) 285 if b == nil { 286 return nil, nil 287 } 288 289 bj := b.Get([]byte(pkgName)) 290 if bj == nil { 291 return nil, nil 292 } 293 var si Package 294 err = json.Unmarshal(bj, &si) 295 if err != nil { 296 return nil, err 297 } 298 299 return &Package{Snap: pkgName, Version: si.Version, Summary: si.Summary}, nil 300 }