github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/config/config.go (about) 1 // Package config reads, writes and edits the config file and deals with command line flags 2 package config 3 4 import ( 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "log" 10 mathrand "math/rand" 11 "os" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strings" 16 "time" 17 18 "github.com/mitchellh/go-homedir" 19 20 "github.com/rclone/rclone/fs" 21 "github.com/rclone/rclone/fs/cache" 22 "github.com/rclone/rclone/fs/config/configmap" 23 "github.com/rclone/rclone/fs/config/obscure" 24 "github.com/rclone/rclone/fs/fspath" 25 "github.com/rclone/rclone/fs/rc" 26 "github.com/rclone/rclone/lib/file" 27 "github.com/rclone/rclone/lib/random" 28 ) 29 30 const ( 31 configFileName = "rclone.conf" 32 hiddenConfigFileName = "." + configFileName 33 noConfigFile = "notfound" 34 35 // ConfigToken is the key used to store the token under 36 ConfigToken = "token" 37 38 // ConfigClientID is the config key used to store the client id 39 ConfigClientID = "client_id" 40 41 // ConfigClientSecret is the config key used to store the client secret 42 ConfigClientSecret = "client_secret" 43 44 // ConfigAuthURL is the config key used to store the auth server endpoint 45 ConfigAuthURL = "auth_url" 46 47 // ConfigTokenURL is the config key used to store the token server endpoint 48 ConfigTokenURL = "token_url" 49 50 // ConfigEncoding is the config key to change the encoding for a backend 51 ConfigEncoding = "encoding" 52 53 // ConfigEncodingHelp is the help for ConfigEncoding 54 ConfigEncodingHelp = "The encoding for the backend.\n\nSee the [encoding section in the overview](/overview/#encoding) for more info." 55 56 // ConfigAuthorize indicates that we just want "rclone authorize" 57 ConfigAuthorize = "config_authorize" 58 59 // ConfigAuthNoBrowser indicates that we do not want to open browser 60 ConfigAuthNoBrowser = "config_auth_no_browser" 61 62 // ConfigTemplate is the template content to be used in the authorization webserver 63 ConfigTemplate = "config_template" 64 65 // ConfigTemplateFile is the path to a template file to read into the value of `ConfigTemplate` above 66 ConfigTemplateFile = "config_template_file" 67 ) 68 69 // Storage defines an interface for loading and saving config to 70 // persistent storage. Rclone provides a default implementation to 71 // load and save to a config file when this is imported 72 // 73 // import "github.com/rclone/rclone/fs/config/configfile" 74 // configfile.Install() 75 type Storage interface { 76 // GetSectionList returns a slice of strings with names for all the 77 // sections 78 GetSectionList() []string 79 80 // HasSection returns true if section exists in the config file 81 HasSection(section string) bool 82 83 // DeleteSection removes the named section and all config from the 84 // config file 85 DeleteSection(section string) 86 87 // GetKeyList returns the keys in this section 88 GetKeyList(section string) []string 89 90 // GetValue returns the key in section with a found flag 91 GetValue(section string, key string) (value string, found bool) 92 93 // SetValue sets the value under key in section 94 SetValue(section string, key string, value string) 95 96 // DeleteKey removes the key under section 97 DeleteKey(section string, key string) bool 98 99 // Load the config from permanent storage 100 Load() error 101 102 // Save the config to permanent storage 103 Save() error 104 105 // Serialize the config into a string 106 Serialize() (string, error) 107 } 108 109 // Global 110 var ( 111 // Password can be used to configure the random password generator 112 Password = random.Password 113 ) 114 115 var ( 116 configPath string 117 cacheDir string 118 data Storage 119 dataLoaded bool 120 ) 121 122 func init() { 123 // Set the function pointers up in fs 124 fs.ConfigFileGet = FileGetFlag 125 fs.ConfigFileSet = SetValueAndSave 126 fs.ConfigFileHasSection = func(section string) bool { 127 return LoadedData().HasSection(section) 128 } 129 configPath = makeConfigPath() 130 cacheDir = makeCacheDir() // Has fallback to tempDir, so set that first 131 data = newDefaultStorage() 132 } 133 134 // Join directory with filename, and check if exists 135 func findFile(dir string, name string) string { 136 path := filepath.Join(dir, name) 137 if _, err := os.Stat(path); err != nil { 138 return "" 139 } 140 return path 141 } 142 143 // Find current user's home directory 144 func findHomeDir() (string, error) { 145 path, err := homedir.Dir() 146 if err != nil { 147 fs.Debugf(nil, "Home directory lookup failed and cannot be used as configuration location: %v", err) 148 } else if path == "" { 149 // On Unix homedir return success but empty string for user with empty home configured in passwd file 150 fs.Debugf(nil, "Home directory not defined and cannot be used as configuration location") 151 } 152 return path, err 153 } 154 155 // Find rclone executable directory and look for existing rclone.conf there 156 // (<rclone_exe_dir>/rclone.conf) 157 func findLocalConfig() (configDir string, configFile string) { 158 if exePath, err := os.Executable(); err == nil { 159 configDir = filepath.Dir(exePath) 160 configFile = findFile(configDir, configFileName) 161 } 162 return 163 } 164 165 // Get path to Windows AppData config subdirectory for rclone and look for existing rclone.conf there 166 // ($AppData/rclone/rclone.conf) 167 func findAppDataConfig() (configDir string, configFile string) { 168 if appDataDir := os.Getenv("APPDATA"); appDataDir != "" { 169 configDir = filepath.Join(appDataDir, "rclone") 170 configFile = findFile(configDir, configFileName) 171 } else { 172 fs.Debugf(nil, "Environment variable APPDATA is not defined and cannot be used as configuration location") 173 } 174 return 175 } 176 177 // Get path to XDG config subdirectory for rclone and look for existing rclone.conf there 178 // (see XDG Base Directory specification: https://specifications.freedesktop.org/basedir-spec/latest/). 179 // ($XDG_CONFIG_HOME\rclone\rclone.conf) 180 func findXDGConfig() (configDir string, configFile string) { 181 if xdgConfigDir := os.Getenv("XDG_CONFIG_HOME"); xdgConfigDir != "" { 182 configDir = filepath.Join(xdgConfigDir, "rclone") 183 configFile = findFile(configDir, configFileName) 184 } 185 return 186 } 187 188 // Get path to .config subdirectory for rclone and look for existing rclone.conf there 189 // (~/.config/rclone/rclone.conf) 190 func findDotConfigConfig(home string) (configDir string, configFile string) { 191 if home != "" { 192 configDir = filepath.Join(home, ".config", "rclone") 193 configFile = findFile(configDir, configFileName) 194 } 195 return 196 } 197 198 // Look for existing .rclone.conf (legacy hidden filename) in root of user's home directory 199 // (~/.rclone.conf) 200 func findOldHomeConfig(home string) (configDir string, configFile string) { 201 if home != "" { 202 configDir = home 203 configFile = findFile(home, hiddenConfigFileName) 204 } 205 return 206 } 207 208 // Return the path to the configuration file 209 func makeConfigPath() string { 210 // Look for existing rclone.conf in prioritized list of known locations 211 // Also get configuration directory to use for new config file when no existing is found. 212 var ( 213 configFile string 214 configDir string 215 primaryConfigDir string 216 fallbackConfigDir string 217 ) 218 // <rclone_exe_dir>/rclone.conf 219 if _, configFile = findLocalConfig(); configFile != "" { 220 return configFile 221 } 222 // Windows: $AppData/rclone/rclone.conf 223 // This is also the default location for new config when no existing is found 224 if runtime.GOOS == "windows" { 225 if primaryConfigDir, configFile = findAppDataConfig(); configFile != "" { 226 return configFile 227 } 228 } 229 // $XDG_CONFIG_HOME/rclone/rclone.conf 230 // Also looking for this on Windows, for backwards compatibility reasons. 231 if configDir, configFile = findXDGConfig(); configFile != "" { 232 return configFile 233 } 234 if runtime.GOOS != "windows" { 235 // On Unix this is also the default location for new config when no existing is found 236 primaryConfigDir = configDir 237 } 238 // ~/.config/rclone/rclone.conf 239 // This is also the fallback location for new config 240 // (when $AppData on Windows and $XDG_CONFIG_HOME on Unix is not defined) 241 homeDir, homeDirErr := findHomeDir() 242 if fallbackConfigDir, configFile = findDotConfigConfig(homeDir); configFile != "" { 243 return configFile 244 } 245 // ~/.rclone.conf 246 if _, configFile = findOldHomeConfig(homeDir); configFile != "" { 247 return configFile 248 } 249 250 // No existing config file found, prepare proper default for a new one. 251 // But first check if user supplied a --config variable or environment 252 // variable, since then we skip actually trying to create the default 253 // and report any errors related to it (we can't use pflag for this because 254 // it isn't initialised yet so we search the command line manually). 255 _, configSupplied := os.LookupEnv("RCLONE_CONFIG") 256 if !configSupplied { 257 for _, item := range os.Args { 258 if item == "--config" || strings.HasPrefix(item, "--config=") { 259 configSupplied = true 260 break 261 } 262 } 263 } 264 // If we found a configuration directory to be used for new config during search 265 // above, then create it to be ready for rclone.conf file to be written into it 266 // later, and also as a test of permissions to use fallback if not even able to 267 // create the directory. 268 if primaryConfigDir != "" { 269 configDir = primaryConfigDir 270 } else if fallbackConfigDir != "" { 271 configDir = fallbackConfigDir 272 } else { 273 configDir = "" 274 } 275 if configDir != "" { 276 configFile = filepath.Join(configDir, configFileName) 277 if configSupplied { 278 // User supplied custom config option, just return the default path 279 // as is without creating any directories, since it will not be used 280 // anyway and we don't want to unnecessarily create empty directory. 281 return configFile 282 } 283 var mkdirErr error 284 if mkdirErr = file.MkdirAll(configDir, os.ModePerm); mkdirErr == nil { 285 return configFile 286 } 287 // Problem: Try a fallback location. If we did find a home directory then 288 // just assume file .rclone.conf (legacy hidden filename) can be written in 289 // its root (~/.rclone.conf). 290 if homeDir != "" { 291 fs.Debugf(nil, "Configuration directory could not be created and will not be used: %v", mkdirErr) 292 return filepath.Join(homeDir, hiddenConfigFileName) 293 } 294 if !configSupplied { 295 fs.Errorf(nil, "Couldn't find home directory nor create configuration directory: %v", mkdirErr) 296 } 297 } else if !configSupplied { 298 if homeDirErr != nil { 299 fs.Errorf(nil, "Couldn't find configuration directory nor home directory: %v", homeDirErr) 300 } else { 301 fs.Errorf(nil, "Couldn't find configuration directory nor home directory") 302 } 303 } 304 // No known location that can be used: Did possibly find a configDir 305 // (XDG_CONFIG_HOME or APPDATA) which couldn't be created, but in any case 306 // did not find a home directory! 307 // Report it as an error, and return as last resort the path relative to current 308 // working directory, of .rclone.conf (legacy hidden filename). 309 if !configSupplied { 310 fs.Errorf(nil, "Defaulting to storing config in current directory.") 311 fs.Errorf(nil, "Use --config flag to workaround.") 312 } 313 return hiddenConfigFileName 314 } 315 316 // GetConfigPath returns the current config file path 317 func GetConfigPath() string { 318 return configPath 319 } 320 321 // SetConfigPath sets new config file path 322 // 323 // Checks for empty string, os null device, or special path, all of which indicates in-memory config. 324 func SetConfigPath(path string) (err error) { 325 var cfgPath string 326 if path == "" || path == os.DevNull { 327 cfgPath = "" 328 } else if filepath.Base(path) == noConfigFile { 329 cfgPath = "" 330 } else if err = file.IsReserved(path); err != nil { 331 return err 332 } else if cfgPath, err = filepath.Abs(path); err != nil { 333 return err 334 } 335 configPath = cfgPath 336 return nil 337 } 338 339 // SetData sets new config file storage 340 func SetData(newData Storage) { 341 // If no config file, use in-memory config (which is the default) 342 if configPath == "" { 343 return 344 } 345 data = newData 346 dataLoaded = false 347 } 348 349 // Data returns current config file storage 350 func Data() Storage { 351 return data 352 } 353 354 // LoadedData ensures the config file storage is loaded and returns it 355 func LoadedData() Storage { 356 if !dataLoaded { 357 // Set RCLONE_CONFIG_DIR for backend config and subprocesses 358 // If empty configPath (in-memory only) the value will be "." 359 _ = os.Setenv("RCLONE_CONFIG_DIR", filepath.Dir(configPath)) 360 // Load configuration from file (or initialize sensible default if no file or error) 361 if err := data.Load(); err == nil { 362 fs.Debugf(nil, "Using config file from %q", configPath) 363 dataLoaded = true 364 } else if err == ErrorConfigFileNotFound { 365 if configPath == "" { 366 fs.Debugf(nil, "Config is memory-only - using defaults") 367 } else { 368 fs.Logf(nil, "Config file %q not found - using defaults", configPath) 369 } 370 dataLoaded = true 371 } else { 372 log.Fatalf("Failed to load config file %q: %v", configPath, err) 373 } 374 } 375 return data 376 } 377 378 // ErrorConfigFileNotFound is returned when the config file is not found 379 var ErrorConfigFileNotFound = errors.New("config file not found") 380 381 // SaveConfig calling function which saves configuration file. 382 // if SaveConfig returns error trying again after sleep. 383 func SaveConfig() { 384 ctx := context.Background() 385 ci := fs.GetConfig(ctx) 386 var err error 387 for i := 0; i < ci.LowLevelRetries+1; i++ { 388 if err = LoadedData().Save(); err == nil { 389 return 390 } 391 waitingTimeMs := mathrand.Intn(1000) 392 time.Sleep(time.Duration(waitingTimeMs) * time.Millisecond) 393 } 394 fs.Errorf(nil, "Failed to save config after %d tries: %v", ci.LowLevelRetries, err) 395 } 396 397 // SetValueAndSave sets the key to the value and saves just that 398 // value in the config file. It loads the old config file in from 399 // disk first and overwrites the given value only. 400 func SetValueAndSave(name, key, value string) error { 401 // Set the value in config in case we fail to reload it 402 LoadedData().SetValue(name, key, value) 403 // Save it again 404 SaveConfig() 405 return nil 406 } 407 408 // getWithDefault gets key out of section name returning defaultValue if not 409 // found. 410 func getWithDefault(name, key, defaultValue string) string { 411 value, found := LoadedData().GetValue(name, key) 412 if !found { 413 return defaultValue 414 } 415 return value 416 } 417 418 // UpdateRemoteOpt configures the remote update 419 type UpdateRemoteOpt struct { 420 // Treat all passwords as plain that need obscuring 421 Obscure bool `json:"obscure"` 422 // Treat all passwords as obscured 423 NoObscure bool `json:"noObscure"` 424 // Don't interact with the user - return questions 425 NonInteractive bool `json:"nonInteractive"` 426 // If set then supply state and result parameters to continue the process 427 Continue bool `json:"continue"` 428 // If set then ask all the questions, not just the post config questions 429 All bool `json:"all"` 430 // State to restart with - used with Continue 431 State string `json:"state"` 432 // Result to return - used with Continue 433 Result string `json:"result"` 434 // If set then edit existing values 435 Edit bool `json:"edit"` 436 } 437 438 func updateRemote(ctx context.Context, name string, keyValues rc.Params, opt UpdateRemoteOpt) (out *fs.ConfigOut, err error) { 439 if opt.Obscure && opt.NoObscure { 440 return nil, errors.New("can't use --obscure and --no-obscure together") 441 } 442 443 err = fspath.CheckConfigName(name) 444 if err != nil { 445 return nil, err 446 } 447 interactive := !(opt.NonInteractive || opt.Continue) 448 if interactive && !opt.All { 449 ctx = suppressConfirm(ctx) 450 } 451 452 fsType := FileGet(name, "type") 453 if fsType == "" { 454 return nil, errors.New("couldn't find type field in config") 455 } 456 457 ri, err := fs.Find(fsType) 458 if err != nil { 459 return nil, fmt.Errorf("couldn't find backend for type %q", fsType) 460 } 461 462 // Work out which options need to be obscured 463 needsObscure := map[string]struct{}{} 464 if !opt.NoObscure { 465 for _, option := range ri.Options { 466 if option.IsPassword { 467 needsObscure[option.Name] = struct{}{} 468 } 469 } 470 } 471 472 choices := configmap.Simple{} 473 m := fs.ConfigMap(ri, name, nil) 474 475 // Set the config 476 for k, v := range keyValues { 477 vStr := fmt.Sprint(v) 478 if strings.ContainsAny(k, "\n\r") || strings.ContainsAny(vStr, "\n\r") { 479 return nil, fmt.Errorf("update remote: invalid key or value contains \\n or \\r") 480 } 481 // Obscure parameter if necessary 482 if _, ok := needsObscure[k]; ok { 483 _, err := obscure.Reveal(vStr) 484 if err != nil || opt.Obscure { 485 // If error => not already obscured, so obscure it 486 // or we are forced to obscure 487 vStr, err = obscure.Obscure(vStr) 488 if err != nil { 489 return nil, fmt.Errorf("update remote: obscure failed: %w", err) 490 } 491 } 492 } 493 choices.Set(k, vStr) 494 if !strings.HasPrefix(k, fs.ConfigKeyEphemeralPrefix) { 495 m.Set(k, vStr) 496 } 497 } 498 if opt.Edit { 499 choices[fs.ConfigEdit] = "true" 500 } 501 502 if interactive { 503 var state = "" 504 if opt.All { 505 state = fs.ConfigAll 506 } 507 err = backendConfig(ctx, name, m, ri, choices, state) 508 } else { 509 // Start the config state machine 510 in := fs.ConfigIn{ 511 State: opt.State, 512 Result: opt.Result, 513 } 514 if in.State == "" && opt.All { 515 in.State = fs.ConfigAll 516 } 517 out, err = fs.BackendConfig(ctx, name, m, ri, choices, in) 518 } 519 if err != nil { 520 return nil, err 521 } 522 SaveConfig() 523 cache.ClearConfig(name) // remove any remotes based on this config from the cache 524 return out, nil 525 } 526 527 // UpdateRemote adds the keyValues passed in to the remote of name. 528 // keyValues should be key, value pairs. 529 func UpdateRemote(ctx context.Context, name string, keyValues rc.Params, opt UpdateRemoteOpt) (out *fs.ConfigOut, err error) { 530 opt.Edit = true 531 return updateRemote(ctx, name, keyValues, opt) 532 } 533 534 // CreateRemote creates a new remote with name, type and a list of 535 // parameters which are key, value pairs. If update is set then it 536 // adds the new keys rather than replacing all of them. 537 func CreateRemote(ctx context.Context, name string, Type string, keyValues rc.Params, opts UpdateRemoteOpt) (out *fs.ConfigOut, err error) { 538 err = fspath.CheckConfigName(name) 539 if err != nil { 540 return nil, err 541 } 542 if !opts.Continue { 543 // Delete the old config if it exists 544 LoadedData().DeleteSection(name) 545 // Set the type 546 LoadedData().SetValue(name, "type", Type) 547 } 548 // Set the remaining values 549 return UpdateRemote(ctx, name, keyValues, opts) 550 } 551 552 // PasswordRemote adds the keyValues passed in to the remote of name. 553 // keyValues should be key, value pairs. 554 func PasswordRemote(ctx context.Context, name string, keyValues rc.Params) error { 555 ctx = suppressConfirm(ctx) 556 err := fspath.CheckConfigName(name) 557 if err != nil { 558 return err 559 } 560 for k, v := range keyValues { 561 keyValues[k] = obscure.MustObscure(fmt.Sprint(v)) 562 } 563 _, err = UpdateRemote(ctx, name, keyValues, UpdateRemoteOpt{ 564 NoObscure: true, 565 }) 566 return err 567 } 568 569 // JSONListProviders prints all the providers and options in JSON format 570 func JSONListProviders() error { 571 b, err := json.MarshalIndent(fs.Registry, "", " ") 572 if err != nil { 573 return fmt.Errorf("failed to marshal examples: %w", err) 574 } 575 _, err = os.Stdout.Write(b) 576 if err != nil { 577 return fmt.Errorf("failed to write providers list: %w", err) 578 } 579 return nil 580 } 581 582 // fsOption returns an Option describing the possible remotes 583 func fsOption() *fs.Option { 584 o := &fs.Option{ 585 Name: "Storage", 586 Help: "Type of storage to configure.", 587 Default: "", 588 Required: true, 589 } 590 for _, item := range fs.Registry { 591 if item.Hide { 592 continue 593 } 594 example := fs.OptionExample{ 595 Value: item.Name, 596 Help: item.Description, 597 } 598 o.Examples = append(o.Examples, example) 599 } 600 o.Examples.Sort() 601 return o 602 } 603 604 // FileGetFlag gets the config key under section returning the 605 // the value and true if found and or ("", false) otherwise 606 func FileGetFlag(section, key string) (string, bool) { 607 return LoadedData().GetValue(section, key) 608 } 609 610 // FileGet gets the config key under section returning the default if not set. 611 // 612 // It looks up defaults in the environment if they are present 613 func FileGet(section, key string) string { 614 var defaultVal string 615 envKey := fs.ConfigToEnv(section, key) 616 newValue, found := os.LookupEnv(envKey) 617 if found { 618 defaultVal = newValue 619 } 620 return getWithDefault(section, key, defaultVal) 621 } 622 623 // FileSet sets the key in section to value. It doesn't save 624 // the config file. 625 func FileSet(section, key, value string) { 626 if value != "" { 627 LoadedData().SetValue(section, key, value) 628 } else { 629 FileDeleteKey(section, key) 630 } 631 } 632 633 // FileDeleteKey deletes the config key in the config file. 634 // It returns true if the key was deleted, 635 // or returns false if the section or key didn't exist. 636 func FileDeleteKey(section, key string) bool { 637 return LoadedData().DeleteKey(section, key) 638 } 639 640 var matchEnv = regexp.MustCompile(`^RCLONE_CONFIG_(.*?)_TYPE=.*$`) 641 642 // FileSections returns the sections in the config file 643 // including any defined by environment variables. 644 func FileSections() []string { 645 sections := LoadedData().GetSectionList() 646 for _, item := range os.Environ() { 647 matches := matchEnv.FindStringSubmatch(item) 648 if len(matches) == 2 { 649 sections = append(sections, strings.ToLower(matches[1])) 650 } 651 } 652 return sections 653 } 654 655 // DumpRcRemote dumps the config for a single remote 656 func DumpRcRemote(name string) (dump rc.Params) { 657 params := rc.Params{} 658 for _, key := range LoadedData().GetKeyList(name) { 659 params[key] = FileGet(name, key) 660 } 661 return params 662 } 663 664 // DumpRcBlob dumps all the config as an unstructured blob suitable 665 // for the rc 666 func DumpRcBlob() (dump rc.Params) { 667 dump = rc.Params{} 668 for _, name := range LoadedData().GetSectionList() { 669 dump[name] = DumpRcRemote(name) 670 } 671 return dump 672 } 673 674 // Dump dumps all the config as a JSON file 675 func Dump() error { 676 dump := DumpRcBlob() 677 b, err := json.MarshalIndent(dump, "", " ") 678 if err != nil { 679 return fmt.Errorf("failed to marshal config dump: %w", err) 680 } 681 _, err = os.Stdout.Write(b) 682 if err != nil { 683 return fmt.Errorf("failed to write config dump: %w", err) 684 } 685 return nil 686 } 687 688 // makeCacheDir returns a directory to use for caching. 689 func makeCacheDir() (dir string) { 690 dir, err := os.UserCacheDir() 691 if err != nil || dir == "" { 692 fs.Debugf(nil, "Failed to find user cache dir, using temporary directory: %v", err) 693 // if no dir found then use TempDir - we will have a cachedir! 694 dir = os.TempDir() 695 } 696 return filepath.Join(dir, "rclone") 697 } 698 699 // GetCacheDir returns the default directory for cache 700 // 701 // The directory is neither guaranteed to exist nor have accessible permissions. 702 // Users of this should make a subdirectory and use MkdirAll() to create it 703 // and any parents. 704 func GetCacheDir() string { 705 return cacheDir 706 } 707 708 // SetCacheDir sets new default directory for cache 709 func SetCacheDir(path string) (err error) { 710 cacheDir, err = filepath.Abs(path) 711 return 712 } 713 714 // SetTempDir sets new default directory to use for temporary files. 715 // 716 // Assuming golang's os.TempDir is used to get the directory: 717 // "On Unix systems, it returns $TMPDIR if non-empty, else /tmp. On Windows, 718 // it uses GetTempPath, returning the first non-empty value from %TMP%, %TEMP%, 719 // %USERPROFILE%, or the Windows directory." 720 // 721 // To override the default we therefore set environment variable TMPDIR 722 // on Unix systems, and both TMP and TEMP on Windows (they are almost exclusively 723 // aliases for the same path, and programs may refer to to either of them). 724 // This should make all libraries and forked processes use the same. 725 func SetTempDir(path string) (err error) { 726 var tempDir string 727 if tempDir, err = filepath.Abs(path); err != nil { 728 return err 729 } 730 if runtime.GOOS == "windows" { 731 if err = os.Setenv("TMP", tempDir); err != nil { 732 return err 733 } 734 if err = os.Setenv("TEMP", tempDir); err != nil { 735 return err 736 } 737 } else { 738 return os.Setenv("TMPDIR", tempDir) 739 } 740 return nil 741 }