精通IPFS:IPFS保存內容之中篇

在上一篇文章中,我們分析了保存文件/內容的整體流程,基本上知道在這個過程中文件/內容是怎麼處理的,但是還流下了一個疑問,就是文件是怎麼分片的,又是怎麼保存到本地系統,這篇文章我們就來解決這幾個問題。通過上一篇文章,我們知道 ipfs-unixfs-importer 這個類庫,它實現了 IPFS 用於處理文件的佈局和分塊機制,它的 index.js 文件內容只有一行代碼 require(’./importer’),接下來我們直接來看這個 importer/index.js 是怎麼處理的。
1.把參數傳遞的選項和默認選項進行合併,生成新的選項,然後檢查選項的相關配置。
const options = Object.assign({}, defaultOptions, _options)
options.cidVersion = options.cidVersion || 0
if (options.cidVersion > 0 && _options.rawLeaves === undefined) {
options.rawLeaves = true
}
if (_options && _options.hash !== undefined && _options.rawLeaves === undefined) {
options.rawLeaves = true
}
默認選項即 defaultOptions 內容如下:
const defaultOptions = {
chunker: ‘fixed’,
rawLeaves: false,
hashOnly: false,
cidVersion: 0,
hash: null,
leafType: ‘file’,
hashAlg: ‘sha2-256’
}
2.根據選項中指定的分割方式,從 IPFS 中提供的所有分割方法找到對應的分割對象。
const Chunker = chunkers[options.chunker]
chunkers 表示系統提供的所有分割方法對象,在父目錄下 chunker/index.js 文件中定義的,默認有 fixed、rabin 兩種方法,默認使用的是的前者,即固定大小。
3.生成一個 pull-through 的雙向流,雙向流的意思就是即可以從它讀取數據,又可以提供數據讓其它流讀取。
const entry = {
sink: writable(
(nodes, callback) => {
pending += nodes.length
nodes.forEach((node) => entry.source.push(node))
setImmediate(callback)
},
null,
1,
(err) => entry.source.end(err)
),
source: pushable()
}
source 流是 pull-pushable 類庫提供的一個可以其它流主動 push 的 pull-stream 源流,它提供了一個 push 方法,當調用這個方法時,它開始調用回調函數,從而把數據傳遞給後續的 through 或 sink。當時,它還提供了一個 end 方法,當數據讀取完成後,調用這個方法。
sink 流是 pull-write 類庫提供的一個創建通用 pull-stream sinks 流的基礎類。它的簽名如下:(write, reduce, max, cb), 因爲它是一個 sinks 流,所以它會讀取前一個流的數據,在讀取到數據之後就調用它的 write 方法保存讀取到的數據,如果數據讀取完成就調用它的 cb 方法。
在這裏 sink 函數從前一個流中讀取數據,然後放入 source 中。同時,source 成爲下一個流的讀取函數。
4.生成一個 dagStream 對象,這個對象也是一個 {source,sink} 對象。
const dagStream = DAGBuilder(Chunker, ipld, options)
DAGBuilder 函數定義於父目錄下的 builder/index.js 中,接下來我們看下這個執行過程:
合併選項參數和默認選項
const options = Object.assign({}, defaultOptions, _options)
默認選項如下:
const defaultOptions = {
strategy: ‘balanced’,
highWaterMark: 100,
reduceSingleLeafToSelf: true
}
根據選項指定的 reduce 策略,從系統提供的多個策略中選擇指定的策略。
const strategyName = options.strategy
const reducer = reducers[strategyName]
系統定義的的策略如下:
const reducers = {
flat: require(’./flat’),
balanced: require(’./balanced’),
trickle: require(’./trickle’)
}
在用戶不指定具體策略的默認情況下,根據前面執行過程,最終選定的策略爲 balanced。
調用 Builder 方法創建最終的策略對象。
const createStrategy = Builder(Chunker, ipld, reducer, options)
Builder 方法位於 builder.js 文件中,它會創建一個 pull-stream 的 through 流對象。在看它的內部之前,我們首先看下的 4個參數。
第 1 個參數 Chunker/createChunker,它表示具體分割內容的策略,默認情況下爲 fixed,詳見第一步中的 defaultOptions 變量內容;
第 2 個參數 ipld/ipld,這個是 IPFS 對象的 _ipld 屬性,在 IPFS 對象創建時生成的,表示星際接續的數據,目前它可以連接比特幣、以太坊、git、zcash 等,在 IPFS 體系中具有非常重要的位置;
第 3 個參數 reducer/createReducer 是具體的 reduce 策略,默認情況爲 balanced,詳見第四步中生成 reducer 變量的過程。
第 4 個參數 options/_options 爲選項。
看完參數,接下來,我們看下它的執行邏輯。
o合併指定的選項和自身默認的選項。
const options = extend({}, defaultOptions, _options)
o默認選項如下:
const defaultOptions = {
chunkerOptions: {
maxChunkSize: 262144,
avgChunkSize: 262144
},
rawLeaves: false,
hashAlg: ‘sha2-256’,
leafType: ‘file’,
cidVersion: 0,
progress: () => {}
}
o返回一個函數對象。
return function (source) {
return function (items, cb) {
parallel(items.map((item) => (cb) => {
if (!item.content) {
return createAndStoreDir(item, (err, node) => {
if (err) {
return cb(err)
}
if (node) {
source.push(node)
}
cb()
})
}
createAndStoreFile(item, (err, node) => {
if (err) {
return cb(err)
}
if (node) {
source.push(node)
}
cb()
})
}), cb)
}
}
返回的這個函數,最終成爲了一個 sink 流的 write 方法。
調用 createBuildStream 方法,生成一個雙向流對象。
createBuildStream(createStrategy, ipld, options)
createBuildStream 方法位於 create-build-stream.js 文件中,代碼如下:
const source = pullPushable()
const sink = pullWrite(
createStrategy(source),
null,
options.highWaterMark,
(err) => source.end(err)
)
return {
source: source,
sink: sink
}
在這段代碼中,source 流是 pull-pushable 類庫提供的一個可以主動 push 到其它流的 pull-stream 源流,這個類庫在前面我們已經分析過,這裏就直接略過。
sink 流是 pull-write 類庫提供的一個創建通用 pull-stream sinks 流的基礎類,這個類庫也在前面分析過,這裏也不細講,我們只看下它的 write 方法,這裏的 createStrategy 函數正是調用 Builder 方法返回的 createStrategy 函數,用 source 作爲參數,調用它,用返回的第二層匿名函數作爲 write 方法。
5.生成一個樹構建器流對象,並返回其雙向流對象。
const treeBuilder = createTreeBuilder(ipld, options)
const treeBuilderStream = treeBuilder.stream()
createTreeBuilder 函數位於 tree-builder.js 文件中,我們來看它的執行邏輯。
首先,合併默認選項對象和指定的選項對象。
const options = Object.assign({}, defaultOptions, _options)
默認選擇對象如下:
const defaultOptions = {
wrap: false,
shardSplitThreshold: 1000,
onlyHash: false
}
onlyHash 表示是否不保存文件/內容,只計算其哈希。
創建一個隊列對象。
const queue = createQueue(consumeQueue, 1)

創建一個雙向流對象
let stream = createStream()
其中 sink 對象是一個 pull-write 類庫提供的流,這個已經見過多次了,它的 write 方法後面遇到時再來看,source 是一個 pull-pushable 類庫提供的流,這個也見過多次。
創建一個 DirFlat 對象。
let tree = DirFlat({
path: ‘’,
root: true,
dir: true,
dirty: false,
flat: true
}, options)
返回特權函數構成的對象。
return {
flush: flushRoot,
stream: getStream
}
6.創建一個暫停流。這裏什麼也不做。
7.調用 pull 方法,創建一個完整的流來保存文件內容。
pull(
entry,
pausable,
dagStream,
map((node) => {
pending–
if (!pending) {
process.nextTick(() => {
while (waitingPending.length) {
waitingPending.shift()()
}
})
}
return node
}),
treeBuilderStream
)
pull 函數是 pull-stream 是類庫中的核心函數,在它的執行過程中,最後的 sink 流通過依次調用前面的 through 流,最終從最前面的 source 流中拉取數據,除了最前面的 Source 流和最後面的 Sink 流,中間的都是 through 流,它們即可以被後面的流調用以提供數據,也可以調用前面的流來讀取數據。
當 pull 函數在調用某個參數從前面讀取數據時,如果當前參數是一個對象(即雙向流)時,那麼就會調用它的 sink 方法來讀取,同時用它的 source 方法作爲後面參數的讀取方法。
下面我們分析這段代碼中的幾個流,它們太重要了。
首先是 entry 流,它是一個雙向流,它的 sink 函數(類型爲 pull-write 流)調用前一個流的 read 方法來讀取數據,並把讀取到的數據放在 source 中(類型爲 pull-pushable )。
然後是 dagStream 流,它也是一個雙向流,它的 sink 函數(類型爲 pull-write 流)調用 entry 流的 source 方法來讀取數據。sink 函數的異步寫函數參數爲 builder.js 中返回的第二層函數,當讀取到數據之後,調用 builder.js 中返回的第二層函數進行處理,在第二層函數中,大致流程是把數據保存自身的 source 中(類型爲 pull-pushable )。
dagStream 在 create-build-stream.js 中生成。爲了方便理解,這裏我們再次看下它的代碼。
const source = pullPushable()
const sink = pullWrite( createStrategy(source), null, options.highWaterMark, (err) => source.end(err) )
return { source: source, sink: sink }
最後是 treeBuilderStream 流,它也是一個雙向流,它的 sink 函數(類型爲 pull-write 流)調用 dagStream 流的 source 方法來讀取數據,並把讀取到的數據放在 source 中(類型爲 pull-pushable )。
其他兩個流對流程沒有任何影響,讀者可以自行分析,這裏略過不提。
在這一步中,通過 pull 函數把最重要的幾個流連接到一起,並通過下面最後一步,把它們與外部的流聯繫到一起。
8.最後,返回雙向流對象。
{
sink: entry.sink,
source: treeBuilderStream.source,
flush: flush
}
到這裏,文件已經保存完成了。
啥?文件已經保存完成了?什麼都沒看到就把文件保存完了,不會騙我們的吧?哈,因爲保存文件這個動作太複雜了,所以上面只是靜態的從代碼層面進行梳理,下面我們從頭到尾從動態處理的過程來看下文件到底是怎麼保存在本地的。
一切要從我們在上篇寫的這個示例說起
const {createNode} = require(‘ipfs’)
const node = createNode({
libp2p:{
config:{
dht:{
enabled:true
}
}
}
})
node.on(‘ready’, async () => {
const content = 我愛黑螢;
const filesAdded = await node.add({
content: Buffer.from(content)
},{
chunkerOptions:{
maxChunkSize:1000,
avgChunkSize:1000
}
})
console.log(‘Added file:’, filesAdded[0].path, filesAdded[0].hash)
})
上面這段代碼,最終執行的是 core/components/files-regular/add-pull-stream.js 文件中的函數,它的主體就是下面的這段代碼:
pull(
pull.map(content => normalizeContent(content, opts)),
pull.flatten(),
importer(self._ipld, opts),
pull.asyncMap((file, cb) => prepareFile(file, self, opts, cb)),
pull.map(file => preloadFile(file, self, opts)),
pull.asyncMap((file, cb) => pinFile(file, self, opts, cb))
)
爲了便於分析理解,我們在分析過程中仍然使用推的方式,從源流推到目的流中,注意這個僅是爲了理解方便,真實的過程是目的流從源流中拉取數據。

下面代碼簡單解釋如下:
首先,調用第一個 pull.map 流,對收到的文件或內容並進行一些必要的轉換,
調用 pull.flatten 流,把前一步生成的數組進行扁平化處理。
調用 importer 流來保存內容。
調用 pull.asyncMap 方法,對已經保存的文件/內容進行預處理,生成用戶看到的內容。
調用 pull.map 方法,把已經保存到本地的文件預加載到指定節點。
調用 pull.asyncMap 方法,把已經保存到本地的文件長期保存在本地,確保不被垃圾回收。
下面我們重點看下文件內容在 importer 流中的處理邏輯。
(1)調用 entry.sink 函數從前面的 pull.flatten 流中讀取保存的每一個文件/內容。
(2)調用 dagStream.sink 函數從前面的流中讀取數據,並在讀取到數據之後,調用 builder.js 中定義的第二層匿名函數進行處理。在這個函數中,調用異步流程庫 async 的 parallel 方法對收到的每個要處理的文件內容進行處理,具體處理如下:如果保存的是目錄,那麼調用 createAndStoreDir 方法,創建並保存目錄;如果保存的是文件,那麼調用 createAndStoreFile 方法,創建並保存主文件。因爲我們保存的是文件,所以在這裏詳細看下 createAndStoreFile 方法,它的過程如下:
1.如果保存的內容是 Buffer,那麼調用 pull-stream 的 values 方法,生成內容源流。
if (Buffer.isBuffer(file.content)) {
file.content = values([file.content])
}
調用 createReducer 方法,創建 reducer 對象,默認爲 balanced,所以這裏創建的 reducer 對象類型爲 balanced/balanced-reducer.js 文件中定義的函數。
const reducer = createReducer(reduce(file, ipld, options), options)
調用 createChunker 方法,創建 chunker 對象,默認爲 fixed,所以這裏創建的 chunker 對象類型爲 chunker/fixed-size.js 主文件中定義的函數。
chunker = createChunker(options.chunkerOptions)
調用 pull 函數進行保存文件。
o設置源流爲 file.content。
o調用 chunker 流,對保存的內容進行分塊。
o調用 paraMap 流(類型爲 pull-paramap),對每一個分塊進行處理。
o調用 pullThrough 流(類型爲 pull-through 流),對收到的每個數據進行處理。
o調用 reducer 流,把所有生成的分塊進行 reduce 處理。如果文件進行了多次分塊,這裏就會根據生成的分塊生成一個父塊。
o調用 collect 流,調用回調函數即 createAndStoreFile ,把保存文件的結果傳遞到外部函數中。
這個 pull 函數會進行 IPFS 特有業務,涉及到 IPFS 保存文件核心邏輯,這塊我們留在下一篇文章中進行分析。
(3)調用 treeBuilderStream.sink 函數從前面的流中讀取數據,在這裏即爲保存文件的結果,並在讀取到保存文件結果之後,把結果保存在 source 中。當把保存文件的結果保存到 source 中之後,core/components/files-regular/add-pull-stream.js 文件中定義的 pull.asyncMap 就可以得到這個結果了。

作者:喬瘋,加密貨幣愛好者,ipfs 愛好者,黑螢科技CTO。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章