Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support UnixFS mode and modification times in ipld dag and mfs #658

Merged
merged 5 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 19 additions & 28 deletions ipld/unixfs/file/unixfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
dserv ipld.DAGService
dir uio.Directory
size int64
stat os.FileInfo
mode os.FileMode
mtime time.Time
}

type ufsIterator struct {
Expand Down Expand Up @@ -122,17 +123,11 @@
}

func (d *ufsDirectory) Mode() os.FileMode {
if d.stat == nil {
return 0
}
return d.stat.Mode()
return d.mode
}

func (d *ufsDirectory) ModTime() time.Time {
if d.stat == nil {
return time.Time{}
}
return d.stat.ModTime()
return d.mtime
}

func (d *ufsDirectory) Size() (int64, error) {
Expand All @@ -141,28 +136,21 @@

type ufsFile struct {
uio.DagReader
stat os.FileInfo
}

func (f *ufsFile) Mode() os.FileMode {
if f.stat == nil {
return 0
}
return f.stat.Mode()
return f.DagReader.Mode()
}

func (f *ufsFile) ModTime() time.Time {
if f.stat == nil {
return time.Time{}
}
return f.stat.ModTime()
return f.DagReader.ModTime()
}

func (f *ufsFile) Size() (int64, error) {
return int64(f.DagReader.Size()), nil
}

func newUnixfsDir(ctx context.Context, dserv ipld.DAGService, nd *dag.ProtoNode, stat os.FileInfo) (files.Directory, error) {
func newUnixfsDir(ctx context.Context, dserv ipld.DAGService, nd *dag.ProtoNode) (files.Directory, error) {
dir, err := uio.NewDirectoryFromNode(dserv, nd)
if err != nil {
return nil, err
Expand All @@ -173,32 +161,35 @@
return nil, err
}

fsn, err := ft.FSNodeFromBytes(nd.Data())
if err != nil {
return nil, err
}

Check warning on line 167 in ipld/unixfs/file/unixfile.go

View check run for this annotation

Codecov / codecov/patch

ipld/unixfs/file/unixfile.go#L166-L167

Added lines #L166 - L167 were not covered by tests

return &ufsDirectory{
ctx: ctx,
dserv: dserv,

dir: dir,
size: int64(size),
stat: stat,
dir: dir,
size: int64(size),
mode: fsn.Mode(),
mtime: fsn.ModTime(),
}, nil
}

func NewUnixfsFile(ctx context.Context, dserv ipld.DAGService, nd ipld.Node) (files.Node, error) {
return NewUnixfsFileWithStat(ctx, dserv, nd, nil)
}

func NewUnixfsFileWithStat(ctx context.Context, dserv ipld.DAGService, nd ipld.Node, stat os.FileInfo) (files.Node, error) {
switch dn := nd.(type) {
case *dag.ProtoNode:
fsn, err := ft.FSNodeFromBytes(dn.Data())
if err != nil {
return nil, err
}

if fsn.IsDir() {
return newUnixfsDir(ctx, dserv, dn, stat)
return newUnixfsDir(ctx, dserv, dn)
}
if fsn.Type() == ft.TSymlink {
return files.NewLinkFile(string(fsn.Data()), stat), nil
return files.NewSymlinkFile(string(fsn.Data()), fsn.ModTime()), nil

Check warning on line 192 in ipld/unixfs/file/unixfile.go

View check run for this annotation

Codecov / codecov/patch

ipld/unixfs/file/unixfile.go#L192

Added line #L192 was not covered by tests
}

case *dag.RawNode:
Expand Down
48 changes: 48 additions & 0 deletions ipld/unixfs/importer/balanced/balanced_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
mrand "math/rand"
"testing"
"time"

h "github.com/ipfs/boxo/ipld/unixfs/importer/helpers"
uio "github.com/ipfs/boxo/ipld/unixfs/io"
Expand All @@ -26,6 +27,10 @@ func buildTestDag(ds ipld.DAGService, spl chunker.Splitter) (*dag.ProtoNode, err
Maxlinks: h.DefaultLinksPerBlock,
}

return buildTestDagWithParams(spl, dbp)
}

func buildTestDagWithParams(spl chunker.Splitter, dbp h.DagBuilderParams) (*dag.ProtoNode, error) {
db, err := dbp.New(spl)
if err != nil {
return nil, err
Expand Down Expand Up @@ -335,3 +340,46 @@ func TestSeekingConsistency(t *testing.T) {
t.Fatal(err)
}
}

func TestMetadataNoData(t *testing.T) {
testMetadata(t, new(bytes.Buffer))
}

func TestMetadata(t *testing.T) {
nbytes := 3 * chunker.DefaultBlockSize
buf := new(bytes.Buffer)
_, err := io.CopyN(buf, random.NewRand(), nbytes)
if err != nil {
t.Fatal(err)
}

testMetadata(t, buf)
}

func testMetadata(t *testing.T, buf *bytes.Buffer) {
dagserv := mdtest.Mock()
dbp := h.DagBuilderParams{
Dagserv: dagserv,
Maxlinks: h.DefaultLinksPerBlock,
FileMode: 0522,
FileModTime: time.Unix(1638111600, 76552),
}

nd, err := buildTestDagWithParams(chunker.DefaultSplitter(buf), dbp)
if err != nil {
t.Fatal(err)
}

dr, err := uio.NewDagReader(context.Background(), nd, dagserv)
if err != nil {
t.Fatal(err)
}

if !dr.ModTime().Equal(dbp.FileModTime) {
t.Errorf("got modtime %v, wanted %v", dr.ModTime(), dbp.FileModTime)
}

if dr.Mode() != dbp.FileMode {
t.Errorf("got filemode %o, wanted %o", dr.Mode(), dbp.FileMode)
}
}
29 changes: 22 additions & 7 deletions ipld/unixfs/importer/balanced/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,33 @@
// | Chunk 1 | | Chunk 2 | | Chunk 3 |
// +=========+ +=========+ + - - - - +
func Layout(db *h.DagBuilderHelper) (ipld.Node, error) {
var root ipld.Node
var err error

if db.Done() {
// No data, return just an empty node.
root, err := db.NewLeafNode(nil, ft.TFile)
if err != nil {
return nil, err
}
// No data, just create an empty node.
root, err = db.NewLeafNode(nil, ft.TFile)
// This works without Filestore support (`ProcessFileStore`).
// TODO: Why? Is there a test case missing?
} else {
root, err = layoutData(db)
}

if err != nil {
return nil, err
}

Check warning on line 147 in ipld/unixfs/importer/balanced/builder.go

View check run for this annotation

Codecov / codecov/patch

ipld/unixfs/importer/balanced/builder.go#L146-L147

Added lines #L146 - L147 were not covered by tests

return root, db.Add(root)
if db.HasFileAttributes() {
err = db.SetFileAttributes(root)
if err != nil {
return nil, err
}

Check warning on line 153 in ipld/unixfs/importer/balanced/builder.go

View check run for this annotation

Codecov / codecov/patch

ipld/unixfs/importer/balanced/builder.go#L152-L153

Added lines #L152 - L153 were not covered by tests
}

return root, db.Add(root)
}

func layoutData(db *h.DagBuilderHelper) (ipld.Node, error) {
// The first `root` will be a single leaf node with data
// (corner case), after that subsequent `root` nodes will
// always be internal nodes (with a depth > 0) that can
Expand Down Expand Up @@ -172,7 +187,7 @@
}
}

return root, db.Add(root)
return root, nil
}

// fillNodeRec will "fill" the given internal (non-leaf) `node` with data by
Expand Down
97 changes: 79 additions & 18 deletions ipld/unixfs/importer/helpers/dagbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"errors"
"io"
"os"
"time"

dag "github.com/ipfs/boxo/ipld/merkledag"

Expand All @@ -23,13 +24,15 @@
// DagBuilderHelper wraps together a bunch of objects needed to
// efficiently create unixfs dag trees
type DagBuilderHelper struct {
dserv ipld.DAGService
spl chunker.Splitter
recvdErr error
rawLeaves bool
nextData []byte // the next item to return.
maxlinks int
cidBuilder cid.Builder
dserv ipld.DAGService
spl chunker.Splitter
recvdErr error
rawLeaves bool
nextData []byte // the next item to return.
maxlinks int
cidBuilder cid.Builder
fileMode os.FileMode
fileModTime time.Time

// Filestore support variables.
// ----------------------------
Expand Down Expand Up @@ -62,6 +65,12 @@
// DAGService to write blocks to (required)
Dagserv ipld.DAGService

// The unixfs file mode
FileMode os.FileMode

// The unixfs last modified time
FileModTime time.Time

// NoCopy signals to the chunker that it should track fileinfo for
// filestore adds
NoCopy bool
Expand All @@ -71,11 +80,13 @@
// chunker.Splitter as data source.
func (dbp *DagBuilderParams) New(spl chunker.Splitter) (*DagBuilderHelper, error) {
db := &DagBuilderHelper{
dserv: dbp.Dagserv,
spl: spl,
rawLeaves: dbp.RawLeaves,
cidBuilder: dbp.CidBuilder,
maxlinks: dbp.Maxlinks,
dserv: dbp.Dagserv,
spl: spl,
rawLeaves: dbp.RawLeaves,
cidBuilder: dbp.CidBuilder,
maxlinks: dbp.Maxlinks,
fileMode: dbp.FileMode,
fileModTime: dbp.FileModTime,
}
if fi, ok := spl.Reader().(files.FileInfo); dbp.NoCopy && ok {
db.fullPath = fi.AbsPath()
Expand Down Expand Up @@ -138,9 +149,9 @@
return db.cidBuilder
}

// NewLeafNode creates a leaf node filled with data. If rawLeaves is
// defined then a raw leaf will be returned. Otherwise, it will create
// and return `FSNodeOverDag` with `fsNodeType`.
// NewLeafNode creates a leaf node filled with data. If rawLeaves is defined
// then a raw leaf will be returned. Otherwise, it will create and return
// `FSNodeOverDag` with `fsNodeType`.
func (db *DagBuilderHelper) NewLeafNode(data []byte, fsNodeType pb.Data_DataType) (ipld.Node, error) {
if len(data) > BlockSizeLimit {
return nil, ErrSizeLimitExceeded
Expand All @@ -161,6 +172,7 @@
// Encapsulate the data in UnixFS node (instead of a raw node).
fsNodeOverDag := db.NewFSNodeOverDag(fsNodeType)
fsNodeOverDag.SetFileData(data)

node, err := fsNodeOverDag.Commit()
if err != nil {
return nil, err
Expand All @@ -172,9 +184,10 @@
return node, nil
}

// FillNodeLayer will add datanodes as children to the give node until
// FillNodeLayer will add data-nodes as children to the given node until
// it is full in this layer or no more data.
// NOTE: This function creates raw data nodes so it only works
//
// NOTE: This function creates raw data nodes, so it only works
// for the `trickle.Layout`.
func (db *DagBuilderHelper) FillNodeLayer(node *FSNodeOverDag) error {
// while we have room AND we're not done
Expand Down Expand Up @@ -265,6 +278,34 @@
return db.maxlinks
}

// HasFileAttributes will return false if Filestore is being used,
// otherwise returns true if a file mode or last modification time is set.
func (db *DagBuilderHelper) HasFileAttributes() bool {
return db.fullPath == "" && (db.fileMode != 0 || !db.fileModTime.IsZero())
}

// SetFileAttributes stores file attributes present in the `DagBuilderHelper`
// into the associated `ft.FSNode`.
func (db *DagBuilderHelper) SetFileAttributes(n ipld.Node) error {
if pn, ok := n.(*dag.ProtoNode); ok {
fsn, err := ft.FSNodeFromBytes(pn.Data())
if err != nil {
return err
}

Check warning on line 294 in ipld/unixfs/importer/helpers/dagbuilder.go

View check run for this annotation

Codecov / codecov/patch

ipld/unixfs/importer/helpers/dagbuilder.go#L293-L294

Added lines #L293 - L294 were not covered by tests
fsn.SetModTime(db.fileModTime)
fsn.SetMode(db.fileMode)

d, err := fsn.GetBytes()
if err != nil {
return err
}

Check warning on line 301 in ipld/unixfs/importer/helpers/dagbuilder.go

View check run for this annotation

Codecov / codecov/patch

ipld/unixfs/importer/helpers/dagbuilder.go#L300-L301

Added lines #L300 - L301 were not covered by tests

pn.SetData(d)
}

return nil
}

// FSNodeOverDag encapsulates an `unixfs.FSNode` that will be stored in a
// `dag.ProtoNode`. Instead of just having a single `ipld.Node` that
// would need to be constantly (un)packed to access and modify its
Expand All @@ -288,7 +329,7 @@
}

// NewFSNodeOverDag creates a new `dag.ProtoNode` and `ft.FSNode`
// decoupled from one onther (and will continue in that way until
// decoupled from one anonther (and will continue in that way until
// `Commit` is called), with `fsNodeType` specifying the type of
// the UnixFS layer node (either `File` or `Raw`).
func (db *DagBuilderHelper) NewFSNodeOverDag(fsNodeType pb.Data_DataType) *FSNodeOverDag {
Expand Down Expand Up @@ -374,6 +415,26 @@
n.file.SetData(fileData)
}

// SetMode sets the file mode of the associated `ft.FSNode`.
func (n *FSNodeOverDag) SetMode(mode os.FileMode) {
n.file.SetMode(mode)

Check warning on line 420 in ipld/unixfs/importer/helpers/dagbuilder.go

View check run for this annotation

Codecov / codecov/patch

ipld/unixfs/importer/helpers/dagbuilder.go#L419-L420

Added lines #L419 - L420 were not covered by tests
}

// SetModTime sets the file modification time of the associated `ft.FSNode`.
func (n *FSNodeOverDag) SetModTime(ts time.Time) {
n.file.SetModTime(ts)
}

// Mode returns the file mode of the associated `ft.FSNode`
func (n *FSNodeOverDag) Mode() os.FileMode {
return n.file.Mode()

Check warning on line 430 in ipld/unixfs/importer/helpers/dagbuilder.go

View check run for this annotation

Codecov / codecov/patch

ipld/unixfs/importer/helpers/dagbuilder.go#L429-L430

Added lines #L429 - L430 were not covered by tests
}

// ModTime returns the last modification time of the associated `ft.FSNode`
func (n *FSNodeOverDag) ModTime() time.Time {
return n.file.ModTime()
}

// GetDagNode fills out the proper formatting for the FSNodeOverDag node
// inside of a DAG node and returns the dag node.
// TODO: Check if we have committed (passed the UnixFS information
Expand Down
Loading