1package fs 2 3import ( 4 "bytes" 5 "context" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/pkg/errors" 12) 13 14var ( 15 errTooManyLinks = errors.New("too many links") 16) 17 18type currentPath struct { 19 path string 20 f os.FileInfo 21 fullPath string 22} 23 24func pathChange(lower, upper *currentPath) (ChangeKind, string) { 25 if lower == nil { 26 if upper == nil { 27 panic("cannot compare nil paths") 28 } 29 return ChangeKindAdd, upper.path 30 } 31 if upper == nil { 32 return ChangeKindDelete, lower.path 33 } 34 // TODO: compare by directory 35 36 switch i := strings.Compare(lower.path, upper.path); { 37 case i < 0: 38 // File in lower that is not in upper 39 return ChangeKindDelete, lower.path 40 case i > 0: 41 // File in upper that is not in lower 42 return ChangeKindAdd, upper.path 43 default: 44 return ChangeKindModify, upper.path 45 } 46} 47 48func sameFile(f1, f2 *currentPath) (bool, error) { 49 if os.SameFile(f1.f, f2.f) { 50 return true, nil 51 } 52 53 equalStat, err := compareSysStat(f1.f.Sys(), f2.f.Sys()) 54 if err != nil || !equalStat { 55 return equalStat, err 56 } 57 58 if eq, err := compareCapabilities(f1.fullPath, f2.fullPath); err != nil || !eq { 59 return eq, err 60 } 61 62 // If not a directory also check size, modtime, and content 63 if !f1.f.IsDir() { 64 if f1.f.Size() != f2.f.Size() { 65 return false, nil 66 } 67 t1 := f1.f.ModTime() 68 t2 := f2.f.ModTime() 69 70 if t1.Unix() != t2.Unix() { 71 return false, nil 72 } 73 74 // If the timestamp may have been truncated in both of the 75 // files, check content of file to determine difference 76 if t1.Nanosecond() == 0 && t2.Nanosecond() == 0 { 77 var eq bool 78 if (f1.f.Mode() & os.ModeSymlink) == os.ModeSymlink { 79 eq, err = compareSymlinkTarget(f1.fullPath, f2.fullPath) 80 } else if f1.f.Size() > 0 { 81 eq, err = compareFileContent(f1.fullPath, f2.fullPath) 82 } 83 if err != nil || !eq { 84 return eq, err 85 } 86 } else if t1.Nanosecond() != t2.Nanosecond() { 87 return false, nil 88 } 89 } 90 91 return true, nil 92} 93 94func compareSymlinkTarget(p1, p2 string) (bool, error) { 95 t1, err := os.Readlink(p1) 96 if err != nil { 97 return false, err 98 } 99 t2, err := os.Readlink(p2) 100 if err != nil { 101 return false, err 102 } 103 return t1 == t2, nil 104} 105 106const compareChuckSize = 32 * 1024 107 108// compareFileContent compares the content of 2 same sized files 109// by comparing each byte. 110func compareFileContent(p1, p2 string) (bool, error) { 111 f1, err := os.Open(p1) 112 if err != nil { 113 return false, err 114 } 115 defer f1.Close() 116 f2, err := os.Open(p2) 117 if err != nil { 118 return false, err 119 } 120 defer f2.Close() 121 122 b1 := make([]byte, compareChuckSize) 123 b2 := make([]byte, compareChuckSize) 124 for { 125 n1, err1 := f1.Read(b1) 126 if err1 != nil && err1 != io.EOF { 127 return false, err1 128 } 129 n2, err2 := f2.Read(b2) 130 if err2 != nil && err2 != io.EOF { 131 return false, err2 132 } 133 if n1 != n2 || !bytes.Equal(b1[:n1], b2[:n2]) { 134 return false, nil 135 } 136 if err1 == io.EOF && err2 == io.EOF { 137 return true, nil 138 } 139 } 140} 141 142func pathWalk(ctx context.Context, root string, pathC chan<- *currentPath) error { 143 return filepath.Walk(root, func(path string, f os.FileInfo, err error) error { 144 if err != nil { 145 return err 146 } 147 148 // Rebase path 149 path, err = filepath.Rel(root, path) 150 if err != nil { 151 return err 152 } 153 154 path = filepath.Join(string(os.PathSeparator), path) 155 156 // Skip root 157 if path == string(os.PathSeparator) { 158 return nil 159 } 160 161 p := ¤tPath{ 162 path: path, 163 f: f, 164 fullPath: filepath.Join(root, path), 165 } 166 167 select { 168 case <-ctx.Done(): 169 return ctx.Err() 170 case pathC <- p: 171 return nil 172 } 173 }) 174} 175 176func nextPath(ctx context.Context, pathC <-chan *currentPath) (*currentPath, error) { 177 select { 178 case <-ctx.Done(): 179 return nil, ctx.Err() 180 case p := <-pathC: 181 return p, nil 182 } 183} 184 185// RootPath joins a path with a root, evaluating and bounding any 186// symlink to the root directory. 187func RootPath(root, path string) (string, error) { 188 if path == "" { 189 return root, nil 190 } 191 var linksWalked int // to protect against cycles 192 for { 193 i := linksWalked 194 newpath, err := walkLinks(root, path, &linksWalked) 195 if err != nil { 196 return "", err 197 } 198 path = newpath 199 if i == linksWalked { 200 newpath = filepath.Join("/", newpath) 201 if path == newpath { 202 return filepath.Join(root, newpath), nil 203 } 204 path = newpath 205 } 206 } 207} 208 209func walkLink(root, path string, linksWalked *int) (newpath string, islink bool, err error) { 210 if *linksWalked > 255 { 211 return "", false, errTooManyLinks 212 } 213 214 path = filepath.Join("/", path) 215 if path == "/" { 216 return path, false, nil 217 } 218 realPath := filepath.Join(root, path) 219 220 fi, err := os.Lstat(realPath) 221 if err != nil { 222 // If path does not yet exist, treat as non-symlink 223 if os.IsNotExist(err) { 224 return path, false, nil 225 } 226 return "", false, err 227 } 228 if fi.Mode()&os.ModeSymlink == 0 { 229 return path, false, nil 230 } 231 newpath, err = os.Readlink(realPath) 232 if err != nil { 233 return "", false, err 234 } 235 if filepath.IsAbs(newpath) && strings.HasPrefix(newpath, root) { 236 newpath = newpath[:len(root)] 237 if !strings.HasPrefix(newpath, "/") { 238 newpath = "/" + newpath 239 } 240 } 241 *linksWalked++ 242 return newpath, true, nil 243} 244 245func walkLinks(root, path string, linksWalked *int) (string, error) { 246 switch dir, file := filepath.Split(path); { 247 case dir == "": 248 newpath, _, err := walkLink(root, file, linksWalked) 249 return newpath, err 250 case file == "": 251 if os.IsPathSeparator(dir[len(dir)-1]) { 252 if dir == "/" { 253 return dir, nil 254 } 255 return walkLinks(root, dir[:len(dir)-1], linksWalked) 256 } 257 newpath, _, err := walkLink(root, dir, linksWalked) 258 return newpath, err 259 default: 260 newdir, err := walkLinks(root, dir, linksWalked) 261 if err != nil { 262 return "", err 263 } 264 newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked) 265 if err != nil { 266 return "", err 267 } 268 if !islink { 269 return newpath, nil 270 } 271 if filepath.IsAbs(newpath) { 272 return newpath, nil 273 } 274 return filepath.Join(newdir, newpath), nil 275 } 276} 277