blob: 8c2bf3b23e8a8df67d323954348cc502311af267 [file] [log] [blame]
// Copyright 2017 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// I/O helpers.
package subcmd
import (
"io/ioutil"
"os"
osUser "os/user"
"path/filepath"
"strings"
"upspin.io/config"
"upspin.io/path"
"upspin.io/upspin"
"upspin.io/user"
)
var userLookup = osUser.Lookup
var home string // Main user's home directory.
func homeDir(who string) string {
if who == "" {
if home == "" {
var err error
home, err = config.Homedir()
if err != nil {
return "~" // What else can we do?
}
}
}
u, err := userLookup(who)
if err != nil {
return "~" + who // Again, what else can we do?
}
return u.HomeDir
}
// AtSign processes a leading at sign, if any, in the Upspin file name and replaces it
// with the current user name. The name must be strictly "@" or begin with "@/",
// possibly with a suffix ("@+snapshot", "@+camera/").
// (If the user name ready has a suffix and the file starts @+suffix,
// the returned user name appends the suffix from the argument.)
// Unlike Tilde, it does not look up others user's roots.
// The argument is of type string; once a file becomes an upspin.PathName it should
// not be passed to this function.
// If the file name does not begin with an at sign, AtSign returns the argument
// unchanged except for promotion to upspin.PathName.
// If the target user does not exist, it returns the original string.
func (s *State) AtSign(file string) upspin.PathName {
if s.Config == nil || file == "" || file[0] != '@' {
return upspin.PathName(file)
}
userStr := string(s.Config.UserName())
if file == "@" {
return upspin.PathName(userStr + "/")
}
if strings.HasPrefix(file, "@/") {
return upspin.PathName(userStr + file[1:])
}
if strings.HasPrefix(file, "@+") {
// Need to split the user name.
usr, _, domain, err := user.Parse(s.Config.UserName())
if err != nil { // Can't happen.
return upspin.PathName(file)
}
slash := strings.IndexByte(file, '/')
if slash < 0 {
return upspin.PathName(usr + file[1:] + "@" + domain + "/")
}
return upspin.PathName(usr + file[1:slash] + "@" + domain + file[slash:])
}
return upspin.PathName(file)
}
// Tilde processes a leading tilde, if any, in the local file name.
// If the file name does not begin with a tilde, Tilde returns the argument unchanged.
// This special processing (only) is applied to all local file names passed to
// functions in this package.
// If the target user does not exist, it returns the original string.
func Tilde(file string) string {
if file == "" || file[0] != '~' {
return file
}
if file == "~" {
return homeDir("")
}
slash := strings.IndexByte(file, '/')
if slash < 0 {
return homeDir(file[1:])
}
return filepath.Join(homeDir(file[1:slash]), file[slash+1:])
}
// ReadAll reads all contents from a local input file or from stdin if
// the input file name is empty
func (s *State) ReadAll(fileName string) []byte {
if fileName == "" {
data, err := ioutil.ReadAll(s.Stdin)
if err != nil {
s.Exit(err)
}
return data
}
input := s.OpenLocal(Tilde(fileName))
defer input.Close()
data, err := ioutil.ReadAll(input)
if err != nil {
s.Exit(err)
}
return data
}
// OpenLocal opens a file on local disk.
func (s *State) OpenLocal(path string) *os.File {
f, err := os.Open(Tilde(path))
if err != nil {
s.Exit(err)
}
return f
}
// CreateLocal creates a file on local disk.
func (s *State) CreateLocal(path string) *os.File {
f, err := os.Create(Tilde(path))
if err != nil {
s.Exit(err)
}
return f
}
// MkdirLocal creates a directory on local disk.
// It requires all but the last element to be present.
func (s *State) MkdirLocal(path string) {
err := os.Mkdir(Tilde(path), 0700)
if err != nil {
s.Exit(err)
}
}
// MkdirAllLocal creates a directory on local disk.
// It creates as much of the path as is necessary.
func (s *State) MkdirAllLocal(path string) {
err := os.MkdirAll(Tilde(path), 0700)
if err != nil {
s.Exit(err)
}
}
// ShouldNotExist calls s.Exit if the file already exists.
func (s *State) ShouldNotExist(path string) {
_, err := os.Stat(Tilde(path))
if err == nil {
s.Exitf("%s already exists", path)
}
if !os.IsNotExist(err) {
s.Exit(err)
}
}
// HasGlobChar reports whether the string contains an unescaped Glob metacharacter.
func HasGlobChar(pattern string) bool {
esc := false
for _, r := range pattern {
if esc {
esc = false // TODO: What if next rune is '/'?
continue
}
switch r {
case '\\':
esc = true
case '*', '[', '?':
return true
}
}
return false
}
// GlobAllUpspin processes the arguments, which should be Upspin paths,
// expanding glob patterns.
func (s *State) GlobAllUpspin(args []string) []*upspin.DirEntry {
entries := make([]*upspin.DirEntry, 0, len(args))
for _, arg := range args {
entries = append(entries, s.GlobUpspin(arg)...)
}
return entries
}
// GlobAllUpspinPath processes the arguments, which should be Upspin paths,
// expanding glob patterns. It returns just the paths.
func (s *State) GlobAllUpspinPath(args []string) []upspin.PathName {
paths := make([]upspin.PathName, 0, len(args))
for _, arg := range args {
paths = append(paths, s.GlobUpspinPath(arg)...)
}
return paths
}
// GlobUpspin glob-expands the argument, which must be a syntactically
// valid Upspin glob pattern (including a plain path name). If the path does
// not exist or the pattern matches no paths, the function exits.
func (s *State) GlobUpspin(pattern string) []*upspin.DirEntry {
// Must be a valid Upspin path.
pat := s.AtSign(pattern)
parsed, err := path.Parse(pat)
if err != nil {
s.Exit(err)
}
// If it has no metacharacters, look it up to be sure it exists.
if !HasGlobChar(string(pat)) {
entry, err := s.Client.Lookup(pat, false)
if err != nil {
s.Exit(err)
}
return []*upspin.DirEntry{entry}
}
entries, err := s.Client.Glob(parsed.String())
if err != nil {
s.Exit(err)
}
if len(entries) == 0 {
s.Exitf("no path matches %q", parsed)
}
return entries
}
// GlobUpspinPath glob-expands the argument, which must be a syntactically
// valid Upspin glob pattern (including a plain path name). It returns just
// the path names. If the pattern matches no paths, the function exits.
func (s *State) GlobUpspinPath(pattern string) []upspin.PathName {
// Note: We could call GlobUpspin but that might do an unnecessary Lookup.
pat := s.AtSign(pattern)
parsed, err := path.Parse(pat)
if err != nil {
s.Exit(err)
}
// If it has no metacharacters, leave it alone but clean it.
if !HasGlobChar(string(pat)) {
return []upspin.PathName{parsed.Path()}
}
entries, err := s.Client.Glob(parsed.String())
if err != nil {
s.Exit(err)
}
if len(entries) == 0 {
s.Exitf("no path matches %q", parsed)
}
names := make([]upspin.PathName, len(entries))
for i, entry := range entries {
names[i] = entry.Name
}
return names
}
// GlobOneUpspin glob-expands the argument, which must result in a
// single Upspin path.
func (s *State) GlobOneUpspinPath(pattern string) upspin.PathName {
entries := s.GlobUpspin(pattern)
if len(entries) != 1 {
s.Exitf("more than one file matches %q", pattern)
}
return entries[0].Name
}
// GlobLocal glob-expands the argument, which should be a syntactically
// valid Glob pattern (including a plain file name). If the pattern is erroneous
// or matches no files, the function exits.
func (s *State) GlobLocal(pattern string) []string {
pattern = Tilde(pattern)
// If it has no metacharacters, leave it alone.
if !HasGlobChar(pattern) {
return []string{pattern}
}
strs, err := filepath.Glob(pattern)
if err != nil {
// Bad pattern.
s.Exitf("bad local Glob pattern %q: %v", pattern, err)
}
if len(strs) == 0 {
s.Exitf("no path matches %q", pattern)
}
return strs
}
// GlobOneLocal glob-expands the argument, which must result in a
// single local file name. If not, it exits.
func (s *State) GlobOneLocal(pattern string) string {
strs := s.GlobLocal(pattern)
switch len(strs) {
case 0:
s.Exitf("no path matches %q", pattern)
case 1:
// OK.
default:
s.Exitf("more than one file matches %q", pattern)
}
return strs[0]
}