Skip to content

Commit 32a4478

Browse files
authored
feat: add recursive ACT support for mantaray structures (#1119)
1 parent bd947c9 commit 32a4478

2 files changed

Lines changed: 186 additions & 2 deletions

File tree

src/manifest/manifest.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,19 @@ export class MantarayNode {
424424
requestOptions?: BeeRequestOptions,
425425
): Promise<UploadResult> {
426426
for (const fork of this.forks.values()) {
427-
await fork.node.saveRecursively(bee, postageBatchId, options, requestOptions)
427+
const uploadResult = await fork.node.saveRecursively(bee, postageBatchId, options, requestOptions)
428+
429+
if (options?.act) {
430+
let historyAddress: Reference | undefined
431+
uploadResult.historyAddress.ifPresent(ref => (historyAddress = ref))
432+
433+
if (historyAddress) {
434+
if (!fork.node.metadata) {
435+
fork.node.metadata = {}
436+
}
437+
fork.node.metadata['swarm-act-history-address'] = historyAddress.toHex()
438+
}
439+
}
428440
}
429441
const result = await bee.uploadData(postageBatchId, await this.marshal(), options, requestOptions)
430442
this.selfAddress = result.reference.toUint8Array()
@@ -440,7 +452,17 @@ export class MantarayNode {
440452
if (!fork.node.selfAddress) {
441453
throw Error('MantarayNode#loadRecursively fork.node.selfAddress is not set')
442454
}
443-
const node = await MantarayNode.unmarshal(bee, fork.node.selfAddress, options, requestOptions)
455+
456+
let downloadOptions = options
457+
458+
if (fork.node.metadata && fork.node.metadata['swarm-act-history-address']) {
459+
downloadOptions = {
460+
...options,
461+
actHistoryAddress: fork.node.metadata['swarm-act-history-address'],
462+
}
463+
}
464+
465+
const node = await MantarayNode.unmarshal(bee, fork.node.selfAddress, downloadOptions, requestOptions)
444466
fork.node.targetAddress = node.targetAddress
445467
fork.node.forks = node.forks
446468
fork.node.path = fork.prefix

test/integration/manifest.spec.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,165 @@ test('Manifest add fork with foreign path', async () => {
131131
expect(items['c/中文/index.xml']).toBeDefined()
132132
expect(items['c/中文/index.html']).toBeDefined()
133133
})
134+
135+
test('Manifest save/load with ACT stores history address in fork metadata', async () => {
136+
const bee = makeBee()
137+
138+
const ref1 = arbitraryReference()
139+
const ref2 = arbitraryReference()
140+
141+
const node = new MantarayNode()
142+
node.addFork('folder/file1.txt', ref1, { 'Content-Type': 'text/plain' })
143+
node.addFork('folder/file2.txt', ref2, { 'Content-Type': 'text/plain' })
144+
145+
const result = await node.saveRecursively(bee, batch(), { act: true })
146+
147+
expect(result.reference.toHex()).toHaveLength(64)
148+
expect(result.historyAddress.getOrThrow().toHex()).toHaveLength(64)
149+
})
150+
151+
test('Manifest save/load with ACT preserves structure', async () => {
152+
const bee = makeBee()
153+
const { publicKey } = await bee.getNodeAddresses()
154+
155+
const fileData = 'test file content for ACT preservation'
156+
const fileUpload = await bee.uploadData(batch(), fileData)
157+
158+
const node = new MantarayNode()
159+
node.addFork('deep/nested/file.txt', fileUpload.reference, {
160+
'Content-Type': 'text/plain',
161+
Filename: 'file.txt',
162+
})
163+
164+
const result = await node.saveRecursively(bee, batch(), { act: true })
165+
const rootHistoryAddress = result.historyAddress.getOrThrow()
166+
167+
const loadedNode = await MantarayNode.unmarshal(bee, result.reference, {
168+
actHistoryAddress: rootHistoryAddress,
169+
actPublisher: publicKey,
170+
})
171+
172+
await loadedNode.loadRecursively(bee, { actPublisher: publicKey })
173+
174+
const mapping = loadedNode.collectAndMap()
175+
176+
expect(mapping['deep/nested/file.txt']).toBeDefined()
177+
expect(mapping['deep/nested/file.txt']).toBe(fileUpload.reference.toHex())
178+
})
179+
180+
test('Manifest save/load with ACT nested folders', async () => {
181+
const bee = makeBee()
182+
const { publicKey } = await bee.getNodeAddresses()
183+
184+
const file1 = await bee.uploadData(batch(), 'content-1')
185+
const file2 = await bee.uploadData(batch(), 'content-2')
186+
const file3 = await bee.uploadData(batch(), 'content-3')
187+
188+
const node = new MantarayNode()
189+
node.addFork('a/b/c/file1.txt', file1.reference)
190+
node.addFork('a/b/file2.txt', file2.reference)
191+
node.addFork('a/file3.txt', file3.reference)
192+
193+
const result = await node.saveRecursively(bee, batch(), { act: true })
194+
195+
const loadedNode = await MantarayNode.unmarshal(bee, result.reference, {
196+
actHistoryAddress: result.historyAddress.getOrThrow(),
197+
actPublisher: publicKey,
198+
})
199+
200+
await loadedNode.loadRecursively(bee, { actPublisher: publicKey })
201+
202+
const mapping = loadedNode.collectAndMap()
203+
204+
expect(Object.keys(mapping)).toHaveLength(3)
205+
expect(mapping['a/b/c/file1.txt']).toBe(file1.reference.toHex())
206+
expect(mapping['a/b/file2.txt']).toBe(file2.reference.toHex())
207+
expect(mapping['a/file3.txt']).toBe(file3.reference.toHex())
208+
})
209+
210+
test('Manifest save/load with ACT can download and verify content', async () => {
211+
const bee = makeBee()
212+
const { publicKey } = await bee.getNodeAddresses()
213+
214+
const originalContent = 'This is the secret content that should be encrypted and decrypted correctly!'
215+
const fileUpload = await bee.uploadData(batch(), originalContent)
216+
217+
const node = new MantarayNode()
218+
node.addFork('secret/data.txt', fileUpload.reference, {
219+
'Content-Type': 'text/plain',
220+
Filename: 'data.txt',
221+
})
222+
223+
const result = await node.saveRecursively(bee, batch(), { act: true })
224+
225+
const loadedNode = await MantarayNode.unmarshal(bee, result.reference, {
226+
actHistoryAddress: result.historyAddress.getOrThrow(),
227+
actPublisher: publicKey,
228+
})
229+
230+
await loadedNode.loadRecursively(bee, { actPublisher: publicKey })
231+
232+
const mapping = loadedNode.collectAndMap()
233+
const fileReference = mapping['secret/data.txt']
234+
235+
expect(fileReference).toBeDefined()
236+
237+
const downloadedData = await bee.downloadData(fileReference)
238+
expect(downloadedData.toUtf8()).toBe(originalContent)
239+
})
240+
241+
test('Manifest save/load with ACT single file at root', async () => {
242+
const bee = makeBee()
243+
const { publicKey } = await bee.getNodeAddresses()
244+
245+
const content = 'root-level-file-content'
246+
const fileUpload = await bee.uploadData(batch(), content)
247+
248+
const node = new MantarayNode()
249+
node.addFork('readme.txt', fileUpload.reference)
250+
251+
const result = await node.saveRecursively(bee, batch(), { act: true })
252+
253+
const loadedNode = await MantarayNode.unmarshal(bee, result.reference, {
254+
actHistoryAddress: result.historyAddress.getOrThrow(),
255+
actPublisher: publicKey,
256+
})
257+
258+
await loadedNode.loadRecursively(bee, { actPublisher: publicKey })
259+
260+
const mapping = loadedNode.collectAndMap()
261+
262+
expect(Object.keys(mapping)).toHaveLength(1)
263+
expect(mapping['readme.txt']).toBe(fileUpload.reference.toHex())
264+
})
265+
266+
test('Manifest save/load with ACT preserves existing metadata', async () => {
267+
const bee = makeBee()
268+
const { publicKey } = await bee.getNodeAddresses()
269+
270+
const fileUpload = await bee.uploadData(batch(), 'test-content')
271+
272+
const node = new MantarayNode()
273+
node.addFork('file.txt', fileUpload.reference, {
274+
'Content-Type': 'text/plain',
275+
'Custom-Header': 'custom-value',
276+
Filename: 'file.txt',
277+
})
278+
279+
const result = await node.saveRecursively(bee, batch(), { act: true })
280+
281+
const loadedNode = await MantarayNode.unmarshal(bee, result.reference, {
282+
actHistoryAddress: result.historyAddress.getOrThrow(),
283+
actPublisher: publicKey,
284+
})
285+
286+
await loadedNode.loadRecursively(bee, { actPublisher: publicKey })
287+
288+
const values = loadedNode.collect()
289+
const fileNode = values.find(v => v.fullPathString === 'file.txt')
290+
291+
expect(fileNode).toBeDefined()
292+
expect(fileNode?.metadata?.['Content-Type']).toBe('text/plain')
293+
expect(fileNode?.metadata?.['Custom-Header']).toBe('custom-value')
294+
expect(fileNode?.metadata?.['Filename']).toBe('file.txt')
295+
})

0 commit comments

Comments
 (0)