Skip to content

Commit 8adc245

Browse files
authored
feat: store time as timespec (#40)
* 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. * fix: enforce nsec value range Allow 0-999.. but only marshal 1-999.. * fix: updates to latest spec with optional mtime * docs: document mtime behaviour
1 parent 09fd4ed commit 8adc245

File tree

6 files changed

+351
-26
lines changed

6 files changed

+351
-26
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
docs
22
package-lock.json
33
yarn.lock
4+
.nyc_output
45

56
# Logs
67
logs

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The UnixFS spec can be found inside the [ipfs/specs repository](http://github.co
3636
- [get total fileSize](#get-total-filesize)
3737
- [marshal and unmarshal](#marshal-and-unmarshal)
3838
- [is this UnixFS entry a directory?](#is-this-unixfs-entry-a-directory)
39+
- [has an mtime been set?](#has-an-mtime-been-set)
3940
- [Contribute](#contribute)
4041
- [License](#license)
4142

@@ -116,7 +117,12 @@ message Data {
116117
optional uint64 hashType = 5;
117118
optional uint64 fanout = 6;
118119
optional uint32 mode = 7;
119-
optional int64 mtime = 8;
120+
optional UnixTime mtime = 8;
121+
}
122+
123+
message UnixTime {
124+
required int64 Seconds = 1;
125+
optional fixed32 FractionalNanoseconds = 2;
120126
}
121127
122128
message Metadata {
@@ -142,7 +148,7 @@ const data = new UnixFS([options])
142148
- data (Buffer): The optional data field for this node
143149
- 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.
144150
- mode (Number, default `0644` for files, `0755` for directories/hamt-sharded-directories) file mode
145-
- mtime (Date, default `0`): The modification time of this node
151+
- mtime (Date, { secs, nsecs }, { Seconds, FractionalNanoseconds }, [ secs, nsecs ], default { secs: 0 }): The modification time of this node
146152

147153
#### add and remove a block size to the block size list
148154

@@ -177,6 +183,20 @@ const file = new Data({ type: 'file' })
177183
file.isDirectory() // false
178184
```
179185

186+
#### has an mtime been set?
187+
188+
If no modification time has been set, no `mtime` property will be present on the `Data` instance:
189+
190+
```JavaScript
191+
const file = new Data({ type: 'file' })
192+
file.mtime // undefined
193+
194+
Object.prototype.hasOwnProperty.call(file, 'mtime') // false
195+
196+
const dir = new Data({ type: 'dir', mtime: new Date() })
197+
dir.mtime // { secs: Number, nsecs: Number }
198+
```
199+
180200
## Contribute
181201

182202
Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-ipfs-unixfs/issues)!

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"release": "aegir release",
1818
"release-minor": "aegir release --type minor",
1919
"release-major": "aegir release --type major",
20-
"coverage": "aegir coverage"
20+
"coverage": "nyc -s aegir test -t node && nyc report --reporter=html"
2121
},
2222
"repository": {
2323
"type": "git",
@@ -38,9 +38,11 @@
3838
"devDependencies": {
3939
"aegir": "^20.4.1",
4040
"chai": "^4.2.0",
41-
"dirty-chai": "^2.0.1"
41+
"dirty-chai": "^2.0.1",
42+
"nyc": "^15.0.0"
4243
},
4344
"dependencies": {
45+
"err-code": "^2.0.0",
4446
"protons": "^1.1.0"
4547
},
4648
"contributors": [

src/index.js

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const protons = require('protons')
44
const pb = protons(require('./unixfs.proto'))
55
const unixfsData = pb.Data
6+
const errcode = require('err-code')
67

78
const types = [
89
'raw',
@@ -45,6 +46,84 @@ function parseArgs (args) {
4546
return args[0]
4647
}
4748

49+
function parseMtime (mtime) {
50+
if (mtime == null) {
51+
return undefined
52+
}
53+
54+
// { secs, nsecs }
55+
if (Object.prototype.hasOwnProperty.call(mtime, 'secs')) {
56+
mtime = {
57+
secs: mtime.secs,
58+
nsecs: mtime.nsecs
59+
}
60+
}
61+
62+
// UnixFS TimeSpec
63+
if (Object.prototype.hasOwnProperty.call(mtime, 'Seconds')) {
64+
mtime = {
65+
secs: mtime.Seconds,
66+
nsecs: mtime.FractionalNanoseconds
67+
}
68+
}
69+
70+
// process.hrtime()
71+
if (Array.isArray(mtime)) {
72+
mtime = {
73+
secs: mtime[0],
74+
nsecs: mtime[1]
75+
}
76+
}
77+
78+
// Javascript Date
79+
if (mtime instanceof Date) {
80+
const ms = mtime.getTime()
81+
const secs = Math.floor(ms / 1000)
82+
83+
mtime = {
84+
secs: secs,
85+
nsecs: (ms - (secs * 1000)) * 1000
86+
}
87+
}
88+
89+
/*
90+
TODO: https://github.com/ipfs/aegir/issues/487
91+
92+
// process.hrtime.bigint()
93+
if (typeof mtime === 'bigint') {
94+
const secs = mtime / BigInt(1e9)
95+
const nsecs = mtime - (secs * BigInt(1e9))
96+
97+
mtime = {
98+
secs: parseInt(secs),
99+
nsecs: parseInt(nsecs)
100+
}
101+
}
102+
*/
103+
104+
if (!Object.prototype.hasOwnProperty.call(mtime, 'secs')) {
105+
return undefined
106+
}
107+
108+
if (mtime.nsecs < 0 || mtime.nsecs > 999999999) {
109+
throw errcode(new Error('mtime-nsecs must be within the range [0,999999999]'), 'ERR_INVALID_MTIME_NSECS')
110+
}
111+
112+
return mtime
113+
}
114+
115+
function parseMode (mode) {
116+
if (mode == null) {
117+
return undefined
118+
}
119+
120+
if (typeof mode === 'string' || mode instanceof String) {
121+
mode = parseInt(mode, 8)
122+
}
123+
124+
return mode & 0xFFF
125+
}
126+
48127
class Data {
49128
// decode from protobuf https://github.com/ipfs/specs/blob/master/UNIXFS.md
50129
static unmarshal (marshaled) {
@@ -55,7 +134,7 @@ class Data {
55134
data: decoded.hasData() ? decoded.Data : undefined,
56135
blockSizes: decoded.blocksizes,
57136
mode: decoded.hasMode() ? decoded.mode : undefined,
58-
mtime: decoded.hasMtime() ? new Date(decoded.mtime * 1000) : undefined
137+
mtime: decoded.hasMtime() ? decoded.mtime : undefined
59138
})
60139
}
61140

@@ -71,25 +150,35 @@ class Data {
71150
} = parseArgs(args)
72151

73152
if (!types.includes(type)) {
74-
throw new Error('Type: ' + type + ' is not valid')
153+
throw errcode(new Error('Type: ' + type + ' is not valid'), 'ERR_INVALID_TYPE')
75154
}
76155

77156
this.type = type
78157
this.data = data
79158
this.hashType = hashType
80159
this.fanout = fanout
81160
this.blockSizes = blockSizes || []
82-
this.mtime = mtime || new Date(0)
83-
this.mode = mode || mode === 0 ? (mode & 0xFFF) : undefined
84161
this._originalMode = mode
85162

163+
const parsedMode = parseMode(mode)
164+
165+
if (parsedMode !== undefined) {
166+
this.mode = parsedMode
167+
}
168+
86169
if (this.mode === undefined && type === 'file') {
87170
this.mode = DEFAULT_FILE_MODE
88171
}
89172

90173
if (this.mode === undefined && this.isDirectory()) {
91174
this.mode = DEFAULT_DIRECTORY_MODE
92175
}
176+
177+
const parsedMtime = parseMtime(mtime)
178+
179+
if (parsedMtime) {
180+
this.mtime = parsedMtime
181+
}
93182
}
94183

95184
isDirectory () {
@@ -135,7 +224,7 @@ class Data {
135224
case 'symlink': type = unixfsData.DataType.Symlink; break
136225
case 'hamt-sharded-directory': type = unixfsData.DataType.HAMTShard; break
137226
default:
138-
throw new Error(`Unkown type: "${this.type}"`)
227+
throw errcode(new Error('Type: ' + type + ' is not valid'), 'ERR_INVALID_TYPE')
139228
}
140229

141230
let data = this.data
@@ -152,8 +241,8 @@ class Data {
152241

153242
let mode
154243

155-
if (this.mode || this.mode === 0) {
156-
mode = (this._originalMode & 0xFFFFF000) | (this.mode & 0xFFF)
244+
if (this.mode != null) {
245+
mode = (this._originalMode & 0xFFFFF000) | parseMode(this.mode)
157246

158247
if (mode === DEFAULT_FILE_MODE && this.type === 'file') {
159248
mode = undefined
@@ -166,11 +255,18 @@ class Data {
166255

167256
let mtime
168257

169-
if (this.mtime) {
170-
mtime = Math.round(this.mtime.getTime() / 1000)
258+
if (this.mtime != null) {
259+
const parsed = parseMtime(this.mtime)
260+
261+
if (parsed) {
262+
mtime = {
263+
Seconds: parsed.secs,
264+
FractionalNanoseconds: parsed.nsecs
265+
}
171266

172-
if (mtime === 0) {
173-
mtime = undefined
267+
if (mtime.FractionalNanoseconds === 0) {
268+
delete mtime.FractionalNanoseconds
269+
}
174270
}
175271
}
176272

src/unixfs.proto.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ message Data {
2020
optional uint64 hashType = 5;
2121
optional uint64 fanout = 6;
2222
optional uint32 mode = 7;
23-
optional int64 mtime = 8;
23+
optional UnixTime mtime = 8;
24+
}
25+
26+
message UnixTime {
27+
required int64 Seconds = 1;
28+
optional fixed32 FractionalNanoseconds = 2;
2429
}
2530
2631
message Metadata {

0 commit comments

Comments
 (0)