Skip to content

Commit

Permalink
feat: Support UnixFS mode and modification times in ipld dag and mfs (#…
Browse files Browse the repository at this point in the history
…658)

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

Adds support for storing and retrieving file mode and last modification time.

Support added to:
- Files
- LinkFiles
- Webfiles
- Directories

Tar archives are supported by the parent branch.
  • Loading branch information
gammazero authored Aug 13, 2024
1 parent 0338750 commit 1062062
Show file tree
Hide file tree
Showing 17 changed files with 1,192 additions and 94 deletions.
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 @@ type ufsDirectory struct {
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) Entries() files.DirIterator {
}

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 @@ func (d *ufsDirectory) Size() (int64, error) {

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 @@ func newUnixfsDir(ctx context.Context, dserv ipld.DAGService, nd *dag.ProtoNode,
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 @@ import (
// | 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 @@ func Layout(db *h.DagBuilderHelper) (ipld.Node, error) {
}
}

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 @@ import (
"errors"
"io"
"os"
"time"

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

Expand All @@ -23,13 +24,15 @@ var ErrMissingFsRef = errors.New("missing file path or URL, can't create filesto
// 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 @@ type DagBuilderParams struct {
// 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 @@ type DagBuilderParams struct {
// 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 @@ func (db *DagBuilderHelper) GetCidBuilder() cid.Builder {
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 @@ func (db *DagBuilderHelper) NewLeafNode(data []byte, fsNodeType pb.Data_DataType
// 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 @@ func (db *DagBuilderHelper) NewLeafNode(data []byte, fsNodeType pb.Data_DataType
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 @@ func (db *DagBuilderHelper) Maxlinks() int {
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 @@ type FSNodeOverDag struct {
}

// 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 @@ func (n *FSNodeOverDag) SetFileData(fileData []byte) {
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

0 comments on commit 1062062

Please sign in to comment.