|  | 
|  | 1 | +package system | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"context" | 
|  | 5 | +	"io" | 
|  | 6 | +	"io/fs" | 
|  | 7 | +	"log/slog" | 
|  | 8 | +	"runtime" | 
|  | 9 | +	"time" | 
|  | 10 | + | 
|  | 11 | +	"github.com/ipfs/boxo/files" | 
|  | 12 | +	"github.com/ipfs/boxo/path" | 
|  | 13 | +	iface "github.com/ipfs/kubo/core/coreiface" | 
|  | 14 | +	"github.com/pkg/errors" | 
|  | 15 | +) | 
|  | 16 | + | 
|  | 17 | +var _ fs.FS = (*IPFS)(nil) | 
|  | 18 | + | 
|  | 19 | +// An IPFS provides access to a hierarchical file system. | 
|  | 20 | +// | 
|  | 21 | +// The IPFS interface is the minimum implementation required of the file system. | 
|  | 22 | +// A file system may implement additional interfaces, | 
|  | 23 | +// such as [ReadFileFS], to provide additional or optimized functionality. | 
|  | 24 | +// | 
|  | 25 | +// [testing/fstest.TestFS] may be used to test implementations of an IPFS for | 
|  | 26 | +// correctness. | 
|  | 27 | +type IPFS struct { | 
|  | 28 | +	Ctx  context.Context | 
|  | 29 | +	Root path.Path | 
|  | 30 | +	Unix iface.UnixfsAPI | 
|  | 31 | +} | 
|  | 32 | + | 
|  | 33 | +// Open opens the named file. | 
|  | 34 | +// | 
|  | 35 | +// When Open returns an error, it should be of type *PathError | 
|  | 36 | +// with the Op field set to "open", the Path field set to name, | 
|  | 37 | +// and the Err field describing the problem. | 
|  | 38 | +// | 
|  | 39 | +// Open should reject attempts to open names that do not satisfy | 
|  | 40 | +// fs.ValidPath(name), returning a *fs.PathError with Err set to | 
|  | 41 | +// fs.ErrInvalid or fs.ErrNotExist. | 
|  | 42 | +func (f IPFS) Open(name string) (fs.File, error) { | 
|  | 43 | +	path, node, err := f.Resolve(f.Ctx, name) | 
|  | 44 | +	if err != nil { | 
|  | 45 | +		return nil, &fs.PathError{ | 
|  | 46 | +			Op:   "open", | 
|  | 47 | +			Path: name, | 
|  | 48 | +			Err:  err, | 
|  | 49 | +		} | 
|  | 50 | +	} | 
|  | 51 | + | 
|  | 52 | +	return &ipfsNode{ | 
|  | 53 | +		Path: path, | 
|  | 54 | +		Node: node, | 
|  | 55 | +	}, nil | 
|  | 56 | +} | 
|  | 57 | + | 
|  | 58 | +func (f IPFS) Resolve(ctx context.Context, name string) (path.Path, files.Node, error) { | 
|  | 59 | +	if pathInvalid(name) { | 
|  | 60 | +		return nil, nil, fs.ErrInvalid | 
|  | 61 | +	} | 
|  | 62 | + | 
|  | 63 | +	p, err := path.Join(f.Root, name) | 
|  | 64 | +	if err != nil { | 
|  | 65 | +		return nil, nil, err | 
|  | 66 | +	} | 
|  | 67 | + | 
|  | 68 | +	node, err := f.Unix.Get(ctx, p) | 
|  | 69 | +	return p, node, err | 
|  | 70 | +} | 
|  | 71 | + | 
|  | 72 | +func pathInvalid(name string) bool { | 
|  | 73 | +	return !fs.ValidPath(name) | 
|  | 74 | +} | 
|  | 75 | + | 
|  | 76 | +func (f IPFS) Sub(dir string) (fs.FS, error) { | 
|  | 77 | +	var root path.Path | 
|  | 78 | +	var err error | 
|  | 79 | +	if (f == IPFS{}) { | 
|  | 80 | +		root, err = path.NewPath(dir) | 
|  | 81 | +	} else { | 
|  | 82 | +		root, err = path.Join(f.Root, dir) | 
|  | 83 | +	} | 
|  | 84 | + | 
|  | 85 | +	return &IPFS{ | 
|  | 86 | +		Ctx:  f.Ctx, | 
|  | 87 | +		Root: root, | 
|  | 88 | +		Unix: f.Unix, | 
|  | 89 | +	}, err | 
|  | 90 | +} | 
|  | 91 | + | 
|  | 92 | +var ( | 
|  | 93 | +	_ fs.FileInfo    = (*ipfsNode)(nil) | 
|  | 94 | +	_ fs.ReadDirFile = (*ipfsNode)(nil) | 
|  | 95 | +	_ fs.DirEntry    = (*ipfsNode)(nil) | 
|  | 96 | +) | 
|  | 97 | + | 
|  | 98 | +// ipfsNode provides access to a single file. The fs.File interface is the minimum | 
|  | 99 | +// implementation required of the file. Directory files should also implement [ReadDirFile]. | 
|  | 100 | +// A file may implement io.ReaderAt or io.Seeker as optimizations. | 
|  | 101 | +type ipfsNode struct { | 
|  | 102 | +	Path path.Path | 
|  | 103 | +	files.Node | 
|  | 104 | +} | 
|  | 105 | + | 
|  | 106 | +// base name of the file | 
|  | 107 | +func (n ipfsNode) Name() string { | 
|  | 108 | +	segs := n.Path.Segments() | 
|  | 109 | +	return segs[len(segs)-1] // last segment is name | 
|  | 110 | +} | 
|  | 111 | + | 
|  | 112 | +func (n *ipfsNode) Stat() (fs.FileInfo, error) { | 
|  | 113 | +	return n, nil | 
|  | 114 | +} | 
|  | 115 | + | 
|  | 116 | +// length in bytes for regular files; system-dependent for others | 
|  | 117 | +func (n ipfsNode) Size() int64 { | 
|  | 118 | +	size, err := n.Node.Size() | 
|  | 119 | +	if err != nil { | 
|  | 120 | +		slog.Error("failed to obtain file size", | 
|  | 121 | +			"path", n.Path, | 
|  | 122 | +			"reason", err) | 
|  | 123 | +	} | 
|  | 124 | + | 
|  | 125 | +	return size | 
|  | 126 | +} | 
|  | 127 | + | 
|  | 128 | +// file mode bits | 
|  | 129 | +func (n ipfsNode) Mode() fs.FileMode { | 
|  | 130 | +	switch n.Node.(type) { | 
|  | 131 | +	case files.Directory: | 
|  | 132 | +		return fs.ModeDir | 
|  | 133 | +	default: | 
|  | 134 | +		return 0 // regular read-only file | 
|  | 135 | +	} | 
|  | 136 | +} | 
|  | 137 | + | 
|  | 138 | +// modification time | 
|  | 139 | +func (n ipfsNode) ModTime() time.Time { | 
|  | 140 | +	return time.Time{} // zero-value time | 
|  | 141 | +} | 
|  | 142 | + | 
|  | 143 | +// abbreviation for Mode().IsDir() | 
|  | 144 | +func (n ipfsNode) IsDir() bool { | 
|  | 145 | +	return n.Mode().IsDir() | 
|  | 146 | +} | 
|  | 147 | + | 
|  | 148 | +// underlying data source (never returns nil) | 
|  | 149 | +func (n ipfsNode) Sys() any { | 
|  | 150 | +	return n.Node | 
|  | 151 | +} | 
|  | 152 | + | 
|  | 153 | +func (n ipfsNode) Read(b []byte) (int, error) { | 
|  | 154 | +	switch node := n.Node.(type) { | 
|  | 155 | +	case io.Reader: | 
|  | 156 | +		return node.Read(b) | 
|  | 157 | +	default: | 
|  | 158 | +		return 0, errors.New("unreadable node") | 
|  | 159 | +	} | 
|  | 160 | +} | 
|  | 161 | + | 
|  | 162 | +// ReadDir reads the contents of the directory and returns | 
|  | 163 | +// a slice of up to max DirEntry values in directory order. | 
|  | 164 | +// Subsequent calls on the same file will yield further DirEntry values. | 
|  | 165 | +// | 
|  | 166 | +// If max > 0, ReadDir returns at most max DirEntry structures. | 
|  | 167 | +// In this case, if ReadDir returns an empty slice, it will return | 
|  | 168 | +// a non-nil error explaining why. | 
|  | 169 | +// At the end of a directory, the error is io.EOF. | 
|  | 170 | +// (ReadDir must return io.EOF itself, not an error wrapping io.EOF.) | 
|  | 171 | +// | 
|  | 172 | +// If max <= 0, ReadDir returns all the DirEntry values from the directory | 
|  | 173 | +// in a single slice. In this case, if ReadDir succeeds (reads all the way | 
|  | 174 | +// to the end of the directory), it returns the slice and a nil error. | 
|  | 175 | +// If it encounters an error before the end of the directory, | 
|  | 176 | +// ReadDir returns the DirEntry list read until that point and a non-nil error. | 
|  | 177 | +func (n ipfsNode) ReadDir(max int) (entries []fs.DirEntry, err error) { | 
|  | 178 | +	root, ok := n.Node.(files.Directory) | 
|  | 179 | +	if !ok { | 
|  | 180 | +		return nil, errors.New("not a directory") | 
|  | 181 | +	} | 
|  | 182 | + | 
|  | 183 | +	iter := root.Entries() | 
|  | 184 | +	for iter.Next() { | 
|  | 185 | +		name := iter.Name() | 
|  | 186 | +		node := iter.Node() | 
|  | 187 | + | 
|  | 188 | +		// Callers will typically discard entries if they get a non-nill | 
|  | 189 | +		// error, so we make sure nodes are eventually closed. | 
|  | 190 | +		runtime.SetFinalizer(node, func(c io.Closer) { | 
|  | 191 | +			if err := c.Close(); err != nil { | 
|  | 192 | +				slog.Warn("unable to close node", | 
|  | 193 | +					"name", name, | 
|  | 194 | +					"reason", err) | 
|  | 195 | +			} | 
|  | 196 | +		}) | 
|  | 197 | + | 
|  | 198 | +		var subpath path.Path | 
|  | 199 | +		if subpath, err = path.Join(n.Path, name); err != nil { | 
|  | 200 | +			return | 
|  | 201 | +		} | 
|  | 202 | + | 
|  | 203 | +		entries = append(entries, &ipfsNode{ | 
|  | 204 | +			Path: subpath, | 
|  | 205 | +			Node: node}) | 
|  | 206 | + | 
|  | 207 | +		// got max items? | 
|  | 208 | +		if max--; max == 0 { | 
|  | 209 | +			return | 
|  | 210 | +		} | 
|  | 211 | +	} | 
|  | 212 | + | 
|  | 213 | +	// If we get here, it's because the iterator stopped.  It either | 
|  | 214 | +	// failed or is exhausted. Any other error has already caused us | 
|  | 215 | +	// to return. | 
|  | 216 | +	if iter.Err() != nil { | 
|  | 217 | +		err = iter.Err() // failed | 
|  | 218 | +	} else if max >= 0 { | 
|  | 219 | +		err = io.EOF // exhausted | 
|  | 220 | +	} | 
|  | 221 | + | 
|  | 222 | +	return | 
|  | 223 | +} | 
|  | 224 | + | 
|  | 225 | +// Info returns the FileInfo for the file or subdirectory described by the entry. | 
|  | 226 | +// The returned FileInfo may be from the time of the original directory read | 
|  | 227 | +// or from the time of the call to Info. If the file has been removed or renamed | 
|  | 228 | +// since the directory read, Info may return an error satisfying errors.Is(err, ErrNotExist). | 
|  | 229 | +// If the entry denotes a symbolic link, Info reports the information about the link itself, | 
|  | 230 | +// not the link's target. | 
|  | 231 | +func (n *ipfsNode) Info() (fs.FileInfo, error) { | 
|  | 232 | +	return n, nil | 
|  | 233 | +} | 
|  | 234 | + | 
|  | 235 | +// Type returns the type bits for the entry. | 
|  | 236 | +// The type bits are a subset of the usual FileMode bits, those returned by the FileMode.Type method. | 
|  | 237 | +func (n ipfsNode) Type() fs.FileMode { | 
|  | 238 | +	if n.Mode().IsDir() { | 
|  | 239 | +		return fs.ModeDir | 
|  | 240 | +	} | 
|  | 241 | + | 
|  | 242 | +	return 0 | 
|  | 243 | +} | 
|  | 244 | + | 
|  | 245 | +func (n ipfsNode) Write(b []byte) (int, error) { | 
|  | 246 | +	dst, ok := n.Node.(io.Writer) | 
|  | 247 | +	if ok { | 
|  | 248 | +		return dst.Write(b) | 
|  | 249 | +	} | 
|  | 250 | + | 
|  | 251 | +	return 0, errors.New("not writeable") | 
|  | 252 | +} | 
0 commit comments