From 78af06d397745a1d062f36f70e5660b9bee2d825 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Mon, 23 Dec 2019 09:57:27 +0000 Subject: [PATCH 1/4] feat: store time as timespec To allow high resolution `mtime`, store time as `TimeSpec` message which contains seconds and nanosecond fragments. Also allows passing `mtime` and `mode` in multiple formats, fixes coverage npm script and increases module test coverage. --- .gitignore | 1 + README.md | 2 +- package.json | 6 +- src/index.js | 109 ++++++++++++++++-- src/unixfs.proto.js | 7 +- test/unixfs-format.spec.js | 221 +++++++++++++++++++++++++++++++++++-- 6 files changed, 321 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index c3c6b061..3da8ee83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ docs package-lock.json yarn.lock +.nyc_output # Logs logs diff --git a/README.md b/README.md index 5e7a445a..a36e89c3 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ const data = new UnixFS([options]) - data (Buffer): The optional data field for this node - blockSizes (Array, default: `[]`): If this is a `file` node that is made up of multiple blocks, `blockSizes` is a list numbers that represent the size of the file chunks stored in each child node. It is used to calculate the total file size. - mode (Number, default `0644` for files, `0755` for directories/hamt-sharded-directories) file mode -- mtime (Date, default `0`): The modification time of this node +- mtime (Date, { secs, nsecs }, { EpochSeconds, EpochNanoseconds }, [ secs, nsecs ], default { secs: 0 }): The modification time of this node #### add and remove a block size to the block size list diff --git a/package.json b/package.json index f00d3084..c9c3551e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "release": "aegir release", "release-minor": "aegir release --type minor", "release-major": "aegir release --type major", - "coverage": "aegir coverage" + "coverage": "nyc -s aegir test -t node && nyc report --reporter=html" }, "repository": { "type": "git", @@ -38,9 +38,11 @@ "devDependencies": { "aegir": "^20.4.1", "chai": "^4.2.0", - "dirty-chai": "^2.0.1" + "dirty-chai": "^2.0.1", + "nyc": "^15.0.0" }, "dependencies": { + "err-code": "^2.0.0", "protons": "^1.1.0" }, "contributors": [ diff --git a/src/index.js b/src/index.js index 6331bdc1..6ca3069a 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ const protons = require('protons') const pb = protons(require('./unixfs.proto')) const unixfsData = pb.Data +const errcode = require('err-code') const types = [ 'raw', @@ -45,6 +46,73 @@ function parseArgs (args) { return args[0] } +function parseMtime (mtime) { + if (mtime == null) { + return undefined + } + + // Javascript Date + if (mtime instanceof Date) { + const ms = mtime.getTime() + const secs = Math.floor(ms / 1000) + + return { + secs: secs, + nsecs: (ms - (secs * 1000)) * 1000 + } + } + + // { secs, nsecs } + if (Object.prototype.hasOwnProperty.call(mtime, 'secs')) { + return { + secs: mtime.secs, + nsecs: mtime.nsecs + } + } + + // UnixFS TimeSpec + if (Object.prototype.hasOwnProperty.call(mtime, 'EpochSeconds')) { + return { + secs: mtime.EpochSeconds, + nsecs: mtime.EpochNanoseconds + } + } + + // process.hrtime() + if (Array.isArray(mtime)) { + return { + secs: mtime[0], + nsecs: mtime[1] + } + } + /* + TODO: https://github.com/ipfs/aegir/issues/487 + + // process.hrtime.bigint() + if (typeof mtime === 'bigint') { + const secs = mtime / BigInt(1e9) + const nsecs = mtime - (secs * BigInt(1e9)) + + return { + secs: parseInt(secs), + nsecs: parseInt(nsecs) + } + } + */ +} + +function parseMode (mode) { + if (mode == null) { + return undefined + } + + if (typeof mode === 'string' || mode instanceof String) { + mode = parseInt(mode, 8) + } + + return mode & 0xFFF +} + class Data { // decode from protobuf https://github.com/ipfs/specs/blob/master/UNIXFS.md static unmarshal (marshaled) { @@ -55,7 +123,7 @@ class Data { data: decoded.hasData() ? decoded.Data : undefined, blockSizes: decoded.blocksizes, mode: decoded.hasMode() ? decoded.mode : undefined, - mtime: decoded.hasMtime() ? new Date(decoded.mtime * 1000) : undefined + mtime: decoded.hasMtime() ? decoded.mtime : undefined }) } @@ -71,7 +139,7 @@ class Data { } = parseArgs(args) if (!types.includes(type)) { - throw new Error('Type: ' + type + ' is not valid') + throw errcode(new Error('Type: ' + type + ' is not valid'), 'ERR_INVALID_TYPE') } this.type = type @@ -79,10 +147,14 @@ class Data { this.hashType = hashType this.fanout = fanout this.blockSizes = blockSizes || [] - this.mtime = mtime || new Date(0) - this.mode = mode || mode === 0 ? (mode & 0xFFF) : undefined this._originalMode = mode + const parsedMode = parseMode(mode) + + if (parsedMode !== undefined) { + this.mode = parsedMode + } + if (this.mode === undefined && type === 'file') { this.mode = DEFAULT_FILE_MODE } @@ -90,6 +162,14 @@ class Data { if (this.mode === undefined && this.isDirectory()) { this.mode = DEFAULT_DIRECTORY_MODE } + + const parsedMtime = parseMtime(mtime) + + if (parsedMtime) { + this.mtime = parsedMtime + } else { + this.mtime = { secs: 0, nsecs: 0 } + } } isDirectory () { @@ -135,7 +215,7 @@ class Data { case 'symlink': type = unixfsData.DataType.Symlink; break case 'hamt-sharded-directory': type = unixfsData.DataType.HAMTShard; break default: - throw new Error(`Unkown type: "${this.type}"`) + throw errcode(new Error('Type: ' + type + ' is not valid'), 'ERR_INVALID_TYPE') } let data = this.data @@ -152,8 +232,8 @@ class Data { let mode - if (this.mode || this.mode === 0) { - mode = (this._originalMode & 0xFFFFF000) | (this.mode & 0xFFF) + if (this.mode != null) { + mode = (this._originalMode & 0xFFFFF000) | parseMode(this.mode) if (mode === DEFAULT_FILE_MODE && this.type === 'file') { mode = undefined @@ -166,11 +246,18 @@ class Data { let mtime - if (this.mtime) { - mtime = Math.round(this.mtime.getTime() / 1000) + if (this.mtime != null) { + const parsed = parseMtime(this.mtime) + + if (parsed) { + mtime = { + EpochSeconds: parsed.secs, + EpochNanoseconds: parsed.nsecs + } - if (mtime === 0) { - mtime = undefined + if (parsed.secs === 0 && !parsed.nsecs) { + mtime = undefined + } } } diff --git a/src/unixfs.proto.js b/src/unixfs.proto.js index fcc8931d..b3850da9 100644 --- a/src/unixfs.proto.js +++ b/src/unixfs.proto.js @@ -20,7 +20,12 @@ message Data { optional uint64 hashType = 5; optional uint64 fanout = 6; optional uint32 mode = 7; - optional int64 mtime = 8; + optional TimeSpec mtime = 8; +} + +message TimeSpec { + required int64 EpochSeconds = 1; + optional uint32 EpochNanoseconds = 2; } message Metadata { diff --git a/test/unixfs-format.spec.js b/test/unixfs-format.spec.js index 6190fbf4..db0ffbd0 100644 --- a/test/unixfs-format.spec.js +++ b/test/unixfs-format.spec.js @@ -17,6 +17,20 @@ const protons = require('protons') const unixfsData = protons(require('../src/unixfs.proto')).Data describe('unixfs-format', () => { + it('old style constructor', () => { + const buf = Buffer.from('hello world') + const entry = new UnixFS('file', buf) + + expect(entry.type).to.equal('file') + expect(entry.data).to.deep.equal(buf) + }) + + it('old style constructor with single argument', () => { + const entry = new UnixFS('file') + + expect(entry.type).to.equal('file') + }) + it('defaults to file', () => { const data = new UnixFS() expect(data.type).to.equal('file') @@ -116,6 +130,30 @@ describe('unixfs-format', () => { expect(UnixFS.unmarshal(data.marshal())).to.have.property('mode', mode) }) + it('default mode for files', () => { + const data = new UnixFS({ + type: 'file' + }) + + expect(UnixFS.unmarshal(data.marshal())).to.have.property('mode', parseInt('0644', 8)) + }) + + it('default mode for directories', () => { + const data = new UnixFS({ + type: 'directory' + }) + + expect(UnixFS.unmarshal(data.marshal())).to.have.property('mode', parseInt('0755', 8)) + }) + + it('default mode for hamt shards', () => { + const data = new UnixFS({ + type: 'hamt-sharded-directory' + }) + + expect(UnixFS.unmarshal(data.marshal())).to.have.property('mode', parseInt('0755', 8)) + }) + it('sets mode to 0', () => { const mode = 0 const data = new UnixFS({ @@ -126,8 +164,29 @@ describe('unixfs-format', () => { expect(UnixFS.unmarshal(data.marshal())).to.have.property('mode', mode) }) + it('mode as string', () => { + const data = new UnixFS({ + type: 'file', + mode: '0555' + }) + + expect(UnixFS.unmarshal(data.marshal())).to.have.property('mode', parseInt('0555', 8)) + }) + + it('mode as string set outside constructor', () => { + const data = new UnixFS({ + type: 'file' + }) + data.mode = '0555' + + expect(UnixFS.unmarshal(data.marshal())).to.have.property('mode', parseInt('0555', 8)) + }) + it('mtime', () => { - const mtime = new Date() + const mtime = { + secs: 5, + nsecs: 0 + } const data = new UnixFS({ type: 'file', mtime @@ -135,16 +194,127 @@ describe('unixfs-format', () => { const marshaled = data.marshal() const unmarshaled = UnixFS.unmarshal(marshaled) - expect(unmarshaled.mtime).to.deep.equal(new Date(Math.round(mtime.getTime() / 1000) * 1000)) + expect(unmarshaled).to.have.deep.property('mtime', mtime) + }) + + it('default mtime', () => { + const data = new UnixFS({ + type: 'file' + }) + + const marshaled = data.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + expect(unmarshaled).to.have.deep.property('mtime', { secs: 0, nsecs: 0 }) }) + it('mtime as date', () => { + const mtime = { + secs: 5, + nsecs: 0 + } + const data = new UnixFS({ + type: 'file', + mtime: new Date(5000) + }) + + const marshaled = data.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + expect(unmarshaled).to.have.deep.property('mtime', mtime) + }) + + it('mtime as hrtime', () => { + const mtime = { + secs: 5, + nsecs: 0 + } + const data = new UnixFS({ + type: 'file', + mtime: [5, 0] + }) + + const marshaled = data.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + expect(unmarshaled).to.have.deep.property('mtime', mtime) + }) + /* + TODO: https://github.com/ipfs/aegir/issues/487 + + it('mtime as bigint', () => { + const mtime = { + secs: 5, + nsecs: 0 + } + const data = new UnixFS({ + type: 'file', + mtime: BigInt(5 * 1e9) + }) + + const marshaled = data.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + expect(unmarshaled).to.have.deep.property('mtime', mtime) + }) + */ it('sets mtime to 0', () => { - const mtime = new Date(0) + const mtime = { + secs: 0, + nsecs: 0 + } const data = new UnixFS({ type: 'file', mtime }) - expect(UnixFS.unmarshal(data.marshal())).to.have.deep.property('mtime', new Date(Math.round(mtime.getTime() / 1000) * 1000)) + + const marshaled = data.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + expect(unmarshaled).to.have.deep.property('mtime', mtime) + }) + + it('mtime as date set outside constructor', () => { + const mtime = { + secs: 5, + nsecs: 0 + } + const data = new UnixFS({ + type: 'file' + }) + data.mtime = new Date(5000) + + const marshaled = data.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + expect(unmarshaled).to.have.deep.property('mtime', mtime) + }) + + it('ignores invalid mtime', () => { + const data = new UnixFS({ + type: 'file', + mtime: 'what is this' + }) + + const marshaled = data.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + expect(unmarshaled).to.have.deep.property('mtime', { secs: 0, nsecs: 0 }) + }) + + it('ignores invalid mtime set outside of constructor', () => { + const entry = new UnixFS({ + type: 'file' + }) + entry.mtime = 'what is this' + + const marshaled = entry.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + expect(unmarshaled).to.have.deep.property('mtime', { secs: 0, nsecs: 0 }) + }) + + it('survies null mtime', () => { + const entry = new UnixFS({ + type: 'file' + }) + entry.mtime = null + + const marshaled = entry.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + expect(unmarshaled).to.have.deep.property('mtime', { secs: 0, nsecs: 0 }) }) it('does not overwrite unknown mode bits', () => { @@ -162,7 +332,16 @@ describe('unixfs-format', () => { }) // figuring out what is this metadata for https://github.com/ipfs/js-ipfs-data-importing/issues/3#issuecomment-182336526 - it.skip('metadata', () => {}) + it('metadata', () => { + const entry = new UnixFS({ + type: 'metadata' + }) + + const marshaled = entry.marshal() + const unmarshaled = UnixFS.unmarshal(marshaled) + + expect(unmarshaled).to.have.property('type', 'metadata') + }) it('symlink', () => { const data = new UnixFS({ @@ -175,15 +354,37 @@ describe('unixfs-format', () => { expect(data.blockSizes).to.deep.equal(unmarshaled.blockSizes) expect(data.fileSize()).to.deep.equal(unmarshaled.fileSize()) }) - it('wrong type', (done) => { - let data + + it('invalid type', (done) => { try { - data = new UnixFS({ + // eslint-disable-next-line no-new + new UnixFS({ type: 'bananas' }) } catch (err) { - expect(err).to.exist() - expect(data).to.not.exist() + expect(err).to.have.property('code', 'ERR_INVALID_TYPE') + done() + } + }) + + it('invalid type with old style constructor', (done) => { + try { + // eslint-disable-next-line no-new + new UnixFS('bananas') + } catch (err) { + expect(err).to.have.property('code', 'ERR_INVALID_TYPE') + done() + } + }) + + it('invalid type set outside constructor', (done) => { + const entry = new UnixFS() + entry.type = 'bananas' + + try { + entry.marshal() + } catch (err) { + expect(err).to.have.property('code', 'ERR_INVALID_TYPE') done() } }) From 9e79ec1a0d5602337b8fa2e274dbdf73de7b959a Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 2 Jan 2020 08:33:30 +0100 Subject: [PATCH 2/4] fix: enforce nsec value range Allow 0-999.. but only marshal 1-999.. --- src/index.js | 45 ++++++++++++++++++++++++++++++--------------- src/unixfs.proto.js | 2 +- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/index.js b/src/index.js index 6ca3069a..fa6658cc 100644 --- a/src/index.js +++ b/src/index.js @@ -51,20 +51,9 @@ function parseMtime (mtime) { return undefined } - // Javascript Date - if (mtime instanceof Date) { - const ms = mtime.getTime() - const secs = Math.floor(ms / 1000) - - return { - secs: secs, - nsecs: (ms - (secs * 1000)) * 1000 - } - } - // { secs, nsecs } if (Object.prototype.hasOwnProperty.call(mtime, 'secs')) { - return { + mtime = { secs: mtime.secs, nsecs: mtime.nsecs } @@ -72,7 +61,7 @@ function parseMtime (mtime) { // UnixFS TimeSpec if (Object.prototype.hasOwnProperty.call(mtime, 'EpochSeconds')) { - return { + mtime = { secs: mtime.EpochSeconds, nsecs: mtime.EpochNanoseconds } @@ -80,11 +69,23 @@ function parseMtime (mtime) { // process.hrtime() if (Array.isArray(mtime)) { - return { + mtime = { secs: mtime[0], nsecs: mtime[1] } } + + // Javascript Date + if (mtime instanceof Date) { + const ms = mtime.getTime() + const secs = Math.floor(ms / 1000) + + mtime = { + secs: secs, + nsecs: (ms - (secs * 1000)) * 1000 + } + } + /* TODO: https://github.com/ipfs/aegir/issues/487 @@ -93,12 +94,22 @@ function parseMtime (mtime) { const secs = mtime / BigInt(1e9) const nsecs = mtime - (secs * BigInt(1e9)) - return { + mtime = { secs: parseInt(secs), nsecs: parseInt(nsecs) } } */ + + if (!Object.prototype.hasOwnProperty.call(mtime, 'secs')) { + return undefined + } + + if (mtime.nsecs < 0 || mtime.nsecs > 999999999) { + throw errcode(new Error('mtime-nsecs must be within the range [0,999999999]'), 'ERR_INVALID_MTIME_NSECS') + } + + return mtime } function parseMode (mode) { @@ -255,6 +266,10 @@ class Data { EpochNanoseconds: parsed.nsecs } + if (mtime.EpochNanoseconds === 0) { + delete mtime.EpochNanoseconds + } + if (parsed.secs === 0 && !parsed.nsecs) { mtime = undefined } diff --git a/src/unixfs.proto.js b/src/unixfs.proto.js index b3850da9..1a4df0e6 100644 --- a/src/unixfs.proto.js +++ b/src/unixfs.proto.js @@ -25,7 +25,7 @@ message Data { message TimeSpec { required int64 EpochSeconds = 1; - optional uint32 EpochNanoseconds = 2; + optional fixed32 EpochNanoseconds = 2; } message Metadata { From 66630b8f8b4a9b8ce541ec392fc69686d6d1346f Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 8 Jan 2020 11:36:53 +0000 Subject: [PATCH 3/4] fix: updates to latest spec with optional mtime --- src/index.js | 20 +++++++------------- src/unixfs.proto.js | 8 ++++---- test/unixfs-format.spec.js | 10 +++++----- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/index.js b/src/index.js index fa6658cc..5f77a9c1 100644 --- a/src/index.js +++ b/src/index.js @@ -60,10 +60,10 @@ function parseMtime (mtime) { } // UnixFS TimeSpec - if (Object.prototype.hasOwnProperty.call(mtime, 'EpochSeconds')) { + if (Object.prototype.hasOwnProperty.call(mtime, 'Seconds')) { mtime = { - secs: mtime.EpochSeconds, - nsecs: mtime.EpochNanoseconds + secs: mtime.Seconds, + nsecs: mtime.FractionalNanoseconds } } @@ -178,8 +178,6 @@ class Data { if (parsedMtime) { this.mtime = parsedMtime - } else { - this.mtime = { secs: 0, nsecs: 0 } } } @@ -262,16 +260,12 @@ class Data { if (parsed) { mtime = { - EpochSeconds: parsed.secs, - EpochNanoseconds: parsed.nsecs + Seconds: parsed.secs, + FractionalNanoseconds: parsed.nsecs } - if (mtime.EpochNanoseconds === 0) { - delete mtime.EpochNanoseconds - } - - if (parsed.secs === 0 && !parsed.nsecs) { - mtime = undefined + if (mtime.FractionalNanoseconds === 0) { + delete mtime.FractionalNanoseconds } } } diff --git a/src/unixfs.proto.js b/src/unixfs.proto.js index 1a4df0e6..5b6b3181 100644 --- a/src/unixfs.proto.js +++ b/src/unixfs.proto.js @@ -20,12 +20,12 @@ message Data { optional uint64 hashType = 5; optional uint64 fanout = 6; optional uint32 mode = 7; - optional TimeSpec mtime = 8; + optional UnixTime mtime = 8; } -message TimeSpec { - required int64 EpochSeconds = 1; - optional fixed32 EpochNanoseconds = 2; +message UnixTime { + required int64 Seconds = 1; + optional fixed32 FractionalNanoseconds = 2; } message Metadata { diff --git a/test/unixfs-format.spec.js b/test/unixfs-format.spec.js index db0ffbd0..e8bf137d 100644 --- a/test/unixfs-format.spec.js +++ b/test/unixfs-format.spec.js @@ -204,7 +204,7 @@ describe('unixfs-format', () => { const marshaled = data.marshal() const unmarshaled = UnixFS.unmarshal(marshaled) - expect(unmarshaled).to.have.deep.property('mtime', { secs: 0, nsecs: 0 }) + expect(unmarshaled).to.not.have.property('mtime') }) it('mtime as date', () => { @@ -292,7 +292,7 @@ describe('unixfs-format', () => { const marshaled = data.marshal() const unmarshaled = UnixFS.unmarshal(marshaled) - expect(unmarshaled).to.have.deep.property('mtime', { secs: 0, nsecs: 0 }) + expect(unmarshaled).to.not.have.property('mtime') }) it('ignores invalid mtime set outside of constructor', () => { @@ -303,10 +303,10 @@ describe('unixfs-format', () => { const marshaled = entry.marshal() const unmarshaled = UnixFS.unmarshal(marshaled) - expect(unmarshaled).to.have.deep.property('mtime', { secs: 0, nsecs: 0 }) + expect(unmarshaled).to.not.have.property('mtime') }) - it('survies null mtime', () => { + it('survives null mtime', () => { const entry = new UnixFS({ type: 'file' }) @@ -314,7 +314,7 @@ describe('unixfs-format', () => { const marshaled = entry.marshal() const unmarshaled = UnixFS.unmarshal(marshaled) - expect(unmarshaled).to.have.deep.property('mtime', { secs: 0, nsecs: 0 }) + expect(unmarshaled).to.not.have.property('mtime') }) it('does not overwrite unknown mode bits', () => { From 85b87bc6af87a7bcd7775541baa9d8636a8acd0d Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 8 Jan 2020 12:30:30 +0000 Subject: [PATCH 4/4] docs: document mtime behaviour --- README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a36e89c3..0b111b4f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ The UnixFS spec can be found inside the [ipfs/specs repository](http://github.co - [get total fileSize](#get-total-filesize) - [marshal and unmarshal](#marshal-and-unmarshal) - [is this UnixFS entry a directory?](#is-this-unixfs-entry-a-directory) + - [has an mtime been set?](#has-an-mtime-been-set) - [Contribute](#contribute) - [License](#license) @@ -116,7 +117,12 @@ message Data { optional uint64 hashType = 5; optional uint64 fanout = 6; optional uint32 mode = 7; - optional int64 mtime = 8; + optional UnixTime mtime = 8; +} + +message UnixTime { + required int64 Seconds = 1; + optional fixed32 FractionalNanoseconds = 2; } message Metadata { @@ -142,7 +148,7 @@ const data = new UnixFS([options]) - data (Buffer): The optional data field for this node - blockSizes (Array, default: `[]`): If this is a `file` node that is made up of multiple blocks, `blockSizes` is a list numbers that represent the size of the file chunks stored in each child node. It is used to calculate the total file size. - mode (Number, default `0644` for files, `0755` for directories/hamt-sharded-directories) file mode -- mtime (Date, { secs, nsecs }, { EpochSeconds, EpochNanoseconds }, [ secs, nsecs ], default { secs: 0 }): The modification time of this node +- mtime (Date, { secs, nsecs }, { Seconds, FractionalNanoseconds }, [ secs, nsecs ], default { secs: 0 }): The modification time of this node #### add and remove a block size to the block size list @@ -177,6 +183,20 @@ const file = new Data({ type: 'file' }) file.isDirectory() // false ``` +#### has an mtime been set? + +If no modification time has been set, no `mtime` property will be present on the `Data` instance: + +```JavaScript +const file = new Data({ type: 'file' }) +file.mtime // undefined + +Object.prototype.hasOwnProperty.call(file, 'mtime') // false + +const dir = new Data({ type: 'dir', mtime: new Date() }) +dir.mtime // { secs: Number, nsecs: Number } +``` + ## Contribute Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-ipfs-unixfs/issues)!