Skip to content

Commit 05a04a1

Browse files
authored
Merge pull request #92 from ardriveapp/dev
PE-741: Release ArDrive Core v1.0.5
2 parents 6a22ded + fd1850f commit 05a04a1

File tree

13 files changed

+838
-26
lines changed

13 files changed

+838
-26
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ardrive-core-js",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"description": "ArDrive Core contains the essential back end application features to support the ArDrive CLI and Desktop apps, such as file management, Permaweb upload/download, wallet management and other common functions.",
55
"main": "./lib/exports.js",
66
"types": "./lib/exports.d.ts",

src/ardrive.ts

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
ArFSPrivateFile,
66
ArFSPrivateFileOrFolderWithPaths
77
} from './arfs/arfs_entities';
8-
import { ArFSFolderToUpload, ArFSFileToUpload } from './arfs/arfs_file_wrapper';
8+
import {
9+
ArFSFolderToUpload,
10+
ArFSFileToUpload,
11+
ArFSEntityToUpload,
12+
ArFSManifestToUpload
13+
} from './arfs/arfs_file_wrapper';
914
import {
1015
ArFSPublicFileMetadataTransactionData,
1116
ArFSPrivateFileMetadataTransactionData,
@@ -38,7 +43,9 @@ import {
3843
DriveID,
3944
stubTransactionID,
4045
UploadPublicFileParams,
41-
UploadPrivateFileParams
46+
UploadPrivateFileParams,
47+
ArFSManifestResult,
48+
UploadPublicManifestParams
4249
} from './types';
4350
import {
4451
CommunityTipParams,
@@ -1002,6 +1009,83 @@ export class ArDrive extends ArDriveAnonymous {
10021009
};
10031010
}
10041011

1012+
public async uploadPublicManifest({
1013+
folderId,
1014+
destManifestName = 'DriveManifest.json',
1015+
maxDepth = Number.MAX_SAFE_INTEGER,
1016+
conflictResolution = upsertOnConflicts
1017+
}: UploadPublicManifestParams): Promise<ArFSManifestResult> {
1018+
const driveId = await this.arFsDao.getDriveIdForFolderId(folderId);
1019+
1020+
// Assert that the owner of this drive is consistent with the provided wallet
1021+
const owner = await this.getOwnerForDriveId(driveId);
1022+
await this.assertOwnerAddress(owner);
1023+
1024+
const filesAndFolderNames = await this.arFsDao.getPublicNameConflictInfoInFolder(folderId);
1025+
1026+
const fileToFolderConflict = filesAndFolderNames.folders.find((f) => f.folderName === destManifestName);
1027+
if (fileToFolderConflict) {
1028+
// File names CANNOT conflict with folder names
1029+
throw new Error(errorMessage.entityNameExists);
1030+
}
1031+
1032+
// Manifest becomes a new revision if the destination name conflicts for
1033+
// --replace and --upsert behaviors, since it will be newly created each time
1034+
const existingFileId = filesAndFolderNames.files.find((f) => f.fileName === destManifestName)?.fileId;
1035+
if (existingFileId && conflictResolution === skipOnConflicts) {
1036+
// Return empty result if there is an existing manifest and resolution is set to skip
1037+
return { ...emptyArFSResult, links: [] };
1038+
}
1039+
1040+
const children = await this.listPublicFolder({
1041+
folderId,
1042+
maxDepth,
1043+
includeRoot: true,
1044+
owner
1045+
});
1046+
const arweaveManifest = new ArFSManifestToUpload(children, destManifestName);
1047+
1048+
const uploadBaseCosts = await this.estimateAndAssertCostOfFileUpload(
1049+
arweaveManifest.size,
1050+
this.stubPublicFileMetadata(arweaveManifest),
1051+
'public'
1052+
);
1053+
const fileDataRewardSettings = { reward: uploadBaseCosts.fileDataBaseReward, feeMultiple: this.feeMultiple };
1054+
const metadataRewardSettings = { reward: uploadBaseCosts.metaDataBaseReward, feeMultiple: this.feeMultiple };
1055+
1056+
const uploadFileResult = await this.arFsDao.uploadPublicFile({
1057+
parentFolderId: folderId,
1058+
wrappedFile: arweaveManifest,
1059+
driveId,
1060+
fileDataRewardSettings,
1061+
metadataRewardSettings,
1062+
destFileName: destManifestName,
1063+
existingFileId
1064+
});
1065+
1066+
const { tipData, reward: communityTipTrxReward } = await this.sendCommunityTip({
1067+
communityWinstonTip: uploadBaseCosts.communityWinstonTip
1068+
});
1069+
1070+
return Promise.resolve({
1071+
created: [
1072+
{
1073+
type: 'file',
1074+
metadataTxId: uploadFileResult.metaDataTrxId,
1075+
dataTxId: uploadFileResult.dataTrxId,
1076+
entityId: uploadFileResult.fileId
1077+
}
1078+
],
1079+
tips: [tipData],
1080+
fees: {
1081+
[`${uploadFileResult.dataTrxId}`]: uploadFileResult.dataTrxReward,
1082+
[`${uploadFileResult.metaDataTrxId}`]: uploadFileResult.metaDataTrxReward,
1083+
[`${tipData.txId}`]: communityTipTrxReward
1084+
},
1085+
links: arweaveManifest.getLinksOutput(uploadFileResult.dataTrxId)
1086+
});
1087+
}
1088+
10051089
public async createPublicFolder({
10061090
folderName,
10071091
driveId,
@@ -1487,7 +1571,7 @@ export class ArDrive extends ArDriveAnonymous {
14871571

14881572
// Provides for stubbing metadata during cost estimations since the data trx ID won't yet be known
14891573
private stubPublicFileMetadata(
1490-
wrappedFile: ArFSFileToUpload,
1574+
wrappedFile: ArFSEntityToUpload,
14911575
destinationFileName?: string
14921576
): ArFSPublicFileMetadataTransactionData {
14931577
const { fileSize, dataContentType, lastModifiedDateMS } = wrappedFile.gatherFileInfo();

src/arfs/arfs_file_wrapper.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { expect } from 'chai';
2+
import {
3+
stubEntitiesWithNestedFileWithPaths,
4+
stubEntitiesWithNoFilesWithPaths,
5+
stubEntitiesWithOneFileWithPaths,
6+
stubEntitiesWithPathsAndIndexInRoot,
7+
stubPublicEntitiesWithPaths,
8+
stubSpecialCharEntitiesWithPaths
9+
} from '../../tests/stubs';
10+
import { stubTransactionID, W } from '../types';
11+
import { ArFSFileToUpload, ArFSFolderToUpload, ArFSManifestToUpload, wrapFileOrFolder } from './arfs_file_wrapper';
12+
13+
describe('ArFSManifestToUpload class', () => {
14+
it('will link to an index.html file in the root of the folder if it exists', () => {
15+
const manifest = new ArFSManifestToUpload(stubEntitiesWithPathsAndIndexInRoot, 'DriveManifest.json');
16+
17+
// Ensure arweave required fields exist
18+
expect(manifest.manifest.manifest).to.equal('arweave/paths');
19+
expect(manifest.manifest.version).to.equal('0.1.0');
20+
21+
// Expect index path to be linked to index.html
22+
expect(manifest.manifest.index.path).to.equal('index.html');
23+
24+
// Assert the structure is consistent with provided stub hierarchy
25+
expect(manifest.manifest.paths).to.deep.equal({
26+
'file-in-root': {
27+
id: '0000000000000000000000000000000000000000001'
28+
},
29+
'index.html': {
30+
id: '0000000000000000000000000000000000000000004'
31+
},
32+
'parent-folder/child-folder/file-in-child': {
33+
id: '0000000000000000000000000000000000000000003'
34+
},
35+
'parent-folder/file-in-parent': {
36+
id: '0000000000000000000000000000000000000000002'
37+
}
38+
});
39+
});
40+
41+
it('constructs a manifest compatible with arweave gateways', () => {
42+
const manifest = new ArFSManifestToUpload(stubPublicEntitiesWithPaths, 'DriveManifest.json');
43+
44+
// Ensure arweave required fields exist
45+
expect(manifest.manifest.manifest).to.equal('arweave/paths');
46+
expect(manifest.manifest.version).to.equal('0.1.0');
47+
48+
// Expect index path to be the first file
49+
expect(manifest.manifest.index.path).to.equal('file-in-root');
50+
51+
// Assert the structure is consistent with provided stub hierarchy
52+
expect(manifest.manifest.paths).to.deep.equal({
53+
'file-in-root': {
54+
id: '0000000000000000000000000000000000000000001'
55+
},
56+
'parent-folder/child-folder/file-in-child': {
57+
id: '0000000000000000000000000000000000000000003'
58+
},
59+
'parent-folder/file-in-parent': {
60+
id: '0000000000000000000000000000000000000000002'
61+
}
62+
});
63+
});
64+
65+
it('throws an error when constructed with a hierarchy that has no file entities', () => {
66+
expect(() => new ArFSManifestToUpload(stubEntitiesWithNoFilesWithPaths, 'NameTestManifest.json')).to.throw(
67+
Error,
68+
'Cannot construct a manifest of a folder that has no file entities!'
69+
);
70+
});
71+
72+
it('getBaseFileName function returns the provided name', () => {
73+
const manifest = new ArFSManifestToUpload(stubPublicEntitiesWithPaths, 'NameTestManifest.json');
74+
75+
expect(manifest.getBaseFileName()).to.equal('NameTestManifest.json');
76+
});
77+
78+
it('gatherFileInfo function returns the expected results', () => {
79+
const currentUnixTimeMs = Math.round(Date.now() / 1000);
80+
const manifest = new ArFSManifestToUpload(stubPublicEntitiesWithPaths, 'TestManifest.json');
81+
82+
const { dataContentType, lastModifiedDateMS, fileSize } = manifest.gatherFileInfo();
83+
84+
expect(dataContentType).to.equal('application/x.arweave-manifest+json');
85+
expect(+lastModifiedDateMS).to.equal(currentUnixTimeMs);
86+
expect(+fileSize).to.equal(336);
87+
});
88+
89+
it('getFileDataBuffer function returns a compatible Buffer we can use to upload', () => {
90+
const manifest = new ArFSManifestToUpload(stubPublicEntitiesWithPaths, 'TestManifest.json');
91+
92+
expect(manifest.getFileDataBuffer() instanceof Buffer).to.be.true;
93+
});
94+
95+
describe('getLinksOutput function', () => {
96+
it('produces compatible links with a standard hierarchy', () => {
97+
const manifest = new ArFSManifestToUpload(stubPublicEntitiesWithPaths, 'TestManifest.json');
98+
99+
const linksOutput = manifest.getLinksOutput(stubTransactionID);
100+
101+
expect(linksOutput.length).to.equal(4);
102+
expect(linksOutput[0]).to.equal(`https://arweave.net/${stubTransactionID}`);
103+
expect(linksOutput[1]).to.equal(`https://arweave.net/${stubTransactionID}/file-in-root`);
104+
expect(linksOutput[2]).to.equal(
105+
`https://arweave.net/${stubTransactionID}/parent-folder/child-folder/file-in-child`
106+
);
107+
expect(linksOutput[3]).to.equal(`https://arweave.net/${stubTransactionID}/parent-folder/file-in-parent`);
108+
});
109+
110+
it('produces compatible links with a hierarchy of one single file', () => {
111+
const manifest = new ArFSManifestToUpload(stubEntitiesWithOneFileWithPaths, 'TestManifest.json');
112+
113+
const linksOutput = manifest.getLinksOutput(stubTransactionID);
114+
115+
expect(linksOutput.length).to.equal(2);
116+
expect(linksOutput[0]).to.equal(`https://arweave.net/${stubTransactionID}`);
117+
expect(linksOutput[1]).to.equal(`https://arweave.net/${stubTransactionID}/file-in-root`);
118+
});
119+
120+
it('produces compatible links with a hierarchy of one nested file', () => {
121+
const manifest = new ArFSManifestToUpload(stubEntitiesWithNestedFileWithPaths, 'TestManifest.json');
122+
123+
const linksOutput = manifest.getLinksOutput(stubTransactionID);
124+
125+
expect(linksOutput.length).to.equal(2);
126+
expect(linksOutput[0]).to.equal(`https://arweave.net/${stubTransactionID}`);
127+
expect(linksOutput[1]).to.equal(
128+
`https://arweave.net/${stubTransactionID}/parent-folder/child-folder/file-in-child`
129+
);
130+
});
131+
132+
it('produces compatible links with a hierarchy of entities with special characters', () => {
133+
const manifest = new ArFSManifestToUpload(stubSpecialCharEntitiesWithPaths, 'TestManifest.json');
134+
135+
const linksOutput = manifest.getLinksOutput(stubTransactionID);
136+
137+
expect(linksOutput.length).to.equal(4);
138+
expect(linksOutput[0]).to.equal(`https://arweave.net/${stubTransactionID}`);
139+
expect(linksOutput[1]).to.equal(
140+
`https://arweave.net/${stubTransactionID}/%25%26%40*(%25%26(%40*%3A%22%3E%3F%7B%7D%5B%5D`
141+
);
142+
expect(linksOutput[2]).to.equal(
143+
`https://arweave.net/${stubTransactionID}/~!%40%23%24%25%5E%26*()_%2B%7B%7D%7C%5B%5D%3A%22%3B%3C%3E%3F%2C./%60/'/''_%5C___''_'__/'___'''_/QWERTYUIOPASDFGHJKLZXCVBNM!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%22%3E%3F`
144+
);
145+
expect(linksOutput[3]).to.equal(
146+
`https://arweave.net/${stubTransactionID}/~!%40%23%24%25%5E%26*()_%2B%7B%7D%7C%5B%5D%3A%22%3B%3C%3E%3F%2C./%60/dwijqndjqwnjNJKNDKJANKDNJWNJIvmnbzxnmvbcxvbm%2Cuiqwerioeqwndjkla`
147+
);
148+
});
149+
});
150+
});
151+
152+
describe('ArFSFileToUpload class', () => {
153+
let fileToUpload: ArFSFileToUpload;
154+
155+
beforeEach(() => {
156+
// Start each test with a newly wrapped file
157+
fileToUpload = wrapFileOrFolder('./test_wallet.json') as ArFSFileToUpload;
158+
});
159+
160+
it('throws an error on construction if file max size limit is exceeded', () => {
161+
expect(
162+
() => new ArFSFileToUpload('./test_wallet.json', { ...fileToUpload.fileStats, size: 2_147_483_647 })
163+
).to.throw(Error, 'Files greater than "2147483646" bytes are not yet supported!');
164+
});
165+
166+
it('gatherFileInfo function returns the expected results', () => {
167+
const { dataContentType, fileSize, lastModifiedDateMS } = fileToUpload.gatherFileInfo();
168+
169+
expect(dataContentType).to.equal('application/json');
170+
expect(+fileSize).to.equal(3204);
171+
172+
// Last modified date varies between local dev environments and CI environment
173+
const expectedLastModifiedDate = fileToUpload.lastModifiedDate;
174+
expect(+lastModifiedDateMS).to.equal(+expectedLastModifiedDate);
175+
});
176+
177+
it('getFileDataBuffer function returns a compatible Buffer we can use to upload', () => {
178+
expect(fileToUpload.getFileDataBuffer() instanceof Buffer).to.be.true;
179+
});
180+
181+
it('getBaseFileName function returns the correct name', () => {
182+
expect(fileToUpload.getBaseFileName()).to.equal('test_wallet.json');
183+
});
184+
185+
it('encryptedDataSize function returns the expected size', () => {
186+
expect(+fileToUpload.encryptedDataSize()).to.equal(3220);
187+
});
188+
189+
it('getBaseCosts function throws an error if base costs are not set', () => {
190+
expect(() => fileToUpload.getBaseCosts()).to.throw(Error, 'Base costs on file were never set!');
191+
});
192+
193+
it('getBaseCosts function returns any assigned base costs', () => {
194+
fileToUpload.baseCosts = { fileDataBaseReward: W(1), metaDataBaseReward: W(2) };
195+
196+
expect(+fileToUpload.getBaseCosts().fileDataBaseReward).to.equal(1);
197+
expect(+fileToUpload.getBaseCosts().metaDataBaseReward).to.equal(2);
198+
});
199+
});
200+
201+
describe('ArFSFolderToUpload class', () => {
202+
let folderToUpload: ArFSFolderToUpload;
203+
204+
beforeEach(() => {
205+
// Start each test with a newly wrapped folder
206+
folderToUpload = wrapFileOrFolder('./tests/stub_files/bulk_root_folder') as ArFSFolderToUpload;
207+
});
208+
209+
it('getTotalByteCount function returns the expected size', () => {
210+
expect(+folderToUpload.getTotalByteCount()).to.equal(52);
211+
});
212+
213+
it('getTotalByteCount function returns the expected size if files are to be encrypted', () => {
214+
expect(+folderToUpload.getTotalByteCount(true)).to.equal(116);
215+
});
216+
217+
it('getBaseFileName function returns the correct name', () => {
218+
expect(folderToUpload.getBaseFileName()).to.equal('bulk_root_folder');
219+
});
220+
221+
it('getBaseCosts function throws an error if base costs are not set', () => {
222+
expect(() => folderToUpload.getBaseCosts()).to.throw(Error, 'Base costs on folder were never set!');
223+
});
224+
225+
it('getBaseCosts function returns any assigned base costs', () => {
226+
folderToUpload.baseCosts = { metaDataBaseReward: W(1) };
227+
228+
expect(+folderToUpload.getBaseCosts().metaDataBaseReward).to.equal(1);
229+
});
230+
});

0 commit comments

Comments
 (0)