diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb5ac84 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# user-config + +A go library for easily managing config files/directories in your XDGConfig directory \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..a849a08 --- /dev/null +++ b/config.go @@ -0,0 +1,128 @@ +// Package userConfig eases the use of config files in a user's home directory +package userConfig + +import ( + "errors" + "os" + "strings" + + "gogs.bullercodeworks.com/brian/user-config/ext/go-xdg" +) + +// Config is a stuct for managing the config +type Config struct { + name string + generalConfig *GeneralConfig +} + +// NewConfig generates a Config struct +func NewConfig(name string) (*Config, error) { + c := &Config{name: name} + if err := c.Load(); err != nil { + return c, err + } + return c, nil +} + +// Set at the config level sets a value in the .conf file +func (c *Config) Set(k, v string) error { + return c.generalConfig.Set(k, v) +} + +// Get at the config level retrieves a value from the .conf file +func (c *Config) Get(k string) string { + return c.generalConfig.Get(k) +} + +// GetConfigPath just returns the config path +func (c *Config) GetConfigPath() string { + return c.generalConfig.Path +} + +// Load loads config files into the config +func (c *Config) Load() error { + var err error + if strings.TrimSpace(c.name) == "" { + return errors.New("Invalid Config Name: " + c.name) + } + + var cfgPath string + cfgPath = xdg.Config.Dirs()[0] + if cfgPath != "" { + cfgPath = cfgPath + "/" + c.name + if err = c.verifyOrCreateDirectory(cfgPath); err != nil { + return err + } + // We always have a .conf file + //cfgPath = cfgPath + "/" + c.name + ".conf" + } + // Load general config + if c.generalConfig, err = NewGeneralConfig(c.name, cfgPath); err != nil { + return err + } + + return nil +} + +// Save writes the config to file(s) +func (c *Config) Save() error { + if c.generalConfig == nil { + return errors.New("Bad setup.") + } + return c.generalConfig.Save() + /* + var cfgPath string + var configLines []string + //configLines = append(configLines, "server="+client.ServerAddr) + //configLines = append(configLines, "key="+client.ServerKey) + cfgPath = os.Getenv("HOME") + if cfgPath != "" { + cfgPath = cfgPath + "/.config" + if err := c.verifyOrCreateDirectory(cfgPath); err != nil { + return err + } + cfgPath = cfgPath + "/" + c.name + } + if cfgPath != "" { + file, err := os.Create(cfgPath) + if err != nil { + // Couldn't load config even though one was specified + return err + } + defer file.Close() + + w := bufio.NewWriter(file) + for _, line := range configLines { + fmt.Fprintln(w, line) + } + if err = w.Flush(); err != nil { + return err + } + } + return nil + */ +} + +// verifyOrCreateDirectory is a helper function for building an +// individual directory +func (c *Config) verifyOrCreateDirectory(path string) error { + var tstDir *os.File + var tstDirInfo os.FileInfo + var err error + if tstDir, err = os.Open(path); err != nil { + if err = os.Mkdir(path, 0755); err != nil { + return err + } + if tstDir, err = os.Open(path); err != nil { + return err + } + } + if tstDirInfo, err = tstDir.Stat(); err != nil { + return err + } + if !tstDirInfo.IsDir() { + return errors.New(path + " exists and is not a directory") + } + // We were able to open the path and it was a directory + return nil +} diff --git a/config_file.go b/config_file.go new file mode 100644 index 0000000..076a7ce --- /dev/null +++ b/config_file.go @@ -0,0 +1,82 @@ +// Package userConfig eases the use of config files in a user's home directory +package userConfig + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "strings" + + "github.com/BurntSushi/toml" +) + +// GeneralConfig is the basic config structure +// All configs make with package userConfig will have this file +type GeneralConfig struct { + Name string `toml:"-"` + Path string `toml:"-"` + ConfigFiles []string `toml:"additional_config"` + RawFiles []string `toml:"raw_files"` + Values map[string]string `toml:"general"` +} + +// NewGeneralConfig generates a General Config struct +func NewGeneralConfig(name, path string) (*GeneralConfig, error) { + gf := &GeneralConfig{Name: name, Path: path} + gf.ConfigFiles = []string{} + gf.RawFiles = []string{} + gf.Values = make(map[string]string) + + if err := gf.Load(); err != nil { + return gf, err + } + return gf, nil +} + +// Load loads config files into the config +func (gf *GeneralConfig) Load() error { + if strings.TrimSpace(gf.Name) == "" || strings.TrimSpace(gf.Path) == "" { + return errors.New("Invalid ConfigFile Name: " + gf.Path + "/" + gf.Name) + } + + // Config files end with .conf + cfgPath := gf.Path + "/" + gf.Name + ".conf" + tomlData, err := ioutil.ReadFile(cfgPath) + if err != nil { + return err + } + if _, err := toml.Decode(string(tomlData), &gf); err != nil { + return err + } + return nil +} + +// Save writes the config to file(s) +func (gf *GeneralConfig) Save() error { + buf := new(bytes.Buffer) + cfgPath := gf.Path + "/" + gf.Name + ".conf" + fmt.Println("Writing Config File: " + cfgPath) + if err := toml.NewEncoder(buf).Encode(gf); err != nil { + return err + } + fmt.Println("Writing Config File: " + buf.String()) + return ioutil.WriteFile(cfgPath, buf.Bytes(), 0644) +} + +// Set sets a key/value pair in gf, if unable to save, revert to old value +// (and return the error) +func (gf *GeneralConfig) Set(k, v string) error { + oldVal := gf.Values[k] + gf.Values[k] = v + if err := gf.Save(); err != nil { + gf.Values[k] = oldVal + return err + } + return nil +} + +// Get gets a key/value pair from gf +func (gf *GeneralConfig) Get(k string) string { + return gf.Values[k] +} \ No newline at end of file diff --git a/ext/go-xdg/.bzr/README b/ext/go-xdg/.bzr/README new file mode 100644 index 0000000..f82dc1c --- /dev/null +++ b/ext/go-xdg/.bzr/README @@ -0,0 +1,3 @@ +This is a Bazaar control directory. +Do not change any files in this directory. +See http://bazaar.canonical.com/ for more information about Bazaar. diff --git a/ext/go-xdg/.bzr/branch-format b/ext/go-xdg/.bzr/branch-format new file mode 100644 index 0000000..9eb09b7 --- /dev/null +++ b/ext/go-xdg/.bzr/branch-format @@ -0,0 +1 @@ +Bazaar-NG meta directory, format 1 diff --git a/ext/go-xdg/.bzr/branch/branch.conf b/ext/go-xdg/.bzr/branch/branch.conf new file mode 100644 index 0000000..8877240 --- /dev/null +++ b/ext/go-xdg/.bzr/branch/branch.conf @@ -0,0 +1 @@ +parent_location = http://bazaar.launchpad.net/~chipaca/go-xdg/trunk/ diff --git a/ext/go-xdg/.bzr/branch/format b/ext/go-xdg/.bzr/branch/format new file mode 100644 index 0000000..dc392f4 --- /dev/null +++ b/ext/go-xdg/.bzr/branch/format @@ -0,0 +1 @@ +Bazaar Branch Format 7 (needs bzr 1.6) diff --git a/ext/go-xdg/.bzr/branch/last-revision b/ext/go-xdg/.bzr/branch/last-revision new file mode 100644 index 0000000..c518c0b --- /dev/null +++ b/ext/go-xdg/.bzr/branch/last-revision @@ -0,0 +1 @@ +10 john.lenton@canonical.com-20140208094800-gubd5md7cro3mtxa diff --git a/ext/go-xdg/.bzr/branch/tags b/ext/go-xdg/.bzr/branch/tags new file mode 100644 index 0000000..e69de29 diff --git a/ext/go-xdg/.bzr/checkout/conflicts b/ext/go-xdg/.bzr/checkout/conflicts new file mode 100644 index 0000000..0dc2d3a --- /dev/null +++ b/ext/go-xdg/.bzr/checkout/conflicts @@ -0,0 +1 @@ +BZR conflict list format 1 diff --git a/ext/go-xdg/.bzr/checkout/dirstate b/ext/go-xdg/.bzr/checkout/dirstate new file mode 100644 index 0000000..0bd7e6b Binary files /dev/null and b/ext/go-xdg/.bzr/checkout/dirstate differ diff --git a/ext/go-xdg/.bzr/checkout/format b/ext/go-xdg/.bzr/checkout/format new file mode 100644 index 0000000..e0261c7 --- /dev/null +++ b/ext/go-xdg/.bzr/checkout/format @@ -0,0 +1 @@ +Bazaar Working Tree Format 6 (bzr 1.14) diff --git a/ext/go-xdg/.bzr/checkout/views b/ext/go-xdg/.bzr/checkout/views new file mode 100644 index 0000000..e69de29 diff --git a/ext/go-xdg/.bzr/repository/format b/ext/go-xdg/.bzr/repository/format new file mode 100644 index 0000000..b200528 --- /dev/null +++ b/ext/go-xdg/.bzr/repository/format @@ -0,0 +1 @@ +Bazaar repository format 2a (needs bzr 1.16 or later) diff --git a/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.cix b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.cix new file mode 100644 index 0000000..9184958 Binary files /dev/null and b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.cix differ diff --git a/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.iix b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.iix new file mode 100644 index 0000000..73f2931 Binary files /dev/null and b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.iix differ diff --git a/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.rix b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.rix new file mode 100644 index 0000000..7593b1b Binary files /dev/null and b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.rix differ diff --git a/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.six b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.six new file mode 100644 index 0000000..2fc2675 --- /dev/null +++ b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.six @@ -0,0 +1,8 @@ +B+Tree Graph Index 2 +node_ref_lists=0 +key_elements=1 +len=10 +row_lengths=1 +x;0 { +.eE3{cGbp 8NtШ/ ޔ?Xw1vtk W[(p +L 犸.х˅ %eL qc AVϐ{Ѕ1Р11up(?a%mc&},2!as!T;ǝ)c1')o[ 3;޿_cLS2F},-Îm f<pTnF_"=|Jvl )Qd~09tH#bct+4RR‰);Dl|unI~? \ No newline at end of file diff --git a/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.tix b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.tix new file mode 100644 index 0000000..bdb632b Binary files /dev/null and b/ext/go-xdg/.bzr/repository/indices/d61ea2acf68934c72101b4363af27ab1.tix differ diff --git a/ext/go-xdg/.bzr/repository/pack-names b/ext/go-xdg/.bzr/repository/pack-names new file mode 100644 index 0000000..46bc0ba Binary files /dev/null and b/ext/go-xdg/.bzr/repository/pack-names differ diff --git a/ext/go-xdg/.bzr/repository/packs/d61ea2acf68934c72101b4363af27ab1.pack b/ext/go-xdg/.bzr/repository/packs/d61ea2acf68934c72101b4363af27ab1.pack new file mode 100644 index 0000000..93e98b0 Binary files /dev/null and b/ext/go-xdg/.bzr/repository/packs/d61ea2acf68934c72101b4363af27ab1.pack differ diff --git a/ext/go-xdg/LICENSE b/ext/go-xdg/LICENSE new file mode 100644 index 0000000..f6fb46d --- /dev/null +++ b/ext/go-xdg/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2014, John R. Lenton. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ext/go-xdg/README.md b/ext/go-xdg/README.md new file mode 100644 index 0000000..b59a263 --- /dev/null +++ b/ext/go-xdg/README.md @@ -0,0 +1,39 @@ +Go, XDG, go! +=========== + +This is `go-xdg`, a little library to help you use the `XDG` +[base directory spec](http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html). + +(There are other `XDG` specs, that might get included in time. Patches welcome.) + +Sample usage +------------ + +Let's say you are writing an app called “frobz”. It has a config file +and a sqlite database. You'd do something like this: + + configFileName, err := xdg.Config.Find("frobz/config.txt") + if err == nil { + // a config file exists! load it... + } + dbFileName, err := xdg.Data.Ensure("frobz/frobz.db") + // now the file and all its directories exist; it's up to you to + // determine if it's empty, etc. + + +Resources +--------- + +Both `Find` and `Ensure` take a `resource` to construct the path they return. + +A resource is usually an application name (or a well-known shared resource +pool name, such as `icons`), followed by a filename. However nothing in the +standard nor in this library limits you to that; you may store e.g. your +application's configuration in just `$XDG_CONFIG_HOME/application.conf` (in +which case the "resource" here would be just `application.conf`), or in a +sub-directory of an application-specific directory. + +License, etc. +------------ + +BSD simplified, © John R. Lenton, blah blah. diff --git a/ext/go-xdg/base_directory.go b/ext/go-xdg/base_directory.go new file mode 100644 index 0000000..fe9cdd0 --- /dev/null +++ b/ext/go-xdg/base_directory.go @@ -0,0 +1,103 @@ +// (c) 2014 John R. Lenton. See LICENSE. + +package xdg + +import ( + "os" + "os/user" + "path/filepath" +) + +// An XDGDir holds configuration for and can be used to access the +// XDG-specified base directories relative to which user-specific files of a +// given type should be stored. +// +// Typically you wouldn't use XDGDir directly, but one of the +// predefined ones which implement the spec: Data, Config and Cache. +type XDGDir struct { + homeEnv string + homeDefault string + dirsEnv string + dirsDefault string +} + +var ( + Data *XDGDir // for data files. + Config *XDGDir // for configuration files. + Cache *XDGDir // for non-essential data files. +) + +func init() { + // do this here to make the docs nicer + Data = &XDGDir{"XDG_DATA_HOME", ".local/share", "XDG_DATA_DIRS", "/usr/local/share:/usr/share"} + Config = &XDGDir{"XDG_CONFIG_HOME", ".config", "XDG_CONFIG_DIRS", "/etc/xdg"} + Cache = &XDGDir{"XDG_CACHE_HOME", ".cache", "", ""} +} + +// Home gets the path to the given user-specific XDG directory, as specified +// (or not) by the user's environment. +func (x *XDGDir) Home() string { + dir := os.Getenv(x.homeEnv) + if dir != "" { + return dir + } + home := os.Getenv("HOME") + if home == "" { + user, err := user.Current() + if err != nil { + panic("unable to determine $HOME") + } + home = user.HomeDir + } + return filepath.Join(home, x.homeDefault) +} + +// Dirs returns the preference-ordered set of base directories to search for +// files of the given type, starting with the user-specific one, as specified +// (or not) by the user's environment. +func (x *XDGDir) Dirs() []string { + dirs := []string{x.Home()} + if x.dirsEnv != "" { + xtra := os.Getenv(x.dirsEnv) + if xtra == "" { + xtra = x.dirsDefault + } + for _, path := range filepath.SplitList(xtra) { + if path != "" { + dirs = append(dirs, path) + } + } + } + return dirs +} + +// Find attempts to find the path suffix in all of the known XDG directories. +// If not found, an error is returned. +func (x *XDGDir) Find(suffix string) (absPath string, err error) { + var firstError error = nil + for _, path := range x.Dirs() { + name := filepath.Join(path, suffix) + _, err = os.Stat(name) + if err == nil { + return name, nil + } else if firstError == nil { + firstError = err + } + } + return "", firstError +} + +// Ensure takes the path suffix given, and ensures that a matching file exists +// in the home XDG directory. If it doesn't exist it is created. If it can't +// be created, or exists but is unreadable, an error is returned. +func (x *XDGDir) Ensure(suffix string) (absPath string, err error) { + absPath = filepath.Join(x.Home(), suffix) + err = os.MkdirAll(filepath.Dir(absPath), 0700) + if err == nil { + f, err := os.OpenFile(absPath, os.O_CREATE, 0600) + if err == nil { + f.Close() + } + } + return +} diff --git a/ext/go-xdg/base_directory_test.go b/ext/go-xdg/base_directory_test.go new file mode 100644 index 0000000..387322c --- /dev/null +++ b/ext/go-xdg/base_directory_test.go @@ -0,0 +1,151 @@ +// (c) 2014 John R. Lenton. See LICENSE. + +package xdg + +import ( + . "launchpad.net/gocheck" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestXDGd(t *testing.T) { TestingT(t) } + +type xdgdSuite struct { + home string + env1 string + val1 string + env2 string + val2 string + dir *XDGDir +} + +var _ = Suite(&xdgdSuite{}) + +func (s *xdgdSuite) SetUpTest(c *C) { + s.home = os.Getenv("HOME") + s.env1 = "go_xdg_one" + s.env2 = "go_xdg_two" + s.val1 = "something" + s.val2 = "one:two:three" + s.dir = &XDGDir{s.env1, s.val1, s.env2, s.val2} +} + +func (s *xdgdSuite) TestHomePrefersEnviron(c *C) { + err := os.Setenv(s.env1, "algo") + c.Assert(err, IsNil) + defer os.Setenv(s.env1, "") + h := s.dir.Home() + c.Check(h, Equals, "algo") +} + +func (s *xdgdSuite) TestHomeUsesDefault(c *C) { + h := s.dir.Home() + c.Check(h, Matches, s.home+".*"+s.val1) +} + +func (s *xdgdSuite) TestDirsPrefersEnviron(c *C) { + err := os.Setenv(s.env1, "cero") + c.Assert(err, IsNil) + defer os.Setenv(s.env1, "") + err = os.Setenv(s.env2, "uno:dos") + c.Assert(err, IsNil) + defer os.Setenv(s.env2, "") + hs := s.dir.Dirs() + c.Check(hs, DeepEquals, []string{"cero", "uno", "dos"}) +} + +func (s *xdgdSuite) TestDirsSkipsEmpty(c *C) { + err := os.Setenv(s.env2, "::") + c.Assert(err, IsNil) + defer os.Setenv(s.env2, "") + hs := s.dir.Dirs() + c.Check(hs, HasLen, 1) +} + +func (s *xdgdSuite) TestDirsUsesDefault(c *C) { + hs := s.dir.Dirs() + c.Assert(hs, HasLen, 4) + c.Check(hs[1:], DeepEquals, strings.Split(s.val2, ":")) + c.Check(hs[0], Matches, s.home+".*"+s.val1) +} + +// now repeat all the tests, but without the HOME environ. +type xdgdNoHomeSuite struct { + xdgdSuite +} + +var _ = Suite(&xdgdNoHomeSuite{}) + +func (s *xdgdNoHomeSuite) SetUpTest(c *C) { + s.xdgdSuite.SetUpTest(c) + os.Setenv("HOME", "") +} + +func (s *xdgdNoHomeSuite) TearDownTest(c *C) { + os.Setenv("HOME", s.home) +} + +// and for these tests, an entirely fake HOME +type xdgdFHSuite struct { + xdgdSuite + real_home string +} + +var _ = Suite(&xdgdFHSuite{}) + +func (s *xdgdFHSuite) SetUpTest(c *C) { + s.real_home = os.Getenv("HOME") + home := c.MkDir() + os.Setenv("HOME", home) + s.xdgdSuite.SetUpTest(c) + s.val2 = c.MkDir() + ":" + c.MkDir() + ":" + c.MkDir() + s.dir = &XDGDir{s.env1, s.val1, s.env2, s.val2} +} + +func (s *xdgdFHSuite) TearDownTest(c *C) { + os.Setenv("HOME", s.real_home) +} + +func (s *xdgdFHSuite) TestFind(c *C) { + vs := strings.Split(s.val2, ":") + res1 := "stuff" + exp1 := filepath.Join(s.home, s.val1, res1) + res2 := "things/that" + exp2 := filepath.Join(vs[1], res2) + res3 := "more" + exp3 := filepath.Join(vs[2], res3) + for _, d := range []string{exp1, exp2, exp3} { + err := os.MkdirAll(d, 0700) + c.Assert(err, IsNil, Commentf(d)) + } + for _, it := range []struct { + res string + exp string + }{{res1, exp1}, {res2, exp2}, {res3, exp3}} { + rv, err := s.dir.Find(it.res) + c.Assert(err, IsNil) + c.Check(rv, Equals, it.exp) + } + _, err := s.dir.Find("missing") + c.Check(err, NotNil) +} + +func (s *xdgdFHSuite) TestEnsureFirst(c *C) { + // creates it if missing + rv1, err := s.dir.Ensure("missing/file") + c.Assert(err, IsNil) + _, err = os.Stat(rv1) + c.Check(err, IsNil) + c.Check(rv1, Matches, s.home+".*"+"missing/file") + // just gets it if existing + rv2, err := s.dir.Ensure("missing/file") + c.Assert(err, IsNil) + c.Check(rv2, Equals, rv1) +} + +func (s *xdgdFHSuite) TestEnsureFirstFailures(c *C) { + _, err := s.dir.Ensure(strings.Repeat("*", 1<<9) + "/" + strings.Repeat("*", 1<<9)) + c.Assert(err, NotNil) +} diff --git a/ext/go-xdg/doc.go b/ext/go-xdg/doc.go new file mode 100644 index 0000000..2868481 --- /dev/null +++ b/ext/go-xdg/doc.go @@ -0,0 +1,7 @@ +// (c) 2014 John R. Lenton. See LICENSE. + +// xdg implements helpers for you to use the XDG spec in your apps. +// +// For now, that's just the base directory spec, +// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html +package xdg