├── .gitignore ├── Makefile ├── README.md ├── UNLICENSE ├── deck.go ├── deckrc.example ├── fobject.go ├── funcs.go ├── logger.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /deck 2 | *.swp 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell perl -wnE 'say $$1 if /Version\s=\s"(.*?)\"/' main.go) 2 | 3 | deck: 4 | go build -ldflags "-linkmode external -extldflags -static" 5 | dist: deck 6 | strip deck 7 | mv deck deck-v$(VERSION)-$$(uname | tr [A-Z] [a-z])-$$(uname -m)-static 8 | xz deck-v$(VERSION)-$$(uname | tr [A-Z] [a-z])-$$(uname -m)-static 9 | clean: 10 | rm -f deck 11 | rm -f deck-v* 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deck - hands-off package manager 2 | 3 | There is no shortage of traditional package management tools for linux. Every distro has 4 | a different one - rpm, apt, dpkg, pacman, yum, emerge etc. They all take a pre-built 5 | package archive, expand it on top of your root fs and record the files they just added 6 | to a database. When you remove a package, the package manager looks up the filenames in 7 | the database and removes them. The creation of the initial package archive is hidden from 8 | the enduser and handled by the distros package maintainers or build scripts in the ports 9 | system. 10 | 11 | But what if all you've got is a source tarrball and a makefile, like when you build 12 | Linux From Scratch or work on a custom distribution? You can build and install it 13 | with `make install`, but usually there is no `make uninstall`. The LFS project 14 | privides some hints on how to [add package management to an LFS system](http://www.linuxfromscratch.org/lfs/view/development/chapter06/pkgmgt.html), 15 | but all of the methods require a lot of extra work and are not 100% proof. 16 | 17 | This small project tries to address this problem: 18 | 19 | * install a package with `./configure --prefix=/usr && make && make install` 20 | * run `deck scan` to see what files have been installed, modified or deleted 21 | * run `deck commit` to remember the changes or `deck reset` to discard 22 | * run `deck uninstall` to remove a previously installed package 23 | 24 | ## how it works 25 | 26 | `deck` was built with two assumptions: 27 | 28 | 1. modern hardware is fast: you can walk the filesystem and calculate a hash for 29 | every file in reasonable time. On my old _Sandy Bridge Core i5_ laptop with an 30 | oldish _SSD_ drive hashing the full system (~4gb) takes about a minute. 31 | 2. storage is cheap. you can keep a backup copy of every file and restore it if 32 | it was modified 33 | 34 | `deck` keeps a record of the file's metadata and a copy of the contents for every file 35 | you add to the database. Then when you scan the filesystem for changes it compares it 36 | with the previously recorded state and reports any files that have been added, modified 37 | or deleted. 38 | 39 | `deck` is a written in *Go* and *statically linked*. There are no external dependencies, 40 | not even libc. You can drop it anywhere in your *$PATH* and start using right away. 41 | 42 | ## example session 43 | 44 | root@warthog ~ # tar xf src/rfkill-0.5.tar.xz 45 | root@warthog ~/rfkill-0.5 # make 46 | CC rfkill.o 47 | GEN version.c 48 | CC version.o 49 | CC rfkill 50 | root@warthog ~/rfkill-0.5 # make PREFIX=/usr install 51 | GZIP rfkill.8 52 | INST rfkill 53 | INST rfkill.8 54 | root@warthog ~/rfkill-0.5 # cd .. 55 | root@warthog ~ # deck scan 56 | New files : 57 | 58 | /usr/bin/rfkill 59 | /usr/share/man/man8/rfkill.8.gz 60 | 61 | root@warthog ~ # deck pick /usr/bin/rfkill /usr/share/man/man8/rfkill.8.gz 62 | root@warthog ~ # deck commit -p rfkill -v 0.5 63 | root@warthog ~ # deck show rfkill 64 | /usr/bin/rfkill 65 | /usr/share/man/man8/rfkill.8.gz 66 | root@warthog ~/rfkill-0.5 # deck uninstall rfkill 67 | rm /usr/bin/rfkill 68 | rm /usr/share/man/man8/rfkill.8.gz 69 | root@warthog ~ # 70 | 71 | # config file 72 | 73 | deck looks for the config file in `/etc/deckrc` or `${HOME}/.deckrc`. Config file location can be 74 | overriden with the global `--config` option 75 | 76 | * `root` is the top directory for filesystem scans 77 | * `data` is the directory where it keeps its database and a copy of the tracked files 78 | * `prune` is a list of regular expressions. If a directory name matches one of the 79 | expressions, deck will skip it 80 | * `ignore` - same as prune but for files 81 | * `git` - if there is a git repsitory at the specified `root` and `git` is set to `true`, 82 | deck will ignore all files tracked by git 83 | 84 | there is an example config to start with in `deckrc.example` 85 | 86 | # installation 87 | 88 | A prebuilt static binary can be obtained in the [releases section](https://github.com/pampa/deck/releases) on github. 89 | Download the most recent release, unpack it with xz -d, set the executable bit and put it in your *$PATH* 90 | 91 | ## build from source 92 | 93 | To build from source you will need a working copy of the [Go compiler](https://golang.org/doc/install) 94 | The following instructions will build a statically linked binary: 95 | 96 | ~ $ git clone https://github.com/pampa/deck.git 97 | ~ $ cd deck 98 | ~/deck $ go get -v -d 99 | ~/deck $ make deck 100 | 101 | You can also use the `go get` method, but the resulting binary will be dynamically linked against glibc 102 | 103 | go get -v github.com/pampa/deck 104 | 105 | ## cross compile for a different platform 106 | 107 | Deck can be easily cross compiled for a different platform and architecture 108 | 109 | ~ $ git clone https://github.com/pampa/deck.git 110 | ~ $ cd deck 111 | ~/deck $ go get -v -d 112 | ~/deck $ GOOS=linux GOARCH=arm go build -v 113 | 114 | This will build a statically linked binary for the target platform. For a list of supported target platforms see https://golang.org/doc/install/source#environment 115 | 116 | # usage 117 | 118 | deck [global options] command [command options] [arguments...] 119 | 120 | ## commands 121 | 122 | scan, s scan the filesystem for changes 123 | pick, p pick file for further processing 124 | unpick, u unpick file 125 | commit commit picked files to index, adding package and version tags 126 | list, l list all packages in index 127 | show, o show package contents 128 | remove, rm remove file from index 129 | reset reset file to its previous state 130 | uninstall uninstall package 131 | which, w, who, what show which package a file belongs to 132 | doctor, doc, d run database sanity checks 133 | help, h Shows a list of commands or help for one command 134 | 135 | ## global options 136 | 137 | --config value, -c value config file to use instead of /etc/deckrc or $HOME/.deckrc 138 | --debug, -d print debug info to stderr 139 | --help, -h show help 140 | --version, -v print the version 141 | 142 | ## deck scan - scan the filesystem for changes 143 | 144 | deck scan [command options] [arguments...] 145 | 146 | --hash, -s use sha1 to compare files 147 | --pick, -p pick new files 148 | 149 | ## deck pick - pick file for further processing 150 | 151 | deck pick [arguments...] 152 | 153 | ## deck commit - commit picked files to index, adding package and version tags 154 | 155 | deck commit [command options] [arguments...] 156 | 157 | --package value, -p value package name 158 | --version value, -v value package version 159 | 160 | ## deck list - list all packages in index 161 | 162 | deck list [command options] [arguments...] 163 | 164 | --version, -v do not print version number 165 | 166 | ## deck show - show package contents 167 | 168 | deck show [arguments...] 169 | 170 | --all show all tracked files 171 | 172 | ## deck remove - remove file from index 173 | 174 | deck remove [arguments...] 175 | 176 | ## deck reset - reset file to its previous state 177 | 178 | deck reset [arguments...] 179 | 180 | ## deck uninstall - uninstall package 181 | 182 | deck uninstall [arguments...] 183 | 184 | ## deck which - show which package a file belongs to 185 | 186 | deck which [arguments...] 187 | 188 | ## deck doctor - run database sanity checks 189 | 190 | deck doctor [arguments...] 191 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /deck.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/BurntSushi/toml" 6 | "github.com/boltdb/bolt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "regexp" 11 | "sort" 12 | "strings" 13 | ) 14 | 15 | type Deck struct { 16 | Root string 17 | Data string 18 | Prune []string 19 | Ignore []string 20 | pruneRe []*regexp.Regexp 21 | ignoreRe []*regexp.Regexp 22 | Git bool 23 | gitFiles map[string]bool 24 | db *bolt.DB 25 | } 26 | 27 | var picks = []byte("picks") 28 | var index = []byte("index") 29 | 30 | var deck Deck 31 | 32 | func (d *Deck) Init(f string) { 33 | if _, err := toml.DecodeFile(f, &d); err != nil { 34 | log.Error(f, ":", err) 35 | } 36 | log.Debug("Root", d.Root) 37 | log.Debug("Data", d.Data) 38 | log.Debug("Prune", d.Prune) 39 | log.Debug("Ignore", d.Ignore) 40 | log.Debug("Git", d.Git) 41 | 42 | for _, s := range d.Prune { 43 | r, err := regexp.Compile(s) 44 | if err != nil { 45 | log.Error(err, s) 46 | } 47 | d.pruneRe = append(d.pruneRe, r) 48 | } 49 | 50 | for _, s := range d.Ignore { 51 | r, err := regexp.Compile(s) 52 | if err != nil { 53 | log.Error(err, s) 54 | } 55 | d.ignoreRe = append(d.ignoreRe, r) 56 | } 57 | 58 | if _, err := os.Stat(d.Root); err != nil { 59 | log.Error(err) 60 | } 61 | 62 | if err := os.MkdirAll(d.Data, 0744); err != nil { 63 | log.Error(err) 64 | } 65 | 66 | var err error 67 | if d.db, err = bolt.Open(d.Data+"/deck.db", 0644, nil); err != nil { 68 | log.Error(err) 69 | } 70 | 71 | if err := d.db.Update(func(tx *bolt.Tx) error { 72 | if _, err := tx.CreateBucketIfNotExists(index); err != nil { 73 | return err 74 | } 75 | if _, err := tx.CreateBucketIfNotExists(picks); err != nil { 76 | return err 77 | } 78 | return nil 79 | }); err != nil { 80 | log.Error(err) 81 | } 82 | 83 | if d.Git { 84 | git, err := exec.LookPath("git") 85 | if err != nil { 86 | log.Error(err) 87 | } 88 | cmd := exec.Command(git, "ls-tree", "-r", "HEAD", "--name-only") 89 | cmd.Dir = d.Root 90 | 91 | out, err := cmd.Output() 92 | if err != nil { 93 | fmt.Println(string(out)) 94 | log.Error(err) 95 | } 96 | a_out := strings.Split(strings.TrimSpace(string(out)), "\n") 97 | d.gitFiles = make(map[string]bool) 98 | for _, v := range a_out { 99 | d.gitFiles[d.Root+v] = true 100 | } 101 | } 102 | } 103 | 104 | func (d *Deck) Close() { 105 | d.db.Close() 106 | } 107 | 108 | func (d *Deck) fsWalk(hash bool) ([]string, []string, []string, []string) { 109 | var newFiles []string 110 | var pickedFiles []string 111 | var modifiedFiles []string 112 | var missingFiles []string 113 | 114 | if err := d.db.View(func(tx *bolt.Tx) error { 115 | bkPicks := tx.Bucket(picks) 116 | bkIndex := tx.Bucket(index) 117 | filepath.Walk(d.Root, func(p string, i os.FileInfo, _ error) error { 118 | if i.IsDir() && matchAny(p, d.pruneRe) { 119 | log.Debug("Prune", p) 120 | return filepath.SkipDir 121 | } else if matchAny(p, d.ignoreRe) { 122 | log.Debug("Ignore", p) 123 | return nil 124 | } else if d.ignoreGit(p) { 125 | log.Debug("Skip git", p) 126 | return nil 127 | } else { 128 | if i.Mode().IsRegular() || (i.Mode()&os.ModeSymlink != 0) { 129 | pk := bkPicks.Get([]byte(p)) 130 | kn := bkIndex.Get([]byte(p)) 131 | if pk != nil { 132 | pickedFiles = append(pickedFiles, p) 133 | } else if kn != nil { 134 | //log.Debug("Know", p) 135 | fs := getFileObject(p, hash) 136 | fk := readFileObject(kn) 137 | if err := fs.IsDifferent(fk, hash); err != nil { 138 | log.Debug(err, p) 139 | modifiedFiles = append(modifiedFiles, p) 140 | } 141 | } else { 142 | newFiles = append(newFiles, p) 143 | } 144 | } else { 145 | //log.Debug("Skip", p) 146 | } 147 | return nil 148 | } 149 | }) 150 | bkIndex.ForEach(func(k, v []byte) error { 151 | _, err := os.Lstat(string(k)) 152 | if os.IsNotExist(err) { 153 | missingFiles = append(missingFiles, string(k)) 154 | } 155 | return nil 156 | }) 157 | return nil 158 | }); err != nil { 159 | log.Error(err) 160 | } 161 | 162 | return newFiles, pickedFiles, modifiedFiles, missingFiles 163 | } 164 | 165 | func (d *Deck) Scan(hash bool, pick bool) { 166 | 167 | newFiles, pickedFiles, modifiedFiles, missingFiles := d.fsWalk(hash) 168 | 169 | if pick { 170 | deck.Pick(newFiles) 171 | deck.Pick(modifiedFiles) 172 | pickedFiles = append(pickedFiles, newFiles...) 173 | pickedFiles = append(pickedFiles, modifiedFiles...) 174 | newFiles = nil 175 | modifiedFiles = nil 176 | } 177 | 178 | printFiles("New files", newFiles) 179 | printFiles("Missing files", missingFiles) 180 | printFiles("Modified files", modifiedFiles) 181 | printFiles("Picked files", pickedFiles) 182 | } 183 | 184 | func (d *Deck) Pick(files []string) { 185 | d.db.Update(func(tx *bolt.Tx) error { 186 | bkPicks := tx.Bucket(picks) 187 | for _, f := range files { 188 | s, err := os.Lstat(f) 189 | if os.IsNotExist(err) { 190 | log.Error(err) 191 | } 192 | if s.Mode().IsRegular() || (s.Mode()&os.ModeSymlink != 0) { 193 | log.Debug("pick", f) 194 | if err := bkPicks.Put([]byte(f), nil); err != nil { 195 | return err 196 | } 197 | } else { 198 | log.Error("Only regular files and symlinks allowed", f) 199 | } 200 | } 201 | return nil 202 | }) 203 | } 204 | 205 | func (d *Deck) Unpick(all bool, files []string) { 206 | d.db.Update(func(tx *bolt.Tx) error { 207 | bkPicks := tx.Bucket(picks) 208 | 209 | if all { 210 | bkPicks.ForEach(func(k, v []byte) error { 211 | log.Debug("unpick", string(k)) 212 | if err := bkPicks.Delete(k); err != nil { 213 | return err 214 | } 215 | return nil 216 | }) 217 | } else { 218 | for _, f := range files { 219 | log.Debug("unpick", f) 220 | if err := bkPicks.Delete([]byte(f)); err != nil { 221 | return err 222 | } 223 | } 224 | } 225 | return nil 226 | }) 227 | } 228 | 229 | func (d *Deck) Remove(files []string) { 230 | d.db.Update(func(tx *bolt.Tx) error { 231 | bkIndex := tx.Bucket(index) 232 | for _, f := range files { 233 | log.Debug("remove", f) 234 | if err := bkIndex.Delete([]byte(f)); err != nil { 235 | return err 236 | } 237 | } 238 | return nil 239 | }) 240 | } 241 | 242 | func (d *Deck) Reset(files []string) { 243 | d.db.View(func(tx *bolt.Tx) error { 244 | bkIndex := tx.Bucket(index) 245 | for _, f := range files { 246 | kn := bkIndex.Get([]byte(f)) 247 | if kn == nil { 248 | log.Error("File not in index", f) 249 | } 250 | fo := readFileObject(kn) 251 | fo.Reset(f) 252 | } 253 | return nil 254 | }) 255 | } 256 | 257 | func (d *Deck) Commit(pak string, ver string) { 258 | d.db.Update(func(tx *bolt.Tx) error { 259 | bkPicks := tx.Bucket(picks) 260 | bkIndex := tx.Bucket(index) 261 | 262 | bkPicks.ForEach(func(k, v []byte) error { 263 | log.Debug("commit", pak, ver, string(k)) 264 | fo := getFileObject(string(k), true) 265 | fo.Package = Package{ 266 | Name: pak, 267 | Version: ver, 268 | } 269 | 270 | if err := bkIndex.Put(k, fo.ToBytes()); err != nil { 271 | log.Error(err) 272 | } 273 | 274 | fo.Stov(string(k)) 275 | 276 | if err := bkPicks.Delete(k); err != nil { 277 | log.Error(err) 278 | } 279 | 280 | return nil 281 | }) 282 | return nil 283 | }) 284 | } 285 | 286 | func appendPackage(s []Package, n Package) []Package { 287 | for _, i := range s { 288 | if i == n { 289 | return s 290 | } 291 | } 292 | return append(s, n) 293 | } 294 | 295 | func (d *Deck) Packages() []Package { 296 | var packages []Package 297 | d.db.View(func(tx *bolt.Tx) error { 298 | bkIndex := tx.Bucket(index) 299 | bkIndex.ForEach(func(k, v []byte) error { 300 | fo := readFileObject(v) 301 | packages = appendPackage(packages, fo.Package) 302 | return nil 303 | }) 304 | return nil 305 | }) 306 | sort.Sort(ByName(packages)) 307 | return packages 308 | } 309 | 310 | func (d *Deck) ignoreGit(k string) bool { 311 | if d.Git { 312 | if _, ok := d.gitFiles[k]; ok == true { 313 | return true 314 | } else { 315 | return false 316 | } 317 | } else { 318 | return false 319 | } 320 | } 321 | 322 | func (d *Deck) Doctor() { 323 | d.db.View(func(tx *bolt.Tx) error { 324 | bkIndex := tx.Bucket(index) 325 | bkIndex.ForEach(func(k, v []byte) error { 326 | if matchAny(string(k), d.ignoreRe) { 327 | fmt.Println("Ignored file in index", string(k)) 328 | } 329 | if d.ignoreGit(string(k)) { 330 | fmt.Println("Git tracked file in index", string(k)) 331 | } 332 | return nil 333 | }) 334 | return nil 335 | }) 336 | } 337 | 338 | func (d *Deck) Show(pak string, all bool) { 339 | d.db.View(func(tx *bolt.Tx) error { 340 | bkIndex := tx.Bucket(index) 341 | bkIndex.ForEach(func(k, v []byte) error { 342 | if all { 343 | fmt.Println(string(k)) 344 | } else { 345 | fo := readFileObject(v) 346 | if fo.Package.Name == pak { 347 | fmt.Println(string(k)) 348 | } 349 | } 350 | return nil 351 | }) 352 | return nil 353 | }) 354 | } 355 | 356 | func (d *Deck) Uninstall(pak string) { 357 | d.db.Update(func(tx *bolt.Tx) error { 358 | bkIndex := tx.Bucket(index) 359 | bkIndex.ForEach(func(k, v []byte) error { 360 | fo := readFileObject(v) 361 | if fo.Package.Name == pak { 362 | fmt.Println("rm", string(k)) 363 | if err := os.Remove(string(k)); err != nil { 364 | log.Error(err) 365 | } 366 | if err := bkIndex.Delete(k); err != nil { 367 | return err 368 | } 369 | } 370 | return nil 371 | }) 372 | return nil 373 | }) 374 | } 375 | 376 | func (d *Deck) Which(files []string) { 377 | d.db.View(func(tx *bolt.Tx) error { 378 | bkIndex := tx.Bucket(index) 379 | for _, f := range files { 380 | kn := bkIndex.Get([]byte(f)) 381 | if kn != nil { 382 | fo := readFileObject(kn) 383 | fmt.Println(fo.Package.Name, fo.Package.Version, f) 384 | } 385 | } 386 | return nil 387 | }) 388 | } 389 | 390 | func (d *Deck) List(ver bool) { 391 | for _, p := range d.Packages() { 392 | if ver { 393 | fmt.Println(p.Name) 394 | } else { 395 | fmt.Println(p.Name, p.Version) 396 | } 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /deckrc.example: -------------------------------------------------------------------------------- 1 | root = "/" 2 | data = "/.deck" 3 | git = true 4 | prune = [ 5 | "^/\\.git$", 6 | "^/dist$", 7 | "^/boot/grub$", 8 | "^/proc$", 9 | "^/dev$", 10 | "^/mnt$", 11 | "^/sys$", 12 | "^/src$", 13 | "^/root$", 14 | "^/home$", 15 | "^/build$", 16 | "^/tools$", 17 | "/lost\\+found$", 18 | ] 19 | ignore = [ 20 | "^/etc/passwd$", 21 | "^/etc/passwd-$", 22 | "^/etc/group$", 23 | "^/etc/group-$", 24 | ] 25 | -------------------------------------------------------------------------------- /fobject.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "encoding/gob" 7 | "encoding/hex" 8 | "errors" 9 | "io" 10 | "os" 11 | ) 12 | 13 | type Package struct { 14 | Name string 15 | Version string 16 | } 17 | 18 | type ByName []Package 19 | 20 | func (a ByName) Len() int { 21 | return len(a) 22 | } 23 | 24 | func (a ByName) Less(i, j int) bool { 25 | return a[i].Name < a[j].Name 26 | } 27 | 28 | func (a ByName) Swap(i, j int) { 29 | a[i], a[j] = a[j], a[i] 30 | } 31 | 32 | type FileObject struct { 33 | FileMode os.FileMode 34 | Size int64 35 | Package Package 36 | Ref string 37 | Sha1 []byte 38 | } 39 | 40 | func (f FileObject) Reset(dst string) { 41 | if f.IsLink() { 42 | _, err := os.Lstat(dst) 43 | if os.IsNotExist(err) { 44 | } else { 45 | if err := os.Remove(dst); err != nil { 46 | log.Error(err) 47 | } 48 | } 49 | if err := os.Symlink(f.Ref, dst); err != nil { 50 | log.Error(err) 51 | } 52 | } else { 53 | f.cp(f.objFile(), dst) 54 | if err := os.Chmod(dst, f.FileMode); err != nil { 55 | log.Error(err) 56 | } 57 | } 58 | } 59 | 60 | func (f FileObject) Stov(src string) { 61 | if !f.IsLink() { 62 | if err := os.MkdirAll(f.objDir(), 0744); err != nil { 63 | log.Error(err) 64 | } 65 | f.cp(src, f.objFile()) 66 | } 67 | } 68 | 69 | func (f FileObject) IsLink() bool { 70 | return f.FileMode&os.ModeSymlink != 0 71 | } 72 | 73 | func (f FileObject) objFile() string { 74 | return f.objDir() + "/" + hex.EncodeToString(f.Sha1) 75 | } 76 | 77 | func (f FileObject) objDir() string { 78 | return deck.Data + "/" + hex.EncodeToString(f.Sha1)[:2] 79 | } 80 | 81 | func (f FileObject) cp(src string, dst string) { 82 | log.Debug("cp", src, dst) 83 | srcFile, err := os.Open(src) 84 | if err != nil { 85 | log.Error(err) 86 | } 87 | dstFile, err := os.Create(dst) 88 | if err != nil { 89 | log.Error(err) 90 | } 91 | _, err = io.Copy(dstFile, srcFile) 92 | if err != nil { 93 | log.Error(err) 94 | } 95 | dstFile.Sync() 96 | srcFile.Close() 97 | dstFile.Close() 98 | } 99 | 100 | func (f FileObject) IsDifferent(fn FileObject, hash bool) error { 101 | if f.FileMode != fn.FileMode { 102 | return errors.New("Mode does not match") 103 | } 104 | if f.IsLink() { 105 | if f.Ref != fn.Ref { 106 | return errors.New("Ref does not match") 107 | } 108 | } else { 109 | if f.Size != fn.Size { 110 | return errors.New("Size does not match") 111 | } 112 | 113 | if hash { 114 | if bytes.Compare(f.Sha1, fn.Sha1) != 0 { 115 | return errors.New("Sha1 does not match") 116 | } 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | func (f FileObject) ToBytes() []byte { 123 | var buf bytes.Buffer 124 | enc := gob.NewEncoder(&buf) 125 | err := enc.Encode(f) 126 | if err != nil { 127 | log.Error(err) 128 | } 129 | return buf.Bytes() 130 | } 131 | 132 | func readFileObject(v []byte) FileObject { 133 | buf := bytes.NewBuffer(v) 134 | var fo FileObject 135 | enc := gob.NewDecoder(buf) 136 | err := enc.Decode(&fo) 137 | if err != nil { 138 | log.Error(err) 139 | } 140 | return fo 141 | } 142 | 143 | func getFileObject(f string, hash bool) FileObject { 144 | fi, err := os.Lstat(f) 145 | if err != nil { 146 | log.Error(err) 147 | } 148 | 149 | fo := FileObject{ 150 | FileMode: fi.Mode(), 151 | Size: fi.Size(), 152 | } 153 | if fo.IsLink() { 154 | fo.Ref, err = os.Readlink(f) 155 | if err != nil { 156 | log.Error(err) 157 | } 158 | } else { 159 | if hash { 160 | h := sha1.New() 161 | fh, err := os.Open(f) 162 | if err != nil { 163 | log.Error(err) 164 | } 165 | io.Copy(h, fh) 166 | fo.Sha1 = h.Sum(nil) 167 | } 168 | } 169 | return fo 170 | } 171 | -------------------------------------------------------------------------------- /funcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | ) 9 | 10 | func matchAny(p string, a []*regexp.Regexp) bool { 11 | for _, r := range a { 12 | if m := r.MatchString(p); m == true { 13 | return true 14 | } 15 | } 16 | return false 17 | } 18 | 19 | func printFiles(cap string, files []string) { 20 | if len(files) > 0 { 21 | fmt.Println(cap, ":\n") 22 | for _, f := range files { 23 | fmt.Println("\t", f) 24 | } 25 | fmt.Println() 26 | } 27 | } 28 | 29 | func getConfigFile(cFlag string) (string, error) { 30 | var cFiles []string 31 | 32 | if cFlag != "" { 33 | cFiles = append([]string{}, cFlag) 34 | } else { 35 | cFiles = append([]string{os.ExpandEnv("${HOME}/.deckrc"), "/etc/deckrc"}) 36 | } 37 | 38 | var cFile string 39 | 40 | for _, f := range cFiles { 41 | log.Debug("try config file " + f) 42 | if finfo, err := os.Stat(f); err == nil { 43 | if !finfo.IsDir() { 44 | log.Debug("using " + f) 45 | cFile = f 46 | break 47 | } 48 | } 49 | } 50 | 51 | if cFile == "" { 52 | return cFile, errors.New("can't find config file") 53 | } else { 54 | return cFile, nil 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | type Logger struct { 9 | Verbose bool 10 | } 11 | 12 | var log = Logger{ 13 | Verbose: false, 14 | } 15 | 16 | func (l *Logger) Debug(v ...interface{}) { 17 | if l.Verbose { 18 | fmt.Print("DEBUG: ") 19 | fmt.Println(v...) 20 | } 21 | } 22 | 23 | func (l *Logger) Error(v ...interface{}) { 24 | fmt.Print("ERROR: ") 25 | fmt.Println(v...) 26 | os.Exit(1) 27 | } 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/urfave/cli" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | app := cli.NewApp() 10 | app.Usage = "hands off package manager" 11 | app.Version = "0.1.0" 12 | app.Flags = []cli.Flag{ 13 | cli.StringFlag{ 14 | Name: "config, c", 15 | Usage: "config file to use instead of /etc/deckrc or $HOME/.deckrc", 16 | }, 17 | cli.BoolFlag{ 18 | Name: "debug, d", 19 | Usage: "print debug info to stderr", 20 | }, 21 | } 22 | 23 | app.Before = func(c *cli.Context) error { 24 | log.Verbose = c.GlobalBool("debug") 25 | if cFile, err := getConfigFile(c.GlobalString("config")); err == nil { 26 | deck.Init(cFile) 27 | } else { 28 | log.Error(err) 29 | } 30 | return nil 31 | } 32 | 33 | app.After = func(c *cli.Context) error { 34 | log.Debug("closing database") 35 | deck.Close() 36 | return nil 37 | } 38 | app.Commands = []cli.Command{ 39 | { 40 | Name: "scan", 41 | Aliases: []string{"s"}, 42 | Usage: "scan the filesystem for changes", 43 | Flags: []cli.Flag{ 44 | cli.BoolFlag{ 45 | Name: "hash, s", 46 | Usage: "use sha1 to compare files", 47 | }, 48 | cli.BoolFlag{ 49 | Name: "pick, p", 50 | Usage: "pick new files", 51 | }, 52 | }, 53 | Action: func(c *cli.Context) { 54 | deck.Scan(c.Bool("hash"), c.Bool("pick")) 55 | }, 56 | }, 57 | { 58 | Name: "pick", 59 | Aliases: []string{"p"}, 60 | Usage: "pick file for further processing", 61 | Action: func(c *cli.Context) { 62 | deck.Pick(c.Args()) 63 | }, 64 | }, 65 | { 66 | Name: "unpick", 67 | Aliases: []string{"u"}, 68 | Usage: "unpick file", 69 | Flags: []cli.Flag{ 70 | cli.BoolFlag{ 71 | Name: "all, a", 72 | Usage: "unpick all files", 73 | }, 74 | }, 75 | Action: func(c *cli.Context) { 76 | deck.Unpick(c.Bool("all"), c.Args()) 77 | }, 78 | }, 79 | { 80 | Name: "commit", 81 | Usage: "commit picked files to index, adding package and version tags", 82 | Flags: []cli.Flag{ 83 | cli.StringFlag{ 84 | Name: "package, p", 85 | Usage: "package name", 86 | }, 87 | cli.StringFlag{ 88 | Name: "version, v", 89 | Usage: "package version", 90 | }, 91 | }, 92 | Action: func(c *cli.Context) { 93 | pak := c.String("package") 94 | ver := c.String("version") 95 | 96 | if pak == "" && ver == "" { 97 | log.Error("--package and --version flags are required") 98 | } else if pak == "" { 99 | log.Error("--package flag is required") 100 | } else if ver == "" { 101 | log.Error("--version flag is required") 102 | } else { 103 | deck.Commit(pak, ver) 104 | } 105 | }, 106 | }, 107 | { 108 | Name: "list", 109 | Aliases: []string{"l"}, 110 | Usage: "list all packages in index", 111 | Flags: []cli.Flag{ 112 | cli.BoolFlag{ 113 | Name: "version, v", 114 | Usage: "do not print version number", 115 | }, 116 | }, 117 | Action: func(c *cli.Context) { 118 | deck.List(c.Bool("version")) 119 | }, 120 | }, 121 | { 122 | Name: "show", 123 | Aliases: []string{"o"}, 124 | Flags: []cli.Flag{ 125 | cli.BoolFlag{ 126 | Name: "all", 127 | Usage: "show all tracked files", 128 | }, 129 | }, 130 | Usage: "show files in package", 131 | Action: func(c *cli.Context) { 132 | if c.Bool("all") && len(c.Args()) > 0 { 133 | log.Error("cant use --all with package name") 134 | } else if c.Bool("all") { 135 | deck.Show("", true) 136 | } else if len(c.Args()) == 0 { 137 | log.Error("show what?") 138 | } else { 139 | deck.Show(c.Args()[0], false) 140 | } 141 | }, 142 | }, 143 | { 144 | Name: "remove", 145 | Aliases: []string{"rm"}, 146 | Usage: "remove file from index", 147 | Action: func(c *cli.Context) { 148 | deck.Remove(c.Args()) 149 | }, 150 | }, 151 | { 152 | Name: "reset", 153 | Usage: "reset file to its previous state", 154 | Action: func(c *cli.Context) { 155 | deck.Reset(c.Args()) 156 | }, 157 | }, 158 | { 159 | Name: "uninstall", 160 | Usage: "uninstall package", 161 | Action: func(c *cli.Context) { 162 | deck.Uninstall(c.Args()[0]) 163 | }, 164 | }, 165 | { 166 | Name: "which", 167 | Aliases: []string{"w", "who", "what"}, 168 | Usage: "show which package a file belongs to", 169 | Action: func(c *cli.Context) { 170 | deck.Which(c.Args()) 171 | }, 172 | }, 173 | { 174 | Name: "doctor", 175 | Aliases: []string{"doc", "d"}, 176 | Usage: "run database sanity checks", 177 | Action: func(c *cli.Context) { 178 | deck.Doctor() 179 | }, 180 | }, 181 | } 182 | app.Run(os.Args) 183 | } 184 | --------------------------------------------------------------------------------