1package sd23import (4 "crypto/sha1"5 "encoding/hex"6 "encoding/json"7 "errors"8 "io/fs"9 "os"10 "path/filepath"11 "strings"12)1314// Account is a Schedules Direct login as the server keeps it: the user15// name and the SHA-1 of the password, which is what the API takes, so the16// password itself is never stored.17type Account struct {18 User string `json:"user"`19 PasswordSHA1 string `json:"passwordSha1"`20}2122// Set reports whether there's a login.23func (a Account) Set() bool { return a.User != "" && a.PasswordSHA1 != "" }2425// HashPassword is the form of a password the API takes: its SHA-1, in hex.26func HashPassword(password string) string {27 sum := sha1.Sum([]byte(password))28 return hex.EncodeToString(sum[:])29}3031// LoadAccount reads the login kept at path; a missing file is no login.32func LoadAccount(path string) (Account, error) {33 var a Account34 raw, err := os.ReadFile(path)35 if errors.Is(err, fs.ErrNotExist) {36 return a, nil37 }38 if err != nil {39 return a, err40 }41 err = json.Unmarshal(raw, &a)42 a.User = strings.TrimSpace(a.User)43 return a, err44}4546// SaveAccount keeps a at path, readable by this user only; an empty47// account removes the file.48func SaveAccount(path string, a Account) error {49 if !a.Set() {50 err := os.Remove(path)51 if errors.Is(err, fs.ErrNotExist) {52 return nil53 }54 return err55 }56 raw, err := json.MarshalIndent(a, "", " ")57 if err != nil {58 return err59 }60 return writeFile(path, raw, 0o600)61}6263// writeFile replaces path with raw atomically, with the given mode.64func writeFile(path string, raw []byte, mode os.FileMode) error {65 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {66 return err67 }68 f, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".*")69 if err != nil {70 return err71 }72 _, err = f.Write(raw)73 if err == nil {74 err = f.Chmod(mode)75 }76 if err == nil {77 err = f.Sync()78 }79 if cerr := f.Close(); err == nil {80 err = cerr81 }82 if err == nil {83 err = os.Rename(f.Name(), path)84 }85 if err != nil {86 os.Remove(f.Name())87 }88 return err89}