|
| 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