2014-04-27 22:00:15 +00:00
package main
import (
2016-04-01 11:50:45 +00:00
"bufio"
2017-06-04 09:16:55 +00:00
"context"
2014-04-27 22:00:15 +00:00
"fmt"
2017-02-06 11:43:44 +00:00
"io"
2014-09-23 20:39:12 +00:00
"os"
2015-03-02 13:48:47 +00:00
"path/filepath"
2016-04-16 20:04:29 +00:00
"strings"
"time"
2016-09-17 10:36:05 +00:00
"github.com/spf13/cobra"
2015-04-25 17:20:41 +00:00
2017-07-23 12:21:03 +00:00
"github.com/restic/restic/internal/archiver"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
2017-07-24 15:42:25 +00:00
"github.com/restic/restic/internal/restic"
2016-09-17 10:36:05 +00:00
)
2015-04-25 17:20:41 +00:00
2016-09-17 10:36:05 +00:00
var cmdBackup = & cobra . Command {
Use : "backup [flags] FILE/DIR [FILE/DIR] ..." ,
2017-09-11 16:32:44 +00:00
Short : "Create a new backup of files and/or directories" ,
2016-09-17 10:36:05 +00:00
Long : `
The "backup" command creates a new snapshot and saves the files and directories
given as the arguments .
` ,
2017-08-05 09:54:59 +00:00
PreRun : func ( cmd * cobra . Command , args [ ] string ) {
if backupOptions . Hostname == "" {
hostname , err := os . Hostname ( )
if err != nil {
debug . Log ( "os.Hostname() returned err: %v" , err )
return
}
backupOptions . Hostname = hostname
}
} ,
2017-08-06 19:02:16 +00:00
DisableAutoGenTag : true ,
2016-09-17 10:36:05 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2017-02-06 11:43:44 +00:00
if backupOptions . Stdin && backupOptions . FilesFrom == "-" {
return errors . Fatal ( "cannot use both `--stdin` and `--files-from -`" )
}
2016-09-17 10:36:05 +00:00
if backupOptions . Stdin {
return readBackupFromStdin ( backupOptions , globalOptions , args )
}
2015-04-25 17:20:41 +00:00
2016-09-17 10:36:05 +00:00
return runBackup ( backupOptions , globalOptions , args )
} ,
2015-04-25 17:20:41 +00:00
}
2016-09-17 10:36:05 +00:00
// BackupOptions bundles all options for the backup command.
type BackupOptions struct {
2017-08-19 20:44:18 +00:00
Parent string
Force bool
Excludes [ ] string
ExcludeFiles [ ] string
ExcludeOtherFS bool
2017-09-09 19:24:29 +00:00
ExcludeIfPresent [ ] string
2017-08-19 20:44:18 +00:00
ExcludeCaches bool
Stdin bool
StdinFilename string
Tags [ ] string
Hostname string
FilesFrom string
2017-08-29 16:29:46 +00:00
TimeStamp string
2015-04-25 17:20:41 +00:00
}
2016-09-17 10:36:05 +00:00
var backupOptions BackupOptions
2015-01-04 17:23:00 +00:00
2016-09-17 10:36:05 +00:00
func init ( ) {
cmdRoot . AddCommand ( cmdBackup )
f := cmdBackup . Flags ( )
f . StringVar ( & backupOptions . Parent , "parent" , "" , "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)" )
2017-02-13 15:02:47 +00:00
f . BoolVarP ( & backupOptions . Force , "force" , "f" , false , ` force re-reading the target files/directories (overrides the "parent" flag) ` )
2017-07-07 01:19:06 +00:00
f . StringArrayVarP ( & backupOptions . Excludes , "exclude" , "e" , nil , "exclude a `pattern` (can be specified multiple times)" )
f . StringArrayVar ( & backupOptions . ExcludeFiles , "exclude-file" , nil , "read exclude patterns from a `file` (can be specified multiple times)" )
2017-02-13 15:02:47 +00:00
f . BoolVarP ( & backupOptions . ExcludeOtherFS , "one-file-system" , "x" , false , "exclude other file systems" )
2017-09-09 19:24:29 +00:00
f . StringArrayVar ( & backupOptions . ExcludeIfPresent , "exclude-if-present" , nil , "takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)" )
2017-08-19 20:44:18 +00:00
f . BoolVar ( & backupOptions . ExcludeCaches , "exclude-caches" , false , ` excludes cache directories that are marked with a CACHEDIR.TAG file ` )
2016-09-17 10:36:05 +00:00
f . BoolVar ( & backupOptions . Stdin , "stdin" , false , "read backup from stdin" )
2017-02-08 21:37:02 +00:00
f . StringVar ( & backupOptions . StdinFilename , "stdin-filename" , "stdin" , "file name to use when reading from stdin" )
2017-07-07 01:19:06 +00:00
f . StringArrayVar ( & backupOptions . Tags , "tag" , nil , "add a `tag` for the new snapshot (can be specified multiple times)" )
2017-09-22 12:29:04 +00:00
f . StringVar ( & backupOptions . Hostname , "hostname" , "" , "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag" )
2016-11-03 17:48:07 +00:00
f . StringVar ( & backupOptions . FilesFrom , "files-from" , "" , "read the files to backup from file (can be combined with file args)" )
2017-08-29 16:29:46 +00:00
f . StringVar ( & backupOptions . TimeStamp , "time" , "" , "time of the backup (ex. '2012-11-01 22:08:41') (default: now)" )
2014-12-07 15:30:52 +00:00
}
2016-09-17 10:36:05 +00:00
func newScanProgress ( gopts GlobalOptions ) * restic . Progress {
if gopts . Quiet {
2015-02-21 13:23:49 +00:00
return nil
}
2016-08-25 20:13:47 +00:00
p := restic . NewProgress ( )
2015-02-21 14:32:48 +00:00
p . OnUpdate = func ( s restic . Stat , d time . Duration , ticker bool ) {
2016-10-14 23:54:54 +00:00
if IsProcessBackground ( ) {
return
}
2016-08-25 20:13:47 +00:00
PrintProgress ( "[%s] %d directories, %d files, %s" , formatDuration ( d ) , s . Dirs , s . Files , formatBytes ( s . Bytes ) )
2015-02-21 14:32:48 +00:00
}
2016-10-14 23:54:54 +00:00
2015-02-21 14:32:48 +00:00
p . OnDone = func ( s restic . Stat , d time . Duration , ticker bool ) {
2016-08-25 20:13:47 +00:00
PrintProgress ( "scanned %d directories, %d files in %s\n" , s . Dirs , s . Files , formatDuration ( d ) )
2015-02-21 14:32:48 +00:00
}
return p
}
2016-09-17 10:36:05 +00:00
func newArchiveProgress ( gopts GlobalOptions , todo restic . Stat ) * restic . Progress {
if gopts . Quiet {
2015-02-21 13:23:49 +00:00
return nil
}
2016-08-25 20:13:47 +00:00
archiveProgress := restic . NewProgress ( )
2015-02-21 13:23:49 +00:00
var bps , eta uint64
itemsTodo := todo . Files + todo . Dirs
archiveProgress . OnUpdate = func ( s restic . Stat , d time . Duration , ticker bool ) {
2016-10-14 23:54:54 +00:00
if IsProcessBackground ( ) {
return
}
2015-02-21 13:23:49 +00:00
sec := uint64 ( d / time . Second )
if todo . Bytes > 0 && sec > 0 && ticker {
bps = s . Bytes / sec
2015-03-16 19:20:53 +00:00
if s . Bytes >= todo . Bytes {
eta = 0
} else if bps > 0 {
2015-02-21 13:23:49 +00:00
eta = ( todo . Bytes - s . Bytes ) / bps
}
}
itemsDone := s . Files + s . Dirs
2015-03-16 19:20:53 +00:00
2015-04-26 10:09:14 +00:00
status1 := fmt . Sprintf ( "[%s] %s %s/s %s / %s %d / %d items %d errors " ,
2015-04-24 23:39:32 +00:00
formatDuration ( d ) ,
2015-04-25 17:20:41 +00:00
formatPercent ( s . Bytes , todo . Bytes ) ,
2015-04-24 23:39:32 +00:00
formatBytes ( bps ) ,
formatBytes ( s . Bytes ) , formatBytes ( todo . Bytes ) ,
2015-04-26 01:54:35 +00:00
itemsDone , itemsTodo ,
s . Errors )
2015-04-24 23:39:32 +00:00
status2 := fmt . Sprintf ( "ETA %s " , formatSeconds ( eta ) )
2015-03-16 19:48:29 +00:00
2017-03-06 10:23:00 +00:00
if w := stdoutTerminalWidth ( ) ; w > 0 {
2016-08-22 20:07:10 +00:00
maxlen := w - len ( status2 ) - 1
2016-02-10 16:28:48 +00:00
if maxlen < 4 {
status1 = ""
} else if len ( status1 ) > maxlen {
status1 = status1 [ : maxlen - 4 ]
status1 += "... "
2015-03-16 19:48:29 +00:00
}
}
2016-08-25 20:13:47 +00:00
PrintProgress ( "%s%s" , status1 , status2 )
2015-02-21 13:23:49 +00:00
}
archiveProgress . OnDone = func ( s restic . Stat , d time . Duration , ticker bool ) {
2015-04-25 17:20:41 +00:00
fmt . Printf ( "\nduration: %s, %s\n" , formatDuration ( d ) , formatRate ( todo . Bytes , d ) )
2015-02-21 13:23:49 +00:00
}
return archiveProgress
2015-02-16 22:44:26 +00:00
}
2016-09-17 10:36:05 +00:00
func newArchiveStdinProgress ( gopts GlobalOptions ) * restic . Progress {
if gopts . Quiet {
2016-05-10 19:51:56 +00:00
return nil
}
2016-08-25 20:13:47 +00:00
archiveProgress := restic . NewProgress ( )
2016-05-10 19:51:56 +00:00
var bps uint64
archiveProgress . OnUpdate = func ( s restic . Stat , d time . Duration , ticker bool ) {
2016-10-14 23:54:54 +00:00
if IsProcessBackground ( ) {
return
}
2016-05-10 19:51:56 +00:00
sec := uint64 ( d / time . Second )
if s . Bytes > 0 && sec > 0 && ticker {
bps = s . Bytes / sec
}
status1 := fmt . Sprintf ( "[%s] %s %s/s" , formatDuration ( d ) ,
formatBytes ( s . Bytes ) ,
formatBytes ( bps ) )
2017-03-06 10:23:00 +00:00
if w := stdoutTerminalWidth ( ) ; w > 0 {
2016-05-10 19:51:56 +00:00
maxlen := w - len ( status1 )
if maxlen < 4 {
status1 = ""
} else if len ( status1 ) > maxlen {
status1 = status1 [ : maxlen - 4 ]
status1 += "... "
}
}
2016-09-13 19:01:29 +00:00
PrintProgress ( "%s" , status1 )
2016-05-10 19:51:56 +00:00
}
archiveProgress . OnDone = func ( s restic . Stat , d time . Duration , ticker bool ) {
fmt . Printf ( "\nduration: %s, %s\n" , formatDuration ( d ) , formatRate ( s . Bytes , d ) )
}
return archiveProgress
}
2015-07-06 21:02:16 +00:00
// filterExisting returns a slice of all existing items, or an error if no
// items exist at all.
func filterExisting ( items [ ] string ) ( result [ ] string , err error ) {
for _ , item := range items {
Fix 567 (#570)
* Patch for https://github.com/restic/restic/issues/567
Backup also files on windows with longer pathnames than 255 chars (e.g. from node).
as fd0 says "So, as far as I can see, we need to have custom methods for all functions that accept a path, so that on Windows we can substitute the normal (possibly relative) path used within restic by an (absolute) UNC path, and only then call the underlying functions like os.Stat(), os.Lstat(), os.Open() and so on.
I've already thought about adding a generic abstraction for the file system (so we can mock this easier in tests), and this looks like a good opportunity to build it."
* fixed building tests
* Restructured patches
Add Wrapper for filepath.Walk
* using \\?\ requires absolute pathes to be used.
Now all tests run
* used gofmt on the code
* Restructured Code. No patches dir, integrate the file functions into restic/fs/
There is still an issue, because restic.fs.Open has a different api the os.Open, which returns the result of OpenFile, but takes only a string
* Changed the last os.Open() calls to fs.Open() after extending the File interface
* fixed name-clash of restic.fs and fuse.fs detected by travis
* fixed fmt with gofmt
* c&p failure: removed fixpath() call.
* missing include
* fixed includes in linux variant
* Fix for Linux. Fd() is required on File interface
* done gofmt
2016-08-15 19:59:13 +00:00
_ , err := fs . Lstat ( item )
2016-08-29 17:18:57 +00:00
if err != nil && os . IsNotExist ( errors . Cause ( err ) ) {
2017-09-09 19:12:41 +00:00
Warnf ( "%v does not exist, skipping\n" , item )
2015-07-06 21:02:16 +00:00
continue
}
result = append ( result , item )
}
if len ( result ) == 0 {
2016-09-01 20:17:37 +00:00
return nil , errors . Fatal ( "all target directories/files do not exist" )
2015-07-06 21:02:16 +00:00
}
return
}
2016-09-17 10:36:05 +00:00
func readBackupFromStdin ( opts BackupOptions , gopts GlobalOptions , args [ ] string ) error {
2016-05-10 19:51:56 +00:00
if len ( args ) != 0 {
2017-02-10 18:39:49 +00:00
return errors . Fatal ( "when reading from stdin, no additional files can be specified" )
2016-05-10 19:51:56 +00:00
}
2017-02-08 21:37:02 +00:00
if opts . StdinFilename == "" {
return errors . Fatal ( "filename for backup from stdin must not be empty" )
}
2017-07-24 21:15:31 +00:00
if gopts . password == "" {
2017-02-03 16:06:06 +00:00
return errors . Fatal ( "unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD" )
2017-02-03 14:53:07 +00:00
}
2016-09-17 10:36:05 +00:00
repo , err := OpenRepository ( gopts )
2016-05-10 19:51:56 +00:00
if err != nil {
return err
}
lock , err := lockRepo ( repo )
defer unlockRepo ( lock )
if err != nil {
return err
}
2017-06-04 09:16:55 +00:00
err = repo . LoadIndex ( context . TODO ( ) )
2016-05-10 19:51:56 +00:00
if err != nil {
return err
}
2017-03-02 14:45:35 +00:00
r := & archiver . Reader {
Repository : repo ,
Tags : opts . Tags ,
Hostname : opts . Hostname ,
}
2017-06-04 09:16:55 +00:00
_ , id , err := r . Archive ( context . TODO ( ) , opts . StdinFilename , os . Stdin , newArchiveStdinProgress ( gopts ) )
2016-05-10 19:51:56 +00:00
if err != nil {
return err
}
2016-12-03 09:25:44 +00:00
Verbosef ( "archived as %v\n" , id . Str ( ) )
2016-05-10 19:51:56 +00:00
return nil
}
2016-11-03 17:48:07 +00:00
// readFromFile will read all lines from the given filename and write them to a
// string array, if filename is empty readFromFile returns and empty string
2017-02-06 11:43:44 +00:00
// array. If filename is a dash (-), readFromFile will read the lines from
// the standard input.
2016-11-03 17:48:07 +00:00
func readLinesFromFile ( filename string ) ( [ ] string , error ) {
if filename == "" {
return nil , nil
}
2017-02-06 11:43:44 +00:00
var r io . Reader = os . Stdin
if filename != "-" {
f , err := os . Open ( filename )
if err != nil {
return nil , err
}
defer f . Close ( )
r = f
2016-11-03 17:48:07 +00:00
}
var lines [ ] string
2017-02-06 11:43:44 +00:00
scanner := bufio . NewScanner ( r )
2016-11-03 17:48:07 +00:00
for scanner . Scan ( ) {
2017-02-27 18:42:00 +00:00
line := scanner . Text ( )
2017-10-19 14:48:22 +00:00
// ignore empty lines
2017-02-27 18:42:00 +00:00
if line == "" {
continue
}
2017-10-19 14:52:06 +00:00
// strip comments
if strings . HasPrefix ( line , "#" ) {
2017-10-19 14:48:22 +00:00
continue
}
2017-02-27 18:42:00 +00:00
lines = append ( lines , line )
2016-11-03 17:48:07 +00:00
}
if err := scanner . Err ( ) ; err != nil {
return nil , err
}
return lines , nil
}
2016-09-17 10:36:05 +00:00
func runBackup ( opts BackupOptions , gopts GlobalOptions , args [ ] string ) error {
2017-07-24 21:15:31 +00:00
if opts . FilesFrom == "-" && gopts . password == "" {
return errors . Fatal ( "unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD" )
2017-02-06 11:43:44 +00:00
}
2016-12-18 22:20:08 +00:00
fromfile , err := readLinesFromFile ( opts . FilesFrom )
2016-11-03 17:48:07 +00:00
if err != nil {
return err
}
// merge files from files-from into normal args so we can reuse the normal
// args checks and have the ability to use both files-from and args at the
// same time
2016-12-18 22:20:08 +00:00
args = append ( args , fromfile ... )
2015-03-02 13:48:47 +00:00
if len ( args ) == 0 {
2017-09-10 13:14:11 +00:00
return errors . Fatal ( "nothing to backup, please specify target files/dirs" )
2014-12-07 15:30:52 +00:00
}
2016-12-18 22:20:08 +00:00
target := make ( [ ] string , 0 , len ( args ) )
2015-03-02 13:48:47 +00:00
for _ , d := range args {
if a , err := filepath . Abs ( d ) ; err == nil {
d = a
}
target = append ( target , d )
}
2016-11-03 17:48:07 +00:00
target , err = filterExisting ( target )
2015-07-06 21:02:16 +00:00
if err != nil {
return err
}
2017-09-10 13:13:40 +00:00
// rejectFuncs collect functions that can reject items from the backup
var rejectFuncs [ ] RejectFunc
2016-09-18 15:10:33 +00:00
// allowed devices
2016-09-17 10:36:05 +00:00
if opts . ExcludeOtherFS {
2017-09-10 13:13:40 +00:00
f , err := rejectByDevice ( target )
if err != nil {
return err
}
rejectFuncs = append ( rejectFuncs , f )
}
// add patterns from file
if len ( opts . ExcludeFiles ) > 0 {
opts . Excludes = append ( opts . Excludes , readExcludePatternsFromFiles ( opts . ExcludeFiles ) ... )
}
if len ( opts . Excludes ) > 0 {
rejectFuncs = append ( rejectFuncs , rejectByPattern ( opts . Excludes ) )
}
if opts . ExcludeCaches {
opts . ExcludeIfPresent = append ( opts . ExcludeIfPresent , "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55" )
}
for _ , spec := range opts . ExcludeIfPresent {
2017-11-27 16:30:53 +00:00
f , err := rejectIfPresent ( spec )
2016-09-18 15:10:33 +00:00
if err != nil {
return err
}
2017-09-10 13:13:40 +00:00
rejectFuncs = append ( rejectFuncs , f )
2016-09-18 15:10:33 +00:00
}
2016-09-17 10:36:05 +00:00
repo , err := OpenRepository ( gopts )
2014-12-07 15:30:52 +00:00
if err != nil {
return err
2014-04-27 22:00:15 +00:00
}
2015-06-27 13:05:20 +00:00
lock , err := lockRepo ( repo )
defer unlockRepo ( lock )
2015-06-27 12:40:18 +00:00
if err != nil {
return err
}
2017-09-11 19:37:10 +00:00
// exclude restic cache
if repo . Cache != nil {
f , err := rejectResticCache ( repo )
if err != nil {
return err
}
rejectFuncs = append ( rejectFuncs , f )
}
2017-06-04 09:16:55 +00:00
err = repo . LoadIndex ( context . TODO ( ) )
2015-04-26 15:44:38 +00:00
if err != nil {
return err
}
2016-09-01 14:04:29 +00:00
var parentSnapshotID * restic . ID
2014-11-30 21:34:21 +00:00
2015-04-03 19:18:09 +00:00
// Force using a parent
2016-09-17 10:36:05 +00:00
if ! opts . Force && opts . Parent != "" {
id , err := restic . FindSnapshot ( repo , opts . Parent )
2014-11-30 21:34:21 +00:00
if err != nil {
2016-09-17 10:36:05 +00:00
return errors . Fatalf ( "invalid id %q: %v" , opts . Parent , err )
2014-11-30 21:34:21 +00:00
}
2015-07-25 15:05:45 +00:00
parentSnapshotID = & id
2014-11-30 21:34:21 +00:00
}
2014-04-27 22:00:15 +00:00
2015-04-03 19:18:09 +00:00
// Find last snapshot to set it as parent, if not already set
2016-09-17 10:36:05 +00:00
if ! opts . Force && parentSnapshotID == nil {
2017-10-05 18:48:26 +00:00
id , err := restic . FindLatestSnapshot ( context . TODO ( ) , repo , target , [ ] restic . TagList { } , opts . Hostname )
2015-07-25 15:05:45 +00:00
if err == nil {
parentSnapshotID = & id
2016-04-27 16:36:48 +00:00
} else if err != restic . ErrNoSnapshotFound {
2015-07-25 15:05:45 +00:00
return err
2015-04-03 19:18:09 +00:00
}
}
2015-07-25 16:09:38 +00:00
if parentSnapshotID != nil {
2016-09-17 10:36:05 +00:00
Verbosef ( "using parent snapshot %v\n" , parentSnapshotID . Str ( ) )
2015-07-25 16:09:38 +00:00
}
2016-09-17 10:36:05 +00:00
Verbosef ( "scan %v\n" , target )
2014-11-16 20:29:11 +00:00
2015-07-19 22:13:39 +00:00
selectFilter := func ( item string , fi os . FileInfo ) bool {
2017-09-10 12:34:28 +00:00
for _ , reject := range rejectFuncs {
if reject ( item , fi ) {
2017-09-09 19:24:29 +00:00
return false
}
2017-08-19 20:44:18 +00:00
}
2017-09-10 13:13:40 +00:00
return true
2015-07-19 22:13:39 +00:00
}
2014-11-16 20:29:11 +00:00
2016-09-17 10:36:05 +00:00
stat , err := archiver . Scan ( target , selectFilter , newScanProgress ( gopts ) )
2015-07-19 22:13:39 +00:00
if err != nil {
return err
}
2014-11-16 20:29:11 +00:00
2016-09-01 14:04:29 +00:00
arch := archiver . New ( repo )
2016-09-17 10:36:05 +00:00
arch . Excludes = opts . Excludes
2015-07-19 22:13:39 +00:00
arch . SelectFilter = selectFilter
2014-11-23 11:05:43 +00:00
2016-12-10 16:14:13 +00:00
arch . Warn = func ( dir string , fi os . FileInfo , err error ) {
2015-01-04 17:23:00 +00:00
// TODO: make ignoring errors configurable
2016-12-10 16:14:13 +00:00
Warnf ( "%s\rwarning for %s: %v\n" , ClearLine ( ) , dir , err )
2014-11-16 20:29:11 +00:00
}
2017-08-29 16:29:46 +00:00
timeStamp := time . Now ( )
if opts . TimeStamp != "" {
timeStamp , err = time . Parse ( TimeFormat , opts . TimeStamp )
if err != nil {
return errors . Fatalf ( "error in time option: %v\n" , err )
}
}
_ , id , err := arch . Snapshot ( context . TODO ( ) , newArchiveProgress ( gopts , stat ) , target , opts . Tags , opts . Hostname , parentSnapshotID , timeStamp )
2014-11-16 20:29:11 +00:00
if err != nil {
2015-02-03 21:05:46 +00:00
return err
2014-11-16 20:29:11 +00:00
}
2016-09-17 10:36:05 +00:00
Verbosef ( "snapshot %s saved\n" , id . Str ( ) )
2014-04-27 22:00:15 +00:00
return nil
}
2017-08-01 17:20:09 +00:00
func readExcludePatternsFromFiles ( excludeFiles [ ] string ) [ ] string {
var excludes [ ] string
for _ , filename := range excludeFiles {
err := func ( ) ( err error ) {
file , err := fs . Open ( filename )
if err != nil {
return err
}
defer func ( ) {
// return pre-close error if there was one
if errClose := file . Close ( ) ; err == nil {
err = errClose
}
} ( )
scanner := bufio . NewScanner ( file )
for scanner . Scan ( ) {
line := strings . TrimSpace ( scanner . Text ( ) )
// ignore empty lines
if line == "" {
continue
}
// strip comments
if strings . HasPrefix ( line , "#" ) {
continue
}
line = os . ExpandEnv ( line )
excludes = append ( excludes , line )
}
return scanner . Err ( )
} ( )
if err != nil {
Warnf ( "error reading exclude patterns: %v:" , err )
return nil
}
}
return excludes
}