github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/updater/util/file.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package util 5 6 import ( 7 "fmt" 8 "io" 9 "net/url" 10 "os" 11 "path/filepath" 12 "runtime" 13 "strings" 14 "time" 15 ) 16 17 // File uses a safer file API 18 type File struct { 19 name string 20 data []byte 21 perm os.FileMode 22 } 23 24 // SafeWriter defines a writer that is safer (atomic) 25 type SafeWriter interface { 26 GetFilename() string 27 WriteTo(io.Writer) (int64, error) 28 } 29 30 // NewFile returns a File 31 func NewFile(name string, data []byte, perm os.FileMode) File { 32 return File{name, data, perm} 33 } 34 35 // Save file 36 func (f File) Save(log Log) error { 37 return safeWriteToFile(f, f.perm, log) 38 } 39 40 // GetFilename returns the file name for SafeWriter 41 func (f File) GetFilename() string { 42 return f.name 43 } 44 45 // WriteTo is for SafeWriter 46 func (f File) WriteTo(w io.Writer) (int64, error) { 47 n, err := w.Write(f.data) 48 return int64(n), err 49 } 50 51 // safeWriteToFile to safely write to a file 52 func safeWriteToFile(t SafeWriter, mode os.FileMode, log Log) error { 53 filename := t.GetFilename() 54 if filename == "" { 55 return fmt.Errorf("No filename") 56 } 57 log.Debugf("Writing to %s", filename) 58 tempFilename, tempFile, err := openTempFile(filename+"-", "", mode) 59 log.Debugf("Temporary file generated: %s", tempFilename) 60 if err != nil { 61 return err 62 } 63 _, err = t.WriteTo(tempFile) 64 if err != nil { 65 log.Errorf("Error writing temporary file %s: %s", tempFilename, err) 66 _ = tempFile.Close() 67 _ = os.Remove(tempFilename) 68 return err 69 } 70 err = tempFile.Close() 71 if err != nil { 72 log.Errorf("Error closing temporary file %s: %s", tempFilename, err) 73 _ = os.Remove(tempFilename) 74 return err 75 } 76 err = os.Rename(tempFilename, filename) 77 if err != nil { 78 log.Errorf("Error renaming temporary file %s to %s: %s", tempFilename, filename, err) 79 _ = os.Remove(tempFilename) 80 return err 81 } 82 log.Debugf("Wrote to %s", filename) 83 return nil 84 } 85 86 // Close closes a file and ignores the error. 87 // This satisfies lint checks when using with defer and you don't care if there 88 // is an error, so instead of: 89 // 90 // defer func() { _ = f.Close() }() 91 // defer Close(f) 92 func Close(f io.Closer) { 93 if f == nil { 94 return 95 } 96 _ = f.Close() 97 } 98 99 // RemoveFileAtPath removes a file at path (and any children) ignoring any error. 100 // We do nothing if path == "". 101 // This satisfies lint checks when using with defer and you don't care if there 102 // is an error, so instead of: 103 // 104 // defer func() { _ = os.Remove(path) }() 105 // defer RemoveFileAtPath(path) 106 func RemoveFileAtPath(path string) { 107 if path == "" { 108 return 109 } 110 _ = os.RemoveAll(path) 111 } 112 113 // openTempFile creates an opened temporary file. 114 // 115 // openTempFile("foo", ".zip", 0755) => "foo.RCG2KUSCGYOO3PCKNWQHBOXBKACOPIKL.zip" 116 // openTempFile(path.Join(os.TempDir(), "foo"), "", 0600) => "/tmp/foo.RCG2KUSCGYOO3PCKNWQHBOXBKACOPIKL" 117 func openTempFile(prefix string, suffix string, mode os.FileMode) (string, *os.File, error) { 118 filename, err := RandomID(prefix) 119 if err != nil { 120 return "", nil, err 121 } 122 if suffix != "" { 123 filename += suffix 124 } 125 flags := os.O_WRONLY | os.O_CREATE | os.O_EXCL 126 if mode == 0 { 127 mode = 0600 128 } 129 file, err := os.OpenFile(filename, flags, mode) 130 return filename, file, err 131 } 132 133 // FileExists returns whether the given file or directory exists or not 134 func FileExists(path string) (bool, error) { 135 _, err := os.Stat(path) 136 if err == nil { 137 return true, nil 138 } 139 if os.IsNotExist(err) { 140 return false, nil 141 } 142 return false, err 143 } 144 145 // MakeParentDirs ensures parent directory exist for path 146 func MakeParentDirs(path string, mode os.FileMode, log Log) error { 147 // 2nd return value here is filename (not an error), which is not needed 148 dir, _ := filepath.Split(path) 149 if dir == "" { 150 return fmt.Errorf("No base directory") 151 } 152 return MakeDirs(dir, mode, log) 153 } 154 155 // MakeDirs ensures directory exists for path 156 func MakeDirs(dir string, mode os.FileMode, log Log) error { 157 exists, err := FileExists(dir) 158 if err != nil { 159 return err 160 } 161 162 if !exists { 163 log.Debugf("Creating: %s\n", dir) 164 err = os.MkdirAll(dir, mode) 165 if err != nil { 166 return err 167 } 168 } 169 return nil 170 } 171 172 // TempPath returns a temporary unique file path. 173 // If for some reason we can't obtain random data, we still return a valid 174 // path, which may not be as unique. 175 // If tempDir is "", then os.TempDir() is used. 176 func TempPath(tempDir string, prefix string) string { 177 if tempDir == "" { 178 tempDir = os.TempDir() 179 } 180 filename, err := RandomID(prefix) 181 if err != nil { 182 // We had an error getting random bytes, we'll use current nanoseconds 183 filename = fmt.Sprintf("%s%d", prefix, time.Now().UnixNano()) 184 } 185 path := filepath.Join(tempDir, filename) 186 return path 187 } 188 189 // WriteTempFile creates a unique temp file with data. 190 // 191 // For example: 192 // 193 // WriteTempFile("Test.", byte[]("test data"), 0600) 194 func WriteTempFile(prefix string, data []byte, mode os.FileMode) (string, error) { 195 path := TempPath("", prefix) 196 if err := os.WriteFile(path, data, mode); err != nil { 197 return "", err 198 } 199 return path, nil 200 } 201 202 // MakeTempDir creates a unique temp directory. 203 // 204 // For example: 205 // 206 // MakeTempDir("Test.", 0700) 207 func MakeTempDir(prefix string, mode os.FileMode) (string, error) { 208 path := TempPath("", prefix) 209 if err := os.MkdirAll(path, mode); err != nil { 210 return "", err 211 } 212 return path, nil 213 } 214 215 // IsDirReal returns true if directory exists and is a real directory (not a symlink). 216 // If it returns false, an error will be set explaining why. 217 func IsDirReal(path string) (bool, error) { 218 fileInfo, err := os.Lstat(path) 219 if err != nil { 220 return false, err 221 } 222 // Check if symlink 223 if fileInfo.Mode()&os.ModeSymlink != 0 { 224 return false, fmt.Errorf("Path is a symlink") 225 } 226 if !fileInfo.Mode().IsDir() { 227 return false, fmt.Errorf("Path is not a directory") 228 } 229 return true, nil 230 } 231 232 // MoveFile moves a file safely. 233 // It will create parent directories for destinationPath if they don't exist. 234 // If the destination already exists and you specify a tmpDir, it will move 235 // it there, otherwise it will be removed. 236 func MoveFile(sourcePath string, destinationPath string, tmpDir string, log Log) error { 237 if _, statErr := os.Stat(destinationPath); statErr == nil { 238 if tmpDir == "" { 239 log.Infof("Removing existing destination path: %s", destinationPath) 240 if removeErr := os.RemoveAll(destinationPath); removeErr != nil { 241 return removeErr 242 } 243 } else { 244 tmpPath := filepath.Join(tmpDir, filepath.Base(destinationPath)) 245 log.Infof("Moving existing destination %q to %q", destinationPath, tmpPath) 246 if tmpMoveErr := os.Rename(destinationPath, tmpPath); tmpMoveErr != nil { 247 return tmpMoveErr 248 } 249 } 250 } 251 252 if err := MakeParentDirs(destinationPath, 0700, log); err != nil { 253 return err 254 } 255 256 log.Infof("Moving %s to %s", sourcePath, destinationPath) 257 // Rename will copy over an existing destination 258 return os.Rename(sourcePath, destinationPath) 259 } 260 261 // CopyFile copies a file safely. 262 // It will create parent directories for destinationPath if they don't exist. 263 // It will overwrite an existing destinationPath. 264 func CopyFile(sourcePath string, destinationPath string, log Log) error { 265 log.Infof("Copying %s to %s", sourcePath, destinationPath) 266 in, err := os.Open(sourcePath) 267 if err != nil { 268 return err 269 } 270 defer Close(in) 271 272 if _, statErr := os.Stat(destinationPath); statErr == nil { 273 log.Infof("Removing existing destination path: %s", destinationPath) 274 if removeErr := os.RemoveAll(destinationPath); removeErr != nil { 275 return removeErr 276 } 277 } 278 279 if makeDirErr := MakeParentDirs(destinationPath, 0700, log); makeDirErr != nil { 280 return makeDirErr 281 } 282 283 out, err := os.Create(destinationPath) 284 if err != nil { 285 return err 286 } 287 defer Close(out) 288 _, err = io.Copy(out, in) 289 closeErr := out.Close() 290 if err != nil { 291 return err 292 } 293 return closeErr 294 } 295 296 // ReadFile returns data for file at path 297 func ReadFile(path string) ([]byte, error) { 298 file, err := os.Open(path) 299 if err != nil { 300 return nil, err 301 } 302 defer Close(file) 303 data, err := io.ReadAll(file) 304 if err != nil { 305 return nil, err 306 } 307 return data, nil 308 } 309 310 func convertPathForWindows(path string) string { 311 return "/" + strings.ReplaceAll(path, `\`, `/`) 312 } 313 314 // URLStringForPath returns an URL as string with file scheme for path. 315 // For example, 316 // 317 // /usr/local/go/bin => file:///usr/local/go/bin 318 // C:\Go\bin => file:///C:/Go/bin 319 func URLStringForPath(path string) string { 320 if runtime.GOOS == "windows" { 321 path = convertPathForWindows(path) 322 } 323 u := &url.URL{Path: path} 324 encodedPath := u.String() 325 return fmt.Sprintf("%s://%s", fileScheme, encodedPath) 326 } 327 328 // PathFromURL returns path for file URL scheme 329 // For example, 330 // 331 // file:///usr/local/go/bin => /usr/local/go/bin 332 // file:///C:/Go/bin => C:\Go\bin 333 func PathFromURL(u *url.URL) string { 334 path := u.Path 335 if runtime.GOOS == "windows" && u.Scheme == fileScheme { 336 // Remove leading slash for Windows 337 path = strings.TrimPrefix(path, "/") 338 path = filepath.FromSlash(path) 339 } 340 return path 341 } 342 343 // Touch a file, updating its modification time 344 func Touch(path string) error { 345 f, err := os.OpenFile(path, os.O_RDONLY|os.O_CREATE|os.O_TRUNC, 0600) 346 Close(f) 347 return err 348 } 349 350 // FileModTime returns modification time for file. 351 // If file doesn't exist returns error. 352 func FileModTime(path string) (time.Time, error) { 353 info, err := os.Stat(path) 354 if err != nil { 355 return time.Time{}, err 356 } 357 return info.ModTime(), nil 358 }