github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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  }