diff --git a/internal/filesystem/ipfscore.go b/internal/filesystem/ipfscore.go index 2a0d4f92..2b66d5d0 100644 --- a/internal/filesystem/ipfscore.go +++ b/internal/filesystem/ipfscore.go @@ -17,6 +17,7 @@ import ( ipld "github.com/ipfs/go-ipld-format" // TODO: migrate to new standard dag "github.com/ipfs/go-merkledag" "github.com/ipfs/go-unixfs" + unixpb "github.com/ipfs/go-unixfs/pb" coreiface "github.com/ipfs/interface-go-ipfs-core" corepath "github.com/ipfs/interface-go-ipfs-core/path" ) @@ -24,10 +25,32 @@ import ( // TODO: move this to a test file var _ StreamDirFile = (*coreDirectory)(nil) +const ( + // TODO: reconsider if we want these in `filesystem` or not. + // We can probably just use more localy scoped consts + // like ipfsRootPerms = x|y|z + + executeAll = ExecuteUser | ExecuteGroup | ExecuteOther + writeAll = WriteUser | WriteGroup | WriteOther + readAll = ReadUser | ReadGroup | ReadOther + + // These haven't even been used yet. + + allOther = ReadOther | WriteOther | ExecuteOther + allGroup = ReadGroup | WriteGroup | ExecuteGroup + allUser = ReadUser | WriteUser | ExecuteUser +) + type ( - ipfsCoreAPI struct { + coreRootInfo struct { + // TODO: track Atime; + // m,c, and birth time can be the same as initTime. + initTime time.Time + mode fs.FileMode + } + coreFS struct { + rootInfo *coreRootInfo core coreiface.CoreAPI - root rootDirectory systemID ID } coreDirectory struct { @@ -36,6 +59,18 @@ type ( context.Context context.CancelFunc } + coreDirEntry struct { + coreiface.DirEntry + error + initTime time.Time + permissions fs.FileMode + } + coreFileInfo struct { + name string + size int64 + mode fs.FileMode + initTime time.Time + } coreFile struct { stat fs.FileInfo files.File @@ -54,20 +89,21 @@ type ( const ipfsCoreTimeout = 10 * time.Second -func NewIPFS(core coreiface.CoreAPI, systemID ID) *ipfsCoreAPI { - const permissions = readAll | executeAll - return &ipfsCoreAPI{ - root: newRoot(permissions, nil), +func NewIPFS(core coreiface.CoreAPI, systemID ID) *coreFS { + return &coreFS{ + rootInfo: &coreRootInfo{ + initTime: time.Now(), + }, core: core, systemID: systemID, } } -func (ci *ipfsCoreAPI) ID() ID { return ci.systemID } +func (ci *coreFS) ID() ID { return ci.systemID } -func (ci *ipfsCoreAPI) Open(name string) (fs.File, error) { +func (ci *coreFS) Open(name string) (fs.File, error) { if name == rootName { - return ci.root, nil + return ci.rootInfo, nil } if !fs.ValidPath(name) { return nil, @@ -96,15 +132,14 @@ func (ci *ipfsCoreAPI) Open(name string) (fs.File, error) { return ci.openNode(name, corePath, ipldNode) } -func (ci *ipfsCoreAPI) openNode(name string, +func (ci *coreFS) openNode(name string, corePath corepath.Path, ipldNode ipld.Node, ) (fs.File, error) { const ( op fserrors.Op = "ipfscore.openNode" defaultPermissions = readAll | executeAll ) - // stat, err := ci.stat(name, ipldNode) - defaultMtime := ci.root.stat.ModTime() + defaultMtime := ci.rootInfo.ModTime() stat, err := statNode(name, defaultMtime, defaultPermissions, ipldNode) if err != nil { return nil, fserrors.New(op, @@ -116,6 +151,7 @@ func (ci *ipfsCoreAPI) openNode(name string, var file fs.File switch stat.Mode().Type() { // TODO links, etc. case fs.FileMode(0): + // TODO: pass stat file, err = openIPFSFile(name, ci.core, ipldNode) case fs.ModeDir: dirAPI := ci.core.Unixfs() @@ -135,10 +171,10 @@ func (ci *ipfsCoreAPI) openNode(name string, return file, nil } -func (ci *ipfsCoreAPI) Stat(name string) (fs.FileInfo, error) { +func (ci *coreFS) Stat(name string) (fs.FileInfo, error) { const op fserrors.Op = "ipfscore.Stat" if name == rootName { - return ci.root.stat, nil + return ci.rootInfo, nil } corePath, err := goToIPFSCore(ci.systemID, name) @@ -155,8 +191,8 @@ func (ci *ipfsCoreAPI) Stat(name string) (fs.FileInfo, error) { } // TODO: fetch from somewhere else - modTime := ci.root.stat.ModTime() - permissions := ci.root.stat.Mode().Perm() + modTime := ci.rootInfo.ModTime() + permissions := ci.rootInfo.Mode().Perm() // stat, err := statNode(name, modTime, permissions, ipldNode) @@ -175,6 +211,24 @@ func (ci *ipfsCoreAPI) Stat(name string) (fs.FileInfo, error) { return stat, nil } +func (*coreRootInfo) Name() string { return rootName } +func (*coreRootInfo) Size() int64 { return 0 } +func (cr *coreRootInfo) Mode() fs.FileMode { return cr.mode } +func (cr *coreRootInfo) ModTime() time.Time { return cr.initTime } +func (cr *coreRootInfo) IsDir() bool { return cr.Mode().IsDir() } +func (cr *coreRootInfo) Sys() any { return cr } + +func (cr *coreRootInfo) Stat() (fs.FileInfo, error) { + return cr, nil +} + +func (*coreRootInfo) Read([]byte) (int, error) { + const op fserrors.Op = "root.Read" + return -1, fserrors.New(op, fserrors.IsDir) +} + +func (*coreRootInfo) Close() error { return nil } + func (cd *coreDirectory) Stat() (fs.FileInfo, error) { return cd.stat, nil } func (*coreDirectory) Read([]byte) (int, error) { @@ -209,12 +263,31 @@ func (cd *coreDirectory) ReadDir(count int) ([]fs.DirEntry, error) { if returnAll { return entries, nil } + // FIXME: we only want to return EOF /after/ we hit it. + // I.e. if we have 2 entries, and a count of 100, + // we reuturn ([2]{a,b}, nil) + // Only if we're called again, will we return (nil, EOF) + /* Standard does this: + n := len(d.entry) - d.offset + if n == 0 && count > 0 { + return nil, io.EOF + } + if count > 0 && n > count { + n = count + } + list := make([]fs.DirEntry, n) + for i := range list { + list[i] = &d.entry[d.offset+i] + } + d.offset += n + return list, nil + */ return entries, io.EOF } if err := coreEntry.Err; err != nil { return entries, err } - entries = append(entries, translateCoreEntry(&coreEntry)) + entries = append(entries, &coreDirEntry{DirEntry: coreEntry}) if !returnAll { if count--; count == 0 { return entries, nil @@ -226,22 +299,31 @@ func (cd *coreDirectory) ReadDir(count int) ([]fs.DirEntry, error) { } } -func translateCoreEntry(entry *coreiface.DirEntry) fs.DirEntry { - // TODO: this typing is kind of weird. - // It should at least have an xEntry alias instead of xStat. Maybe. - return &staticStat{ - name: entry.Name, - size: int64(entry.Size), - mode: coreTypeToGoType(entry.Type) | - readAll | executeAll, // TODO: from root. - modTime: time.Now(), // TODO: from root. +func (cde *coreDirEntry) Name() string { return cde.DirEntry.Name } +func (cde *coreDirEntry) IsDir() bool { return cde.Type().IsDir() } +func (cde *coreDirEntry) Info() (fs.FileInfo, error) { return cde, nil } +func (cde *coreDirEntry) Size() int64 { return int64(cde.DirEntry.Size) } +func (cde *coreDirEntry) ModTime() time.Time { return cde.initTime } +func (cde *coreDirEntry) Mode() fs.FileMode { return cde.Type() | cde.permissions } +func (cde *coreDirEntry) Sys() any { return cde } +func (cde *coreDirEntry) Error() error { return cde.error } +func (cde *coreDirEntry) Type() fs.FileMode { + switch cde.DirEntry.Type { + case coreiface.TDirectory: + return fs.ModeDir + case coreiface.TFile: + return fs.FileMode(0) + case coreiface.TSymlink: + return fs.ModeSymlink + default: + return fs.ModeIrregular } } -func (cd *coreDirectory) StreamDir(ctx context.Context) <-chan DirStreamEntry { +func (cd *coreDirectory) StreamDir(ctx context.Context) <-chan StreamDirEntry { var ( coreEntries = cd.entries - goEntries = make(chan DirStreamEntry, cap(coreEntries)) + goEntries = make(chan StreamDirEntry, cap(coreEntries)) ) go func() { defer close(goEntries) @@ -252,7 +334,7 @@ func (cd *coreDirectory) StreamDir(ctx context.Context) <-chan DirStreamEntry { const op fserrors.Op = "coreDirectory.StreamDir" err := fserrors.New(op, fserrors.IO) // TODO: error value for E-not-open? select { - case goEntries <- newErrorEntry(err): + case goEntries <- &coreDirEntry{error: err}: case <-ctx.Done(): } }() @@ -261,14 +343,14 @@ func (cd *coreDirectory) StreamDir(ctx context.Context) <-chan DirStreamEntry { func translateCoreEntries(ctx context.Context, coreEntries <-chan coreiface.DirEntry, - goEntries chan<- DirStreamEntry, + goEntries chan<- StreamDirEntry, ) { for coreEntry := range coreEntries { - var entry DirStreamEntry + var entry StreamDirEntry if err := coreEntry.Err; err != nil { - entry = newErrorEntry(err) + entry = &coreDirEntry{error: err} } else { - entry = wrapDirEntry(translateCoreEntry(&coreEntry)) + entry = &coreDirEntry{DirEntry: coreEntry} } select { case goEntries <- entry: @@ -293,6 +375,13 @@ func (cd *coreDirectory) Close() error { ) } +func (cfi *coreFileInfo) Name() string { return cfi.name } +func (cfi *coreFileInfo) Size() int64 { return cfi.size } +func (cfi *coreFileInfo) Mode() fs.FileMode { return cfi.mode } +func (cfi *coreFileInfo) ModTime() time.Time { return cfi.initTime } +func (cfi *coreFileInfo) IsDir() bool { return cfi.Mode().IsDir() } +func (cfi *coreFileInfo) Sys() any { return cfi } + // TODO: [port] // func (cio *coreFile) Write(_ []byte) (int, error) { return 0, errReadOnly } // func (cio *coreFile) Truncate(_ uint64) error { return errReadOnly } @@ -413,12 +502,28 @@ func openUFSNode(name string, core coreiface.CoreAPI, ipldNode ipld.Node, } // TODO: store/get permissions from root. const permissions = readAll | executeAll + // TODO: store permissions on the type and do this in the method for Mode() + mode := func() fs.FileMode { + mode := permissions + switch ufsNode.Type() { + case unixpb.Data_Directory, unixpb.Data_HAMTShard: + mode |= fs.ModeDir + case unixpb.Data_Symlink: + mode |= fs.ModeSymlink + case unixpb.Data_File, unixpb.Data_Raw: + // NOOP: mode |= fs.FileMode(0) + default: + mode |= fs.ModeIrregular + } + return mode + }() return &coreFile{ - stat: staticStat{ - name: name, - size: int64(ufsNode.FileSize()), - mode: unixfsTypeToGoType(ufsNode.Type()) | permissions, - modTime: time.Now(), // TODO: from root + stat: &coreFileInfo{ + name: name, + size: int64(ufsNode.FileSize()), + // mode: unixfsTypeToGoType(ufsNode.Type()) | permissions, + mode: mode, + initTime: time.Now(), // TODO: from root }, File: fileNode, cancel: cancel, @@ -441,19 +546,3 @@ func openCborNode(cborNode *cbor.Node, return &cborFile{node: cborNode, reader: br}, nil } - -func coreTypeToGoType(typ coreiface.FileType) fs.FileMode { - switch typ { - case coreiface.TDirectory: - return fs.ModeDir - case coreiface.TFile: - return fs.FileMode(0) - case coreiface.TSymlink: - return fs.ModeSymlink - default: - panic(fmt.Errorf( - "mode: stat contains unexpected type: %v", - typ, - )) - } -} diff --git a/internal/filesystem/ipfskeys.go b/internal/filesystem/ipfskeys.go index 7d378425..269788a0 100644 --- a/internal/filesystem/ipfskeys.go +++ b/internal/filesystem/ipfskeys.go @@ -15,25 +15,30 @@ import ( ) type ( - IPFSKeyAPI struct { + IPFSKeyFS struct { keyAPI coreiface.KeyAPI ipns fs.FS } keyDirectory struct { + mode fs.FileMode ipns fs.FS - stat fs.FileInfo cancel context.CancelFunc getKeys func() ([]coreiface.Key, error) cursor int } keyDirEntry struct { + permissions fs.FileMode coreiface.Key ipns fs.FS } + keyInfo struct { // TODO: roll into keyDirEntry? + name string + mode fs.FileMode // Without the type, this is only really useful for move+delete permissions. + } ) -func NewKeyFS(core coreiface.KeyAPI, options ...KeyfsOption) *IPFSKeyAPI { - fs := &IPFSKeyAPI{keyAPI: core} +func NewKeyFS(core coreiface.KeyAPI, options ...KeyfsOption) *IPFSKeyFS { + fs := &IPFSKeyFS{keyAPI: core} for _, setter := range options { if err := setter(fs); err != nil { panic(err) @@ -42,11 +47,11 @@ func NewKeyFS(core coreiface.KeyAPI, options ...KeyfsOption) *IPFSKeyAPI { return fs } -func (*IPFSKeyAPI) ID() ID { return IPFSKeys } -func (*IPFSKeyAPI) Close() error { return nil } // TODO: close everything +func (*IPFSKeyFS) ID() ID { return IPFSKeys } +func (*IPFSKeyFS) Close() error { return nil } // TODO: close everything // TODO: probably inefficient. Review. -func (ki *IPFSKeyAPI) translateName(name string) (string, error) { +func (ki *IPFSKeyFS) translateName(name string) (string, error) { keys, err := ki.keyAPI.List(context.Background()) if err != nil { return "", err @@ -66,7 +71,7 @@ func (ki *IPFSKeyAPI) translateName(name string) (string, error) { return keyName, nil } -func (kfs *IPFSKeyAPI) Open(name string) (fs.File, error) { +func (kfs *IPFSKeyFS) Open(name string) (fs.File, error) { const op = "open" if name == rootName { return kfs.openRoot() @@ -90,7 +95,7 @@ func (kfs *IPFSKeyAPI) Open(name string) (fs.File, error) { } } -func (kfs *IPFSKeyAPI) openRoot() (fs.ReadDirFile, error) { +func (kfs *IPFSKeyFS) openRoot() (fs.ReadDirFile, error) { var ( ctx, cancel = context.WithCancel(context.Background()) keys []coreiface.Key @@ -103,26 +108,29 @@ func (kfs *IPFSKeyAPI) openRoot() (fs.ReadDirFile, error) { return keys, err } ) - const permissions = readAll | executeAll + const permissions = readAll | executeAll // TODO: from ctor; writes will be valid eventually. return &keyDirectory{ - ipns: kfs.ipns, - stat: staticStat{ - name: rootName, - mode: fs.ModeDir | permissions, - modTime: time.Now(), // Not really modified, but key-set as-of right now. - }, + mode: fs.ModeDir | permissions, + ipns: kfs.ipns, cancel: cancel, getKeys: lazyKeys, }, nil } -func (kd *keyDirectory) Stat() (fs.FileInfo, error) { return kd.stat, nil } - func (*keyDirectory) Read([]byte) (int, error) { const op fserrors.Op = "keyDirectory.Read" return -1, fserrors.New(op, fserrors.IsDir) } +func (kd *keyDirectory) Stat() (fs.FileInfo, error) { return kd, nil } + +func (*keyDirectory) Name() string { return rootName } +func (*keyDirectory) Size() int64 { return 0 } +func (kd *keyDirectory) Mode() fs.FileMode { return kd.mode } +func (kd *keyDirectory) ModTime() time.Time { return time.Now() } // TODO: is there any way the node can tell us when the last publish was? +func (kd *keyDirectory) IsDir() bool { return kd.Mode().IsDir() } +func (kd *keyDirectory) Sys() any { return kd } + func (kd *keyDirectory) ReadDir(count int) ([]fs.DirEntry, error) { const op fserrors.Op = "keyDirectory.ReadDir" var ( @@ -148,8 +156,9 @@ func (kd *keyDirectory) ReadDir(count int) ([]fs.DirEntry, error) { break } ents = append(ents, &keyDirEntry{ - Key: key, - ipns: kd.ipns, + permissions: kd.mode.Perm(), + Key: key, + ipns: kd.ipns, }) count-- } @@ -185,10 +194,9 @@ func (ke *keyDirEntry) Info() (fs.FileInfo, error) { if subsys := ke.ipns; subsys != nil { return fs.Stat(subsys, pathWithoutNamespace(ke.Key)) } - return staticStat{ - name: ke.Key.Name(), - mode: fs.ModeIrregular, - modTime: time.Now(), + return &keyInfo{ + name: ke.Key.Name(), + mode: fs.ModeIrregular | ke.permissions, }, nil } @@ -201,3 +209,10 @@ func (ke *keyDirEntry) Type() fs.FileMode { } func (ke *keyDirEntry) IsDir() bool { return ke.Type()&fs.ModeDir != 0 } + +func (ki *keyInfo) Name() string { return ki.name } +func (*keyInfo) Size() int64 { return 0 } // Unknown without IPNS subsystem. +func (ki *keyInfo) Mode() fs.FileMode { return ki.mode } +func (ki *keyInfo) ModTime() time.Time { return time.Now() } // TODO: is there any way the node can tell us when the last publish was? +func (ki *keyInfo) IsDir() bool { return ki.Mode().IsDir() } +func (ki *keyInfo) Sys() any { return ki } diff --git a/internal/filesystem/ipfsmfs.go b/internal/filesystem/ipfsmfs.go index 1b949850..889bd33b 100644 --- a/internal/filesystem/ipfsmfs.go +++ b/internal/filesystem/ipfsmfs.go @@ -25,12 +25,12 @@ import ( ) // TODO: move this to a test file -var _ IDFS = (*IPFSMFS)(nil) +var _ IDFS = (*IPFSMFSFS)(nil) // TODO: _ StreamDirFile = (*mfsDirectory)(nil) type ( - IPFSMFS struct { + IPFSMFSFS struct { creationTime time.Time mroot *mfs.Root } @@ -39,7 +39,15 @@ type ( cancel context.CancelFunc listing <-chan unixfs.LinkResult mfsDir *mfs.Directory - stat *staticStat + // stat *staticStat + stat *coreFileInfo // TODO: mfsSpecific or IPFS-shared type + } + mfsDirEntry struct { + parent *mfs.Directory + link *ipld.Link + // TODO: update these value as appropriate + modTime time.Time + permissions fs.FileMode } mfsFile struct { descriptor mfs.FileDescriptor @@ -47,12 +55,12 @@ type ( } mfsStat struct { sizeFn func() (int64, error) - staticStat + coreFileInfo } ) func NewMFS(mroot *mfs.Root) fs.FS { - return &IPFSMFS{ + return &IPFSMFSFS{ creationTime: time.Now(), // TODO: take in metadata from options. mroot: mroot, } @@ -78,8 +86,8 @@ func CidToMFSRoot(ctx context.Context, root cid.Cid, core coreiface.CoreAPI, pub return mfs.NewRoot(ctx, core.Dag(), pbNode, publish) } -func (mi *IPFSMFS) ID() ID { return MFS } -func (mi *IPFSMFS) Close() error { return mi.mroot.Close() } +func (mi *IPFSMFSFS) ID() ID { return MFS } +func (mi *IPFSMFSFS) Close() error { return mi.mroot.Close() } /* TODO func (mi *IPFSMFS) Rename(oldName, newName string) error { @@ -94,7 +102,7 @@ func (mi *IPFSMFS) Rename(oldName, newName string) error { } */ -func (mi *IPFSMFS) Open(name string) (fs.File, error) { +func (mi *IPFSMFSFS) Open(name string) (fs.File, error) { if name == rootName { return mi.openRoot() } @@ -122,7 +130,7 @@ func (mi *IPFSMFS) Open(name string) (fs.File, error) { return file, nil } -func (mi *IPFSMFS) openNode(name string) (mfs.FSNode, error) { +func (mi *IPFSMFSFS) openNode(name string) (mfs.FSNode, error) { const op fserrors.Op = "mfs.openNode" if !fs.ValidPath(name) { return nil, fserrors.New(op, fserrors.InvalidItem) // TODO: convert old-style errors. @@ -144,7 +152,7 @@ func (mi *IPFSMFS) openNode(name string) (mfs.FSNode, error) { return mfsNode, nil } -func (mi *IPFSMFS) openFileNode(name string, mfsNode mfs.FSNode, flag int) (fs.File, error) { +func (mi *IPFSMFSFS) openFileNode(name string, mfsNode mfs.FSNode, flag int) (fs.File, error) { mfsFileIntf, ok := mfsNode.(*mfs.File) if !ok { // TODO: error value. @@ -172,16 +180,16 @@ func (mi *IPFSMFS) openFileNode(name string, mfsNode mfs.FSNode, flag int) (fs.F descriptor: descriptor, stat: &mfsStat{ // TODO: retrieve metadata from node if present; timestamps from constructor otherwise. sizeFn: mfsFileIntf.Size, - staticStat: staticStat{ - name: path.Base(name), - mode: permissions, - modTime: time.Now(), + coreFileInfo: coreFileInfo{ // TODO: type should be mfs-specific or ipfs-shared + name: path.Base(name), + mode: permissions, + initTime: time.Now(), }, }, }, nil } -func (mi *IPFSMFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { +func (mi *IPFSMFSFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { const op fserrors.Op = "mfs.OpenFile" if name == rootName { return nil, &fs.PathError{ @@ -209,7 +217,7 @@ func (mi *IPFSMFS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, e return file, nil } -func (mi *IPFSMFS) openRoot() (fs.File, error) { +func (mi *IPFSMFSFS) openRoot() (fs.File, error) { mfsNode, err := mfs.Lookup(mi.mroot, "/") if err != nil { return nil, err @@ -217,7 +225,7 @@ func (mi *IPFSMFS) openRoot() (fs.File, error) { return mi.openDirNode("/", mfsNode) } -func (mi *IPFSMFS) openDirNode(name string, mfsNode mfs.FSNode) (fs.ReadDirFile, error) { +func (mi *IPFSMFSFS) openDirNode(name string, mfsNode mfs.FSNode) (fs.ReadDirFile, error) { const op fserrors.Op = "mfs.openDirNode" mfsDir, isDir := mfsNode.(*mfs.Directory) if !isDir { @@ -229,10 +237,10 @@ func (mi *IPFSMFS) openDirNode(name string, mfsNode mfs.FSNode) (fs.ReadDirFile, ctx: ctx, cancel: cancel, listing: _hackListAsync(ctx, mfsDir), mfsDir: mfsDir, - stat: &staticStat{ // TODO: retrieve metadata from node if present; timestamps from constructor otherwise. - name: path.Base(name), - mode: fs.ModeDir | permissions, - modTime: time.Now(), + stat: &coreFileInfo{ // TODO: retrieve metadata from node if present; timestamps from constructor otherwise. + name: path.Base(name), + mode: fs.ModeDir | permissions, + initTime: time.Now(), }, }, nil } @@ -258,6 +266,41 @@ func (*mfsDirectory) Read([]byte) (int, error) { return -1, fserrors.New(op, fserrors.IsDir) } +func (mde *mfsDirEntry) getNode() (mfs.FSNode, error) { return mde.parent.Child(mde.link.Name) } +func (mde *mfsDirEntry) Name() string { return mde.link.Name } +func (mde *mfsDirEntry) IsDir() bool { return mde.Type().IsDir() } +func (mde *mfsDirEntry) Size() int64 { + child, err := mde.getNode() + if err != nil { + return 0 + } + if file, ok := child.(*mfs.File); ok { + size, _ := file.Size() + return size + } + return 0 +} +func (mde *mfsDirEntry) ModTime() time.Time { return mde.modTime } +func (mde *mfsDirEntry) Mode() fs.FileMode { return mde.Type() | mde.permissions } +func (mde *mfsDirEntry) Sys() any { return mde } +func (mde *mfsDirEntry) Info() (fs.FileInfo, error) { + // FIXME: We should call methods that can return errors here. + // The info methods just return blanks for that case, + // but the caller could find out why here instead. + return mde, nil +} + +func (mde *mfsDirEntry) Type() fs.FileMode { + child, err := mde.getNode() + if err != nil { + return fs.ModeIrregular + } + if child.Type() == mfs.TDir { + return fs.ModeDir + } + return fs.FileMode(0) +} + func (md *mfsDirectory) ReadDir(count int) ([]fs.DirEntry, error) { const op fserrors.Op = "mfsDirectory.ReadDir" var ( @@ -291,11 +334,12 @@ func (md *mfsDirectory) ReadDir(count int) ([]fs.DirEntry, error) { if err := link.Err; err != nil { return entries, err } - entry, err := translateUFSLinkEntry(parent, link.Link) - if err != nil { - return entries, err - } - entries = append(entries, entry) + entries = append(entries, &mfsDirEntry{ + parent: parent, + link: link.Link, + modTime: time.Now(), // TODO: from node if applicable. + permissions: md.stat.mode.Perm(), // TODO: from node if applicable. + }) if !returnAll { if count--; count == 0 { return entries, nil @@ -305,45 +349,6 @@ func (md *mfsDirectory) ReadDir(count int) ([]fs.DirEntry, error) { } } -func translateUFSLinkEntry(parent *mfs.Directory, link *ipld.Link) (fs.DirEntry, error) { - name := link.Name - child, err := parent.Child(name) - if err != nil { - return nil, err - } - /* - TODO: Symlinks are not currently supported by go-mfs / the IPFS Files API. - But we used to support them in our mount logic and encountered no problems. - We'll have to port over the code for special file types. - ipldNode, _ := mfsNode.GetNode() - ufsNode, _ := unixfs.ExtractFSNode(ipldNode) - nodeType := ufsNode.Type() - if nodeType == unixfs.TSymlink { - typ = fs.ModeSymlink - } - */ - const permissions = readAll | writeAll | executeAll // TODO: perms from caller or node if present. - var ( - mode fs.FileMode - size int64 - ) - if child.Type() == mfs.TDir { - mode |= fs.ModeDir - } else { - if file, ok := child.(*mfs.File); ok { - if size, err = file.Size(); err != nil { - return nil, err - } - } - } - return &staticStat{ - name: name, - size: size, - mode: mode | permissions, - modTime: time.Now(), // TODO: time from somewhere else - }, nil -} - func (md *mfsDirectory) Close() error { const op fserrors.Op = "mfsDirectory.Close" if cancel := md.cancel; cancel != nil { @@ -372,7 +377,7 @@ func (mio *mfsFile) Seek(offset int64, whence int) (int64, error) { func (mio *mfsFile) Stat() (fs.FileInfo, error) { return mio.stat, nil } // TODO: quick ports below. -func (mi *IPFSMFS) Make(name string) error { +func (mi *IPFSMFSFS) Make(name string) error { parentPath, childName := path.Split(name) parentNode, err := mfs.Lookup(mi.mroot, parentPath) if err != nil { @@ -399,7 +404,7 @@ func (mi *IPFSMFS) Make(name string) error { return nil } -func (mi *IPFSMFS) MakeDirectory(path string) error { +func (mi *IPFSMFSFS) MakeDirectory(path string) error { if err := mfs.Mkdir(mi.mroot, path, mfs.MkdirOpts{Flush: true}); err != nil { if errors.Is(err, os.ErrExist) { return fserrors.New(fserrors.Exist) @@ -409,7 +414,7 @@ func (mi *IPFSMFS) MakeDirectory(path string) error { return nil } -func (mi *IPFSMFS) MakeLink(name, linkTarget string) error { +func (mi *IPFSMFSFS) MakeLink(name, linkTarget string) error { parentPath, linkName := path.Split(name) parentNode, err := mfs.Lookup(mi.mroot, parentPath) if err != nil { diff --git a/internal/filesystem/ipfspins.go b/internal/filesystem/ipfspins.go index d93c5d19..3eb44a2a 100644 --- a/internal/filesystem/ipfspins.go +++ b/internal/filesystem/ipfspins.go @@ -13,20 +13,21 @@ import ( ) var ( // TODO: move this to a test file - _ IDFS = (*IPFSPinAPI)(nil) - _ StreamDirFile = (*pinStream)(nil) + _ IDFS = (*IPFSPinFS)(nil) + _ StreamDirFile = (*pinDirectory)(nil) // TODO: // _ POSIXInfo = (*pinDirEntry)(nil) ) type ( - IPFSPinAPI struct { + IPFSPinFS struct { pinAPI coreiface.PinAPI ipfs fs.FS // TODO: subsys should be handled via `bind` instead? fs.Subsys? } - pinStream struct { - stat fs.FileInfo - pins <-chan coreiface.Pin + pinDirectory struct { + mode fs.FileMode + modTime time.Time + pins <-chan coreiface.Pin context.Context context.CancelFunc ipfs fs.FS @@ -35,10 +36,15 @@ type ( coreiface.Pin ipfs fs.FS // TODO: replace this with statfunc or something. We shouldn't need the whole FS. } + pinInfo struct { // TODO: roll into pinDirEntry? + name string + mode fs.FileMode // Without the type, this is only really useful for move+delete permissions. + accessed time.Time + } ) -func NewPinFS(pinAPI coreiface.PinAPI, options ...PinfsOption) *IPFSPinAPI { - fs := &IPFSPinAPI{pinAPI: pinAPI} +func NewPinFS(pinAPI coreiface.PinAPI, options ...PinfsOption) *IPFSPinFS { + fs := &IPFSPinFS{pinAPI: pinAPI} for _, setter := range options { if err := setter(fs); err != nil { panic(err) @@ -47,9 +53,9 @@ func NewPinFS(pinAPI coreiface.PinAPI, options ...PinfsOption) *IPFSPinAPI { return fs } -func (*IPFSPinAPI) ID() ID { return IPFSPins } +func (*IPFSPinFS) ID() ID { return IPFSPins } -func (pfs *IPFSPinAPI) Open(name string) (fs.File, error) { +func (pfs *IPFSPinFS) Open(name string) (fs.File, error) { const op = "open" if name == rootName { return pfs.openRoot() @@ -64,7 +70,7 @@ func (pfs *IPFSPinAPI) Open(name string) (fs.File, error) { } } -func (pfs *IPFSPinAPI) openRoot() (fs.ReadDirFile, error) { +func (pfs *IPFSPinFS) openRoot() (fs.ReadDirFile, error) { const op fserrors.Op = "pinfs.openRoot" var ( ctx, cancel = context.WithCancel(context.Background()) @@ -85,29 +91,32 @@ func (pfs *IPFSPinAPI) openRoot() (fs.ReadDirFile, error) { } // TODO: retrieve permission from somewhere else. (Passed into FS constructor) const permissions = readAll | executeAll - stream := &pinStream{ + stream := &pinDirectory{ + mode: fs.ModeDir | permissions, + modTime: time.Now(), ipfs: pfs.ipfs, pins: pins, Context: ctx, CancelFunc: cancel, - stat: staticStat{ - name: rootName, - mode: fs.ModeDir | permissions, - modTime: time.Now(), // Not really modified, but pin-set as-of right now. - }, } return stream, nil } -func (ps *pinStream) Stat() (fs.FileInfo, error) { return ps.stat, nil } +func (*pinDirectory) Name() string { return rootName } +func (*pinDirectory) Size() int64 { return 0 } +func (ps *pinDirectory) Stat() (fs.FileInfo, error) { return ps, nil } +func (ps *pinDirectory) Mode() fs.FileMode { return ps.mode } +func (ps *pinDirectory) ModTime() time.Time { return ps.modTime } +func (ps *pinDirectory) IsDir() bool { return ps.Mode().IsDir() } +func (ps *pinDirectory) Sys() any { return ps } -func (*pinStream) Read([]byte) (int, error) { +func (*pinDirectory) Read([]byte) (int, error) { const op fserrors.Op = "pinStream.Read" return -1, fserrors.New(op, fserrors.IsDir) } // TODO: also implement StreamDirFile -func (ps *pinStream) ReadDir(count int) ([]fs.DirEntry, error) { +func (ps *pinDirectory) ReadDir(count int) ([]fs.DirEntry, error) { const op fserrors.Op = "pinStream.ReadDir" var ( ctx = ps.Context @@ -156,11 +165,11 @@ func translatePinEntry(pin coreiface.Pin, ipfs fs.FS) fs.DirEntry { } } -func (ps *pinStream) StreamDir(ctx context.Context) <-chan DirStreamEntry { +func (ps *pinDirectory) StreamDir(ctx context.Context) <-chan StreamDirEntry { var ( pins = ps.pins ipfs = ps.ipfs - entries = make(chan DirStreamEntry, cap(pins)) + entries = make(chan StreamDirEntry, cap(pins)) ) go func() { defer close(entries) @@ -171,7 +180,7 @@ func (ps *pinStream) StreamDir(ctx context.Context) <-chan DirStreamEntry { const op fserrors.Op = "pinStream.StreamDir" err := fserrors.New(op, fserrors.IO) // TODO: error value for E-not-open? select { - case entries <- newErrorEntry(err): + case entries <- dirEntryWrapper{error: err}: // TODO: type case <-ctx.Done(): } }() @@ -180,15 +189,15 @@ func (ps *pinStream) StreamDir(ctx context.Context) <-chan DirStreamEntry { func translatePinEntries(ctx context.Context, pins <-chan coreiface.Pin, - entries chan<- DirStreamEntry, + entries chan<- StreamDirEntry, ipfs fs.FS, ) { for pin := range pins { - var entry DirStreamEntry + var entry StreamDirEntry if err := pin.Err(); err != nil { - entry = newErrorEntry(err) + entry = dirEntryWrapper{error: err} // TODO: type } else { - entry = wrapDirEntry(translatePinEntry(pin, ipfs)) + entry = dirEntryWrapper{DirEntry: translatePinEntry(pin, ipfs)} // TODO: type } select { case entries <- entry: @@ -198,7 +207,7 @@ func translatePinEntries(ctx context.Context, } } -func (ps *pinStream) Close() error { +func (ps *pinDirectory) Close() error { const op fserrors.Op = "pinStream.Close" if cancel := ps.CancelFunc; cancel != nil { cancel() @@ -224,10 +233,10 @@ func (pe *pinDirEntry) Info() (fs.FileInfo, error) { } // TODO: permission come from somewhere else. const permissions = readAll | executeAll - return staticStat{ - name: pinCid.String(), - mode: fs.ModeDir | permissions, - modTime: time.Now(), + return &pinInfo{ + name: pinCid.String(), + mode: fs.ModeDir | permissions, + accessed: time.Now(), }, nil } @@ -240,3 +249,10 @@ func (pe *pinDirEntry) Type() fs.FileMode { } func (pe *pinDirEntry) IsDir() bool { return pe.Type().IsDir() } + +func (pi *pinInfo) Name() string { return pi.name } +func (*pinInfo) Size() int64 { return 0 } // Unknown without IPFS subsystem. +func (pi *pinInfo) Mode() fs.FileMode { return pi.mode } +func (pi *pinInfo) ModTime() time.Time { return pi.accessed } +func (pi *pinInfo) IsDir() bool { return pi.Mode().IsDir() } +func (pi *pinInfo) Sys() any { return pi } diff --git a/internal/filesystem/ipfsutil.go b/internal/filesystem/ipfsutil.go index 17290b8a..075ce5d1 100644 --- a/internal/filesystem/ipfsutil.go +++ b/internal/filesystem/ipfsutil.go @@ -13,6 +13,21 @@ import ( corepath "github.com/ipfs/interface-go-ipfs-core/path" ) +type ( + unixFSInfo struct { + name string + permissions fs.FileMode + modtime time.Time + *unixfs.FSNode + } + ipldNodeInfo struct { + name string + permissions fs.FileMode + modtime time.Time + ipld.Node + } +) + func goToIPFSCore(fsid ID, goPath string) (corepath.Path, error) { return corepath.New( path.Join("/", @@ -48,51 +63,51 @@ func statNode(name string, modtime time.Time, permissions fs.FileMode, if err != nil { return nil, err } - return unixFSAttr(name, modtime, permissions, ufsNode) + return &unixFSInfo{ + name: name, + permissions: permissions, + modtime: modtime, + FSNode: ufsNode, + }, nil } // *dag.RawNode, *cbor.Node - return genericAttr(name, modtime, permissions, ipldNode) -} - -func genericAttr(name string, modtime time.Time, permissions fs.FileMode, - genericNode ipld.Node, -) (fs.FileInfo, error) { - // raw nodes only contain data so we'll treat them as a flat file - // cbor nodes are not currently supported via UnixFS so we assume them to contain only data - // TODO: review ^ is there some way we can implement this that won't blow up in the future? - // (if unixfs supports cbor and directories are implemented to use them ) - nodeStat, err := genericNode.Stat() - if err != nil { - return nil, err - } - return staticStat{ - size: int64(nodeStat.CumulativeSize), - name: name, - mode: permissions, - modTime: modtime, - }, nil -} - -func unixFSAttr(name string, modtime time.Time, permissions fs.FileMode, - ufsNode *unixfs.FSNode, -) (fs.FileInfo, error) { - return staticStat{ - name: name, - size: int64(ufsNode.FileSize()), - mode: unixfsTypeToGoType(ufsNode.Type()) | permissions, - modTime: modtime, // TODO: from UFS when v2 lands. + return &ipldNodeInfo{ + name: name, + permissions: permissions, + modtime: modtime, + Node: ipldNode, }, nil } -func unixfsTypeToGoType(ut unixpb.Data_DataType) fs.FileMode { - switch ut { +func (ufi *unixFSInfo) Name() string { return ufi.name } +func (ufi *unixFSInfo) Size() int64 { return int64(ufi.FSNode.FileSize()) } +func (ufi *unixFSInfo) ModTime() time.Time { return ufi.modtime } +func (ufi *unixFSInfo) IsDir() bool { return ufi.Mode().IsDir() } +func (ufi *unixFSInfo) Sys() any { return ufi } +func (ufi *unixFSInfo) Mode() fs.FileMode { + mode := ufi.permissions + switch ufi.FSNode.Type() { case unixpb.Data_Directory, unixpb.Data_HAMTShard: - return fs.ModeDir + mode |= fs.ModeDir case unixpb.Data_Symlink: - return fs.ModeSymlink + mode |= fs.ModeSymlink case unixpb.Data_File, unixpb.Data_Raw: - return fs.FileMode(0) + // NOOP: mode |= fs.FileMode(0) default: - return fs.ModeIrregular + mode |= fs.ModeIrregular + } + return mode +} + +func (idi *ipldNodeInfo) Name() string { return idi.name } +func (idi *ipldNodeInfo) Size() int64 { + nodeStat, err := idi.Node.Stat() + if err != nil { + return 0 } + return int64(nodeStat.CumulativeSize) } +func (idi *ipldNodeInfo) Mode() fs.FileMode { return idi.permissions } +func (idi *ipldNodeInfo) ModTime() time.Time { return idi.modtime } +func (idi *ipldNodeInfo) IsDir() bool { return idi.Mode().IsDir() } +func (idi *ipldNodeInfo) Sys() any { return idi } diff --git a/internal/filesystem/options.go b/internal/filesystem/options.go index cb7dad6a..ee5a1bb6 100644 --- a/internal/filesystem/options.go +++ b/internal/filesystem/options.go @@ -7,8 +7,8 @@ type ( PinfsOption | KeyfsOption } - PinfsOption func(*IPFSPinAPI) error - KeyfsOption func(*IPFSKeyAPI) error + PinfsOption func(*IPFSPinFS) error + KeyfsOption func(*IPFSKeyFS) error ) // TODO: try to eliminate this form of generic options. @@ -25,7 +25,7 @@ type ( func WithIPFS[OT IPFSOption](ipfs fs.FS) (option OT) { switch fnPtrPtr := any(&option).(type) { case *PinfsOption: - *fnPtrPtr = func(pa *IPFSPinAPI) error { pa.ipfs = ipfs; return nil } + *fnPtrPtr = func(pa *IPFSPinFS) error { pa.ipfs = ipfs; return nil } } return option } @@ -33,7 +33,7 @@ func WithIPFS[OT IPFSOption](ipfs fs.FS) (option OT) { func WithIPNS[OT IPFSOption](ipns fs.FS) (option OT) { switch fnPtrPtr := any(&option).(type) { case *KeyfsOption: - *fnPtrPtr = func(ka *IPFSKeyAPI) error { ka.ipns = ipns; return nil } + *fnPtrPtr = func(ka *IPFSKeyFS) error { ka.ipns = ipns; return nil } } return option }