path.go 4.33 KB
Newer Older
Hector Sanjuan's avatar
Hector Sanjuan committed
1
// Package path contains utilities to work with ipfs paths.
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
2 3 4
package path

import (
5
	"errors"
6 7
	"path"
	"strings"
8

Jeromy's avatar
Jeromy committed
9
	cid "github.com/ipfs/go-cid"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
10 11
)

12 13 14 15 16 17 18 19 20
var (
	// ErrBadPath is returned when a given path is incorrectly formatted
	ErrBadPath = errors.New("invalid 'ipfs ref' path")

	// ErrNoComponents is used when Paths after a protocol
	// do not contain at least one component
	ErrNoComponents = errors.New(
		"path must contain at least one component")
)
21

Hector Sanjuan's avatar
Hector Sanjuan committed
22 23 24 25 26 27 28 29
// A Path represents an ipfs content path:
//   * /<cid>/path/to/file
//   * /ipfs/<cid>
//   * /ipns/<cid>/path/to/folder
//   * etc
type Path string

// ^^^
Jeromy's avatar
Jeromy committed
30 31
// TODO: debate making this a private struct wrapped in a public interface
// would allow us to control creation, and cache segments.
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
32

Hector Sanjuan's avatar
Hector Sanjuan committed
33
// FromString safely converts a string type to a Path type.
34 35 36 37
func FromString(s string) Path {
	return Path(s)
}

Hector Sanjuan's avatar
Hector Sanjuan committed
38
// FromCid safely converts a cid.Cid type to a Path type.
39
func FromCid(c cid.Cid) Path {
Jeromy's avatar
Jeromy committed
40
	return Path("/ipfs/" + c.String())
41 42
}

Hector Sanjuan's avatar
Hector Sanjuan committed
43 44
// Segments returns the different elements of a path
// (elements are delimited by a /).
Jeromy's avatar
Jeromy committed
45 46 47
func (p Path) Segments() []string {
	cleaned := path.Clean(string(p))
	segments := strings.Split(cleaned, "/")
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
48

Jeromy's avatar
Jeromy committed
49 50 51
	// Ignore leading slash
	if len(segments[0]) == 0 {
		segments = segments[1:]
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
52 53
	}

Jeromy's avatar
Jeromy committed
54
	return segments
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
55 56
}

Hector Sanjuan's avatar
Hector Sanjuan committed
57
// String converts a path to string.
Jeromy's avatar
Jeromy committed
58 59
func (p Path) String() string {
	return string(p)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
60
}
61

62 63
// IsJustAKey returns true if the path is of the form <key> or /ipfs/<key>, or
// /ipld/<key>
64 65
func (p Path) IsJustAKey() bool {
	parts := p.Segments()
66
	return len(parts) == 2 && (parts[0] == "ipfs" || parts[0] == "ipld")
67 68
}

69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
// PopLastSegment returns a new Path without its final segment, and the final
// segment, separately. If there is no more to pop (the path is just a key),
// the original path is returned.
func (p Path) PopLastSegment() (Path, string, error) {

	if p.IsJustAKey() {
		return p, "", nil
	}

	segs := p.Segments()
	newPath, err := ParsePath("/" + strings.Join(segs[:len(segs)-1], "/"))
	if err != nil {
		return "", "", err
	}

	return newPath, segs[len(segs)-1], nil
}

Hector Sanjuan's avatar
Hector Sanjuan committed
87
// FromSegments returns a path given its different segments.
88 89
func FromSegments(prefix string, seg ...string) (Path, error) {
	return ParsePath(prefix + strings.Join(seg, "/"))
90
}
91

Hector Sanjuan's avatar
Hector Sanjuan committed
92 93 94 95 96
// ParsePath returns a well-formed ipfs Path.
// The returned path will always be prefixed with /ipfs/ or /ipns/.
// The prefix will be added if not present in the given string.
// This function will return an error when the given string is
// not a valid ipfs path.
97 98
func ParsePath(txt string) (Path, error) {
	parts := strings.Split(txt, "/")
Jeromy's avatar
Jeromy committed
99
	if len(parts) == 1 {
Jeromy's avatar
Jeromy committed
100
		kp, err := ParseCidToPath(txt)
Jeromy's avatar
Jeromy committed
101 102 103 104
		if err == nil {
			return kp, nil
		}
	}
105

Hector Sanjuan's avatar
Hector Sanjuan committed
106
	// if the path doesnt begin with a '/'
107
	// we expect this to start with a hash, and be an 'ipfs' path
108
	if parts[0] != "" {
Jeromy's avatar
Jeromy committed
109
		if _, err := ParseCidToPath(parts[0]); err != nil {
110 111 112 113
			return "", ErrBadPath
		}
		// The case when the path starts with hash without a protocol prefix
		return Path("/ipfs/" + txt), nil
114 115
	}

116 117 118 119
	if len(parts) < 3 {
		return "", ErrBadPath
	}

120
	if parts[1] == "ipfs" {
Jeromy's avatar
Jeromy committed
121
		if _, err := ParseCidToPath(parts[2]); err != nil {
122 123
			return "", err
		}
124
	} else if parts[1] != "ipns" && parts[1] != "ipld" { //TODO: make this smarter
125 126 127 128 129 130
		return "", ErrBadPath
	}

	return Path(txt), nil
}

Hector Sanjuan's avatar
Hector Sanjuan committed
131
// ParseCidToPath takes a CID in string form and returns a valid ipfs Path.
Jeromy's avatar
Jeromy committed
132
func ParseCidToPath(txt string) (Path, error) {
133 134 135 136
	if txt == "" {
		return "", ErrNoComponents
	}

Jeromy's avatar
Jeromy committed
137 138
	c, err := cid.Decode(txt)
	if err != nil {
139 140
		return "", err
	}
Jeromy's avatar
Jeromy committed
141 142

	return FromCid(c), nil
143
}
Jeromy's avatar
Jeromy committed
144

Hector Sanjuan's avatar
Hector Sanjuan committed
145
// IsValid checks if a path is a valid ipfs Path.
Jeromy's avatar
Jeromy committed
146 147 148 149
func (p *Path) IsValid() error {
	_, err := ParsePath(p.String())
	return err
}
150

Hector Sanjuan's avatar
Hector Sanjuan committed
151
// Join joins strings slices using /
152 153 154
func Join(pths []string) string {
	return strings.Join(pths, "/")
}
rht's avatar
rht committed
155

Hector Sanjuan's avatar
Hector Sanjuan committed
156
// SplitList splits strings usings /
rht's avatar
rht committed
157 158 159
func SplitList(pth string) []string {
	return strings.Split(pth, "/")
}
160 161 162

// SplitAbsPath clean up and split fpath. It extracts the first component (which
// must be a Multihash) and return it separately.
163
func SplitAbsPath(fpath Path) (cid.Cid, []string, error) {
164
	parts := fpath.Segments()
Łukasz Magiera's avatar
Łukasz Magiera committed
165
	if parts[0] == "ipfs" || parts[0] == "ipld" {
166 167 168 169 170
		parts = parts[1:]
	}

	// if nothing, bail.
	if len(parts) == 0 {
171
		return cid.Cid{}, nil, ErrNoComponents
172 173 174 175 176
	}

	c, err := cid.Decode(parts[0])
	// first element in the path is a cid
	if err != nil {
177
		return cid.Cid{}, nil, err
178 179 180 181
	}

	return c, parts[1:], nil
}