diff --git a/package-lock.json b/package-lock.json index 427dc132..9739e458 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5548,9 +5548,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001407", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001407.tgz", - "integrity": "sha512-4ydV+t4P7X3zH83fQWNDX/mQEzYomossfpViCOx9zHBSMV+rIe3LFqglHHtVyvNl1FhTNxPxs3jei82iqOW04w==", + "version": "1.0.30001465", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001465.tgz", + "integrity": "sha512-HvjgL3MYAJjceTDCcjRnQGjwUz/5qec9n7JPOzUursUoOTIsYCSDOb1l7RsnZE8mjbxG78zVRCKfrBXyvChBag==", "dev": true, "funding": [ { @@ -18602,9 +18602,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001407", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001407.tgz", - "integrity": "sha512-4ydV+t4P7X3zH83fQWNDX/mQEzYomossfpViCOx9zHBSMV+rIe3LFqglHHtVyvNl1FhTNxPxs3jei82iqOW04w==", + "version": "1.0.30001465", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001465.tgz", + "integrity": "sha512-HvjgL3MYAJjceTDCcjRnQGjwUz/5qec9n7JPOzUursUoOTIsYCSDOb1l7RsnZE8mjbxG78zVRCKfrBXyvChBag==", "dev": true }, "chalk": { diff --git a/src/account/mnemonic.ts b/src/account/mnemonic.ts index 380e2193..c453f290 100644 --- a/src/account/mnemonic.ts +++ b/src/account/mnemonic.ts @@ -41,5 +41,5 @@ export async function uploadEncryptedMnemonic( assertUsername(username) assertBase64UrlData(encryptedMnemonic) - return writeFeedDataRaw(connection, username, stringToBytes(encryptedMnemonic), wallet.privateKey) + return writeFeedDataRaw(connection, username, stringToBytes(encryptedMnemonic), wallet) } diff --git a/src/content-items/handler.ts b/src/content-items/handler.ts index 29022615..fdc8f647 100644 --- a/src/content-items/handler.ts +++ b/src/content-items/handler.ts @@ -7,11 +7,12 @@ import { getRawDirectoryMetadataBytes } from '../directory/adapter' import { DIRECTORY_TOKEN, FILE_TOKEN } from '../file/handler' import { assertRawDirectoryMetadata, combine, splitPath } from '../directory/utils' import { RawDirectoryMetadata } from '../pod/types' -import { assertItemIsNotExists, getRawMetadata } from './utils' +import { deleteFeedData, getPathInfo, getRawMetadata } from './utils' import { RawMetadataWithEpoch } from './types' import { prepareEthAddress } from '../utils/wallet' import { PodPasswordBytes } from '../utils/encryption' import { DataUploadOptions } from '../file/types' +import { getNextEpoch } from '../feed/lookup/utils' export const DEFAULT_UPLOAD_OPTIONS: DataUploadOptions = { blockSize: 1000000, @@ -49,7 +50,6 @@ export async function addEntryToDirectory( const address = prepareEthAddress(wallet.address) const itemText = isFile ? 'File' : 'Directory' const fullPath = combine(...splitPath(parentPath), entryPath) - await assertItemIsNotExists(itemText, connection.bee, fullPath, address, downloadOptions) let parentData: RawDirectoryMetadata | undefined let metadataWithEpoch: RawMetadataWithEpoch | undefined @@ -65,7 +65,7 @@ export async function addEntryToDirectory( parentData.fileOrDirNames = parentData.fileOrDirNames ?? [] if (parentData.fileOrDirNames.includes(itemToAdd)) { - throw new Error(`${itemText} already listed in the parent directory list`) + throw new Error(`${itemText} "${fullPath}" already listed in the parent directory list`) } parentData.fileOrDirNames.push(itemToAdd) @@ -75,12 +75,50 @@ export async function addEntryToDirectory( connection, parentPath, getRawDirectoryMetadataBytes(parentData), - wallet.privateKey, + wallet, podPassword, - metadataWithEpoch.epoch.getNextEpoch(getUnixTimestamp()), + getNextEpoch(metadataWithEpoch.epoch), ) } +/** + * Uploads magic word data on the next epoch's level + * + * Magic word should be uploaded if data is not found (in case of data pruning) or not deleted + */ +export async function deleteItem( + connection: Connection, + itemMetaPath: string, + wallet: utils.HDNode, + podPassword: PodPasswordBytes, +): Promise { + let pathInfo + try { + pathInfo = await getPathInfo( + connection.bee, + itemMetaPath, + prepareEthAddress(wallet.address), + connection.options?.requestOptions, + ) + // eslint-disable-next-line no-empty + } catch (e) {} + + // if the item already deleted - do nothing + if (pathInfo?.isDeleted) { + return + } + + let metaPathEpoch + + // if information is stored under the path, calculate the next level of epoch + if (pathInfo) { + pathInfo.lookupAnswer.epoch.level = pathInfo.lookupAnswer.epoch.getNextLevel(pathInfo.lookupAnswer.epoch.time) + metaPathEpoch = pathInfo.lookupAnswer.epoch + } + + await deleteFeedData(connection, itemMetaPath, wallet, podPassword, metaPathEpoch) +} + /** * Removes file or directory from the parent directory * @@ -111,17 +149,23 @@ export async function removeEntryFromDirectory( const parentData = metadataWithEpoch.metadata assertRawDirectoryMetadata(parentData) const itemToRemove = (isFile ? FILE_TOKEN : DIRECTORY_TOKEN) + entryPath + const fileOrDirNames = parentData.fileOrDirNames || [] - if (parentData.fileOrDirNames) { - parentData.fileOrDirNames = parentData.fileOrDirNames.filter(name => name !== itemToRemove) + const fullPath = combine(parentPath, entryPath) + + if (!fileOrDirNames.includes(itemToRemove)) { + throw new Error(`Item "${fullPath}" not found in the list of items`) } + parentData.fileOrDirNames = fileOrDirNames.filter(name => name !== itemToRemove) + await deleteItem(connection, fullPath, wallet, podPassword) + return writeFeedData( connection, parentPath, getRawDirectoryMetadataBytes(parentData), - wallet.privateKey, + wallet, podPassword, - metadataWithEpoch.epoch.getNextEpoch(getUnixTimestamp()), + getNextEpoch(metadataWithEpoch.epoch), ) } diff --git a/src/content-items/types.ts b/src/content-items/types.ts index 2d638abf..1cac9bb2 100644 --- a/src/content-items/types.ts +++ b/src/content-items/types.ts @@ -2,6 +2,17 @@ import { Epoch } from '../feed/lookup/epoch' import { RawDirectoryMetadata, RawFileMetadata } from '../pod/types' import { RequestOptions } from '@ethersphere/bee-js' import { CacheInfo } from '../cache/types' +import { LookupAnswer } from '../feed/types' + +/** + * Information with the deletion status of the path + */ +export interface PathInformation { + // if the data under the path is the deletion magic word + isDeleted: boolean + // data `LookupAnswer` + lookupAnswer: LookupAnswer +} /** * Download data options diff --git a/src/content-items/utils.ts b/src/content-items/utils.ts index 0bfb558f..e7fec6ec 100644 --- a/src/content-items/utils.ts +++ b/src/content-items/utils.ts @@ -1,11 +1,16 @@ import { Bee, Reference, RequestOptions } from '@ethersphere/bee-js' import { EthAddress } from '@ethersphere/bee-js/dist/types/utils/eth' import { RawDirectoryMetadata, RawFileMetadata } from '../pod/types' -import { getFeedData } from '../feed/api' +import { DELETE_FEED_MAGIC_WORD, getFeedData, writeFeedData } from '../feed/api' import { isRawDirectoryMetadata, isRawFileMetadata } from '../directory/utils' -import { DirectoryItem, FileItem, RawMetadataWithEpoch } from './types' +import { DirectoryItem, FileItem, PathInformation, RawMetadataWithEpoch } from './types' import { decryptJson, PodPasswordBytes } from '../utils/encryption' import CryptoJS from 'crypto-js' +import { isObject } from '../utils/type' +import { Connection } from '../connection/connection' +import { utils, Wallet } from 'ethers' +import { Epoch } from '../feed/lookup/epoch' +import { stringToBytes } from '../utils/bytes' /** * Get raw metadata by path @@ -56,9 +61,7 @@ export async function isItemExists( requestOptions: RequestOptions | undefined, ): Promise { try { - await getFeedData(bee, fullPath, address, requestOptions) - - return true + return (await getFeedData(bee, fullPath, address, requestOptions)).data.text() === DELETE_FEED_MAGIC_WORD } catch (e) { return false } @@ -118,3 +121,67 @@ export function rawFileMetadataToFileItem(item: RawFileMetadata): FileItem { reference, } } + +/** + * Gets `PathInformation` under the path + */ +export async function getPathInfo( + bee: Bee, + path: string, + address: EthAddress, + requestOptions?: RequestOptions, +): Promise { + const lookupAnswer = await getFeedData(bee, path, address, requestOptions) + + return { + isDeleted: lookupAnswer.data.text() === DELETE_FEED_MAGIC_WORD, + lookupAnswer, + } +} + +/** + * Asserts that metadata marked as deleted with the magic word + */ +export function assertItemDeleted(value: PathInformation, path: string): asserts value is PathInformation { + const data = value as PathInformation + + if (!(isObject(data) && data.isDeleted)) { + throw new Error(`Item under the path "${path}" is not deleted`) + } +} + +/** + * Gets `PathInformation` for creation and uploading metadata purposes + * + * In case metadata is available for uploading under the path, the method will return `PathInformation`. + * In other case it will return `undefined`. + */ +export async function getCreationPathInfo( + bee: Bee, + fullPath: string, + address: EthAddress, + requestOptions?: RequestOptions, +): Promise { + // check that if directory uploaded - than it should be marked as deleted + let pathInfo + try { + pathInfo = await getPathInfo(bee, fullPath, address, requestOptions) + assertItemDeleted(pathInfo, fullPath) + // eslint-disable-next-line no-empty + } catch (e) {} + + return pathInfo +} + +/** + * Deletes feed data for `topic` using owner's `wallet` + */ +export async function deleteFeedData( + connection: Connection, + topic: string, + wallet: utils.HDNode | Wallet, + podPassword: PodPasswordBytes, + epoch?: Epoch, +): Promise { + return writeFeedData(connection, topic, stringToBytes(DELETE_FEED_MAGIC_WORD), wallet, podPassword, epoch) +} diff --git a/src/directory/handler.ts b/src/directory/handler.ts index d61f5126..0a83d40c 100644 --- a/src/directory/handler.ts +++ b/src/directory/handler.ts @@ -1,6 +1,6 @@ import { writeFeedData } from '../feed/api' import { EthAddress } from '@ethersphere/bee-js/dist/types/utils/eth' -import { Bee, PrivateKeyBytes, Reference, RequestOptions } from '@ethersphere/bee-js' +import { Bee, Reference, RequestOptions } from '@ethersphere/bee-js' import { assertDirectoryName, assertPartsLength, @@ -17,11 +17,18 @@ import { createRawDirectoryMetadata, META_VERSION } from '../pod/utils' import { Connection } from '../connection/connection' import { utils } from 'ethers' import { addEntryToDirectory, DEFAULT_UPLOAD_OPTIONS } from '../content-items/handler' -import { rawDirectoryMetadataToDirectoryItem, rawFileMetadataToFileItem, getRawMetadata } from '../content-items/utils' +import { + rawDirectoryMetadataToDirectoryItem, + rawFileMetadataToFileItem, + getRawMetadata, + getCreationPathInfo, +} from '../content-items/utils' import { PodPasswordBytes } from '../utils/encryption' -import { preparePrivateKey } from '../utils/wallet' import { DataUploadOptions } from '../file/types' import { DirectoryItem } from '../content-items/types' +import { prepareEthAddress } from '../utils/wallet' +import { Epoch } from '../feed/lookup/epoch' +import { getNextEpoch } from '../feed/lookup/utils' /** * Options for uploading a directory @@ -107,19 +114,21 @@ export async function readDirectory( * @param path parent path * @param name name of the directory * @param podPassword bytes for data encryption from pod metadata - * @param privateKey private key for uploading data to the network + * @param wallet feed owner's wallet + * @param epoch epoch where directory info should be uploaded */ async function createDirectoryInfo( connection: Connection, path: string, name: string, podPassword: PodPasswordBytes, - privateKey: PrivateKeyBytes, + wallet: utils.HDNode, + epoch?: Epoch, ): Promise { const now = getUnixTimestamp() const metadata = createRawDirectoryMetadata(META_VERSION, path, name, now, now, now) - return writeFeedData(connection, combine(...splitPath(path), name), metadata, privateKey, podPassword) + return writeFeedData(connection, combine(...splitPath(path), name), metadata, wallet, podPassword, epoch) } /** @@ -127,14 +136,14 @@ async function createDirectoryInfo( * * @param connection Bee connection * @param podPassword bytes for data encryption - * @param privateKey private key for uploading data to the network + * @param wallet feed owner's wallet */ export async function createRootDirectory( connection: Connection, podPassword: PodPasswordBytes, - privateKey: PrivateKeyBytes, + wallet: utils.HDNode, ): Promise { - return createDirectoryInfo(connection, '', '/', podPassword, privateKey) + return createDirectoryInfo(connection, '', '/', podPassword, wallet) } /** @@ -158,8 +167,20 @@ export async function createDirectory( const name = parts[parts.length - 1] assertDirectoryName(name) - const privateKey = preparePrivateKey(podWallet.privateKey) const parentPath = getPathFromParts(parts, 1) + const pathInfo = await getCreationPathInfo( + connection.bee, + fullPath, + prepareEthAddress(podWallet.address), + connection.options?.requestOptions, + ) await addEntryToDirectory(connection, podWallet, podPassword, parentPath, name, false, downloadOptions) - await createDirectoryInfo(connection, parentPath, name, podPassword, privateKey) + await createDirectoryInfo( + connection, + parentPath, + name, + podPassword, + podWallet, + getNextEpoch(pathInfo?.lookupAnswer.epoch), + ) } diff --git a/src/feed/api.ts b/src/feed/api.ts index 06ba386a..ea113e86 100644 --- a/src/feed/api.ts +++ b/src/feed/api.ts @@ -8,6 +8,12 @@ import { getUnixTimestamp } from '../utils/time' import { LookupAnswer } from './types' import { Connection } from '../connection/connection' import { encryptBytes, PodPasswordBytes } from '../utils/encryption' +import { utils, Wallet } from 'ethers' + +/** + * Magic word for replacing content after deletion + */ +export const DELETE_FEED_MAGIC_WORD = '__Fair__' /** * Finds and downloads the latest feed content @@ -39,7 +45,7 @@ export async function getFeedData( * @param connection connection information for data uploading * @param topic key for data * @param data data to upload - * @param privateKey private key to sign data + * @param wallet feed owner's wallet * @param podPassword bytes for data encryption from pod metadata * @param epoch feed epoch */ @@ -47,13 +53,13 @@ export async function writeFeedData( connection: Connection, topic: string, data: Uint8Array, - privateKey: string | Uint8Array, + wallet: utils.HDNode | Wallet, podPassword: PodPasswordBytes, epoch?: Epoch, ): Promise { data = encryptBytes(podPassword, data) - return writeFeedDataRaw(connection, topic, data, privateKey, epoch) + return writeFeedDataRaw(connection, topic, data, wallet, epoch) } /** @@ -64,14 +70,14 @@ export async function writeFeedData( * @param connection connection information for data uploading * @param topic key for data * @param data data to upload - * @param privateKey private key to sign data + * @param wallet feed owner's wallet * @param epoch feed epoch */ export async function writeFeedDataRaw( connection: Connection, topic: string, data: Uint8Array, - privateKey: string | Uint8Array, + wallet: utils.HDNode | Wallet, epoch?: Epoch, ): Promise { if (!epoch) { @@ -80,7 +86,7 @@ export async function writeFeedDataRaw( const topicHash = bmtHashString(topic) const id = getId(topicHash, epoch.time, epoch.level) - const socWriter = connection.bee.makeSOCWriter(privateKey) + const socWriter = connection.bee.makeSOCWriter(wallet.privateKey) return socWriter.upload(connection.postageBatchId, id, data) } diff --git a/src/feed/lookup/utils.ts b/src/feed/lookup/utils.ts new file mode 100644 index 00000000..6cd9c21f --- /dev/null +++ b/src/feed/lookup/utils.ts @@ -0,0 +1,9 @@ +import { Epoch } from './epoch' +import { getUnixTimestamp } from '../../utils/time' + +/** + * Gets next epoch if epoch is defined + */ +export function getNextEpoch(epoch: Epoch | undefined): Epoch | undefined { + return epoch ? epoch.getNextEpoch(getUnixTimestamp()) : undefined +} diff --git a/src/file/file.ts b/src/file/file.ts index 80c06e19..39580ecc 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -150,7 +150,7 @@ export class File { meta = updateFileMetadata(meta, parentPath, fileName) const fullPath = combine(...splitPath(parentPath), fileName) await addEntryToDirectory(connection, podWallet, pod.password, parentPath, fileName, true) - await writeFeedData(connection, fullPath, getFileMetadataRawBytes(meta), podWallet.privateKey, pod.password) + await writeFeedData(connection, fullPath, getFileMetadataRawBytes(meta), podWallet, pod.password) return meta } diff --git a/src/file/handler.ts b/src/file/handler.ts index 227c4630..36165f53 100644 --- a/src/file/handler.ts +++ b/src/file/handler.ts @@ -12,7 +12,7 @@ import { import { FileMetadata } from '../pod/types' import { blocksToManifest, getFileMetadataRawBytes, rawFileMetadataToFileMetadata } from './adapter' import { assertRawFileMetadata } from '../directory/utils' -import { getRawMetadata } from '../content-items/utils' +import { getCreationPathInfo, getRawMetadata } from '../content-items/utils' import { PodPasswordBytes } from '../utils/encryption' import { Blocks, DataUploadOptions } from './types' import { assertPodName, getExtendedPodsListByAccountData, META_VERSION } from '../pod/utils' @@ -20,6 +20,9 @@ import { getUnixTimestamp } from '../utils/time' import { addEntryToDirectory } from '../content-items/handler' import { writeFeedData } from '../feed/api' import { AccountData } from '../account/account-data' +import { prepareEthAddress } from '../utils/wallet' +import { assertWallet } from '../utils/type' +import { getNextEpoch } from '../feed/lookup/utils' /** * File prefix @@ -119,10 +122,18 @@ export async function uploadData( assertPodName(podName) assertFullPathWithName(fullPath) assertPodName(podName) + assertWallet(accountData.wallet) data = typeof data === 'string' ? stringToBytes(data) : data const connection = accountData.connection const { podWallet, pod } = await getExtendedPodsListByAccountData(accountData, podName) + + const fullPathInfo = await getCreationPathInfo( + connection.bee, + fullPath, + prepareEthAddress(podWallet.address), + connection.options?.requestOptions, + ) const pathInfo = extractPathInfo(fullPath) const now = getUnixTimestamp() const blocksCount = Math.ceil(data.length / options.blockSize) @@ -155,7 +166,14 @@ export async function uploadData( } await addEntryToDirectory(connection, podWallet, pod.password, pathInfo.path, pathInfo.filename, true) - await writeFeedData(connection, fullPath, getFileMetadataRawBytes(meta), podWallet.privateKey, pod.password) + await writeFeedData( + connection, + fullPath, + getFileMetadataRawBytes(meta), + podWallet, + pod.password, + getNextEpoch(fullPathInfo?.lookupAnswer.epoch), + ) return meta } diff --git a/src/pod/personal-storage.ts b/src/pod/personal-storage.ts index 1514f677..11e9fad8 100644 --- a/src/pod/personal-storage.ts +++ b/src/pod/personal-storage.ts @@ -16,7 +16,6 @@ import { sharedPodPreparedToSharedPod, podListToJSON, } from './utils' -import { getUnixTimestamp } from '../utils/time' import { getExtendedPodsList } from './api' import { uploadBytes } from '../file/utils' import { stringToBytes } from '../utils/bytes' @@ -25,6 +24,7 @@ import { assertEncryptedReference, EncryptedReference } from '../utils/hex' import { prepareEthAddress, preparePrivateKey } from '../utils/wallet' import { getCacheKey, setEpochCache } from '../cache/utils' import { getPodsList } from './cache/api' +import { getNextEpoch } from '../feed/lookup/utils' export const POD_TOPIC = 'Pods' @@ -104,12 +104,12 @@ export class PersonalStorage { const podsSharedFiltered = podsInfo.podsList.sharedPods.filter(item => item.name !== name) const allPodsData = podListToBytes(podsFiltered, podsSharedFiltered) const wallet = this.accountData.wallet! - const epoch = podsInfo.epoch.getNextEpoch(getUnixTimestamp()) + const epoch = getNextEpoch(podsInfo.epoch) await writeFeedData( this.accountData.connection, POD_TOPIC, allPodsData, - wallet.privateKey, + wallet, preparePrivateKey(wallet.privateKey), epoch, ) diff --git a/src/pod/utils.ts b/src/pod/utils.ts index c0ab3f25..d1dd8370 100644 --- a/src/pod/utils.ts +++ b/src/pod/utils.ts @@ -404,18 +404,11 @@ export async function createPod( } const allPodsData = podListToBytes(pods, sharedPods) - await writeFeedData( - connection, - POD_TOPIC, - allPodsData, - userWallet.privateKey, - preparePrivateKey(userWallet.privateKey), - epoch, - ) + await writeFeedData(connection, POD_TOPIC, allPodsData, userWallet, preparePrivateKey(userWallet.privateKey), epoch) if (isPod(realPod)) { const podWallet = await getWalletByIndex(seed, nextIndex, cacheInfo) - await createRootDirectory(connection, realPod.password, preparePrivateKey(podWallet.privateKey)) + await createRootDirectory(connection, realPod.password, podWallet) } await setEpochCache(cacheInfo, getCacheKey(userWallet.address), { diff --git a/src/utils/type.ts b/src/utils/type.ts index 762d3b48..d9326580 100644 --- a/src/utils/type.ts +++ b/src/utils/type.ts @@ -1,5 +1,6 @@ import { Utils } from '@ethersphere/bee-js' import { POD_PASSWORD_LENGTH, PodPasswordBytes } from './encryption' +import { utils, Wallet } from 'ethers' export type { PublicKey } from '@fairdatasociety/fdp-contracts-js' export const ETH_ADDR_HEX_LENGTH = 40 @@ -82,3 +83,12 @@ export function assertPodPasswordBytes(value: PodPasswordBytes): asserts value i export function isArrayBufferView(value: unknown): value is ArrayBufferView { return ArrayBuffer.isView(value) } + +/** + * Asserts that the given value is a wallet + */ +export function assertWallet(value: unknown): asserts value is utils.HDNode | Wallet { + if (!value) { + throw new Error('Empty wallet') + } +} diff --git a/test/integration/fdp-class.browser.spec.ts b/test/integration/fdp-class.browser.spec.ts index f982f7db..17c0dfd4 100644 --- a/test/integration/fdp-class.browser.spec.ts +++ b/test/integration/fdp-class.browser.spec.ts @@ -533,6 +533,49 @@ describe('Fair Data Protocol class - in browser', () => { }) describe('Directory', () => { + it('should create directories after deletion', async () => { + const pod = generateRandomHexString() + const path1Name = generateRandomHexString() + const path1Full = `/${path1Name}` + + const { listFiles1, listFiles2, listFiles3 } = await page.evaluate( + async (pod: string, path1Full: string) => { + const reuploadTimes = 3 + const fdp = eval(await window.initFdp()) as FdpStorage + fdp.account.createWallet() + + await fdp.personalStorage.create(pod) + await fdp.directory.create(pod, path1Full) + const list1 = await fdp.directory.read(pod, '/') + + let list2 + let list3 + for (let i = 0; i < reuploadTimes; i++) { + await fdp.directory.delete(pod, path1Full) + list2 = await fdp.directory.read(pod, '/') + + await fdp.directory.create(pod, path1Full) + list3 = await fdp.directory.read(pod, '/') + } + + return { + listFiles1: list1.directories, + listFiles2: list2?.directories, + listFiles3: list3?.directories, + } + }, + pod, + path1Full, + ) + + expect(listFiles1).toHaveLength(1) + expect(listFiles2).toHaveLength(0) + expect(listFiles3).toHaveLength(1) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(listFiles3[0].name).toEqual(path1Name) + }) + it('should create new directory', async () => { const pod = generateRandomHexString() const directoryName = generateRandomHexString() @@ -551,12 +594,12 @@ describe('Fair Data Protocol class - in browser', () => { await fdp.directory.create(pod, directoryFull) await window.shouldFail( fdp.directory.create(pod, directoryFull), - `Directory "${directoryFull}" already uploaded to the network`, + `Directory "${directoryFull}" already listed in the parent directory list`, ) await fdp.directory.create(pod, directoryFull1) await window.shouldFail( fdp.directory.create(pod, directoryFull1), - `Directory "${directoryFull1}" already uploaded to the network`, + `Directory "${directoryFull1}" already listed in the parent directory list`, ) const list = await fdp.directory.read(pod, '/', true) @@ -715,6 +758,75 @@ describe('Fair Data Protocol class - in browser', () => { }) describe('File', () => { + it('should upload files after deletion', async () => { + const pod = generateRandomHexString() + const fileSizeSmall = 100 + const fileSizeSmall1 = 10 + const fileSizeSmall2 = 1000 + const contentSmall = generateRandomHexString(fileSizeSmall) + const filenameSmall = generateRandomHexString() + '.txt' + const fullFilenameSmallPath = '/' + filenameSmall + const contentSamples = [ + // the same content + contentSmall, + // less data + generateRandomHexString(fileSizeSmall1), + // more data + generateRandomHexString(fileSizeSmall2), + ] + + const { listFiles1, listFiles2, listFiles3, results } = await page.evaluate( + async ( + pod: string, + filenameSmall: string, + fullFilenameSmallPath: string, + contentSmall: string, + contentSamples: string[], + ) => { + const reuploadTimes = 3 + const fdp = eval(await window.initFdp()) as FdpStorage + const { wrapBytesWithHelpers } = window.fdp.Utils + fdp.account.createWallet() + + await fdp.personalStorage.create(pod) + await fdp.file.uploadData(pod, fullFilenameSmallPath, contentSmall) + const list1 = await fdp.directory.read(pod, '/') + + let list2 + let list3 + const results = [] + for (let i = 0; i < reuploadTimes; i++) { + await fdp.file.delete(pod, fullFilenameSmallPath) + list2 = await fdp.directory.read(pod, '/') + + await fdp.file.uploadData(pod, fullFilenameSmallPath, contentSamples[i]) + list3 = await fdp.directory.read(pod, '/') + results.push(wrapBytesWithHelpers(await fdp.file.downloadData(pod, fullFilenameSmallPath)).text()) + } + + return { + listFiles1: list1.files, + listFiles2: list2?.files, + listFiles3: list3?.files, + results, + } + }, + pod, + filenameSmall, + fullFilenameSmallPath, + contentSmall, + contentSamples, + ) + + expect(listFiles1).toHaveLength(1) + expect(listFiles2).toHaveLength(0) + expect(listFiles3).toHaveLength(1) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(listFiles3[0].name).toEqual(filenameSmall) + expect(results).toEqual(contentSamples) + }) + it('should upload small text data as a file', async () => { const pod = generateRandomHexString() const fileSizeSmall = 100 @@ -732,7 +844,7 @@ describe('Fair Data Protocol class - in browser', () => { await fdp.file.uploadData(pod, fullFilenameSmallPath, contentSmall) await window.shouldFail( fdp.file.uploadData(pod, fullFilenameSmallPath, contentSmall), - `File "${fullFilenameSmallPath}" already uploaded to the network`, + `File "${fullFilenameSmallPath}" already listed in the parent directory list`, ) const dataSmall = wrapBytesWithHelpers(await fdp.file.downloadData(pod, fullFilenameSmallPath)).text() diff --git a/test/integration/fdp-class.fairos.spec.ts b/test/integration/fdp-class.fairos.spec.ts index 95cb127d..23edee49 100644 --- a/test/integration/fdp-class.fairos.spec.ts +++ b/test/integration/fdp-class.fairos.spec.ts @@ -330,7 +330,9 @@ describe('Fair Data Protocol with FairOS-dfs', () => { expect(fdpResponse2.directories).toHaveLength(0) // test mixed interaction (directory created from fairos and deleted with fdp) - await fdp.directory.delete(podName1, fullDirectoryName1) + await expect(fdp.directory.delete(podName1, fullDirectoryName1)).rejects.toThrow( + `Item "${fullDirectoryName1}" not found in the list of items`, + ) const fdpResponse3 = await fdp.directory.read(podName1, '/', true) expect(fdpResponse3.directories).toHaveLength(0) const fairosDirs = await fairos.dirLs(podName1) diff --git a/test/integration/fdp-class.spec.ts b/test/integration/fdp-class.spec.ts index 27f1f9b9..f1765ff3 100644 --- a/test/integration/fdp-class.spec.ts +++ b/test/integration/fdp-class.spec.ts @@ -317,6 +317,32 @@ describe('Fair Data Protocol class', () => { }) describe('Directory', () => { + it('should create directories after deletion', async () => { + const reuploadTimes = 3 + const fdp = createFdp() + generateUser(fdp) + const pod = generateRandomHexString() + const path1Name = generateRandomHexString() + const path1Full = `/${path1Name}` + + await fdp.personalStorage.create(pod) + await fdp.directory.create(pod, path1Full) + const list1 = await fdp.directory.read(pod, '/') + expect(list1.directories).toHaveLength(1) + expect(list1.directories[0].name).toEqual(path1Name) + + for (let i = 0; i < reuploadTimes; i++) { + await fdp.directory.delete(pod, path1Full) + const list2 = await fdp.directory.read(pod, '/') + expect(list2.directories).toHaveLength(0) + + await fdp.directory.create(pod, path1Full) + const list3 = await fdp.directory.read(pod, '/') + expect(list3.directories).toHaveLength(1) + expect(list3.directories[0].name).toEqual(path1Name) + } + }) + it('should create new directory', async () => { const fdp = createFdp() generateUser(fdp) @@ -330,11 +356,11 @@ describe('Fair Data Protocol class', () => { await expect(fdp.directory.create(pod, directoryFull1)).rejects.toThrow('Parent directory does not exist') await fdp.directory.create(pod, directoryFull) await expect(fdp.directory.create(pod, directoryFull)).rejects.toThrow( - `Directory "${directoryFull}" already uploaded to the network`, + `Directory "${directoryFull}" already listed in the parent directory list`, ) await fdp.directory.create(pod, directoryFull1) - await expect(fdp.directory.create(pod, directoryFull)).rejects.toThrow( - `Directory "${directoryFull}" already uploaded to the network`, + await expect(fdp.directory.create(pod, directoryFull1)).rejects.toThrow( + `Directory "${directoryFull1}" already listed in the parent directory list`, ) const list = await fdp.directory.read(pod, '/', true) expect(list.directories).toHaveLength(1) @@ -497,6 +523,46 @@ describe('Fair Data Protocol class', () => { }) describe('File', () => { + it('should upload files after deletion', async () => { + const reuploadTimes = 3 + const fdp = createFdp() + generateUser(fdp) + const pod = generateRandomHexString() + const fileSizeSmall = 100 + const fileSizeSmall1 = 10 + const fileSizeSmall2 = 1000 + const contentSmall = generateRandomHexString(fileSizeSmall) + const filenameSmall = generateRandomHexString() + '.txt' + const fullFilenameSmallPath = '/' + filenameSmall + + await fdp.personalStorage.create(pod) + await fdp.file.uploadData(pod, fullFilenameSmallPath, contentSmall) + const list1 = await fdp.directory.read(pod, '/') + expect(list1.files).toHaveLength(1) + + const contentSamples = [ + // the same content + contentSmall, + // less data + generateRandomHexString(fileSizeSmall1), + // more data + generateRandomHexString(fileSizeSmall2), + ] + for (let i = 0; i < reuploadTimes; i++) { + await fdp.file.delete(pod, fullFilenameSmallPath) + const list2 = await fdp.directory.read(pod, '/') + expect(list2.files).toHaveLength(0) + + const content = contentSamples[i] + await fdp.file.uploadData(pod, fullFilenameSmallPath, content) + const list3 = await fdp.directory.read(pod, '/') + expect(list3.files).toHaveLength(1) + expect(list3.files[0].name).toEqual(filenameSmall) + const data1 = bytesToString(await fdp.file.downloadData(pod, fullFilenameSmallPath)) + expect(data1).toEqual(content) + } + }) + it('should upload small text data as a file', async () => { const fdp = createFdp() generateUser(fdp) @@ -509,7 +575,7 @@ describe('Fair Data Protocol class', () => { await fdp.personalStorage.create(pod) await fdp.file.uploadData(pod, fullFilenameSmallPath, contentSmall) await expect(fdp.file.uploadData(pod, fullFilenameSmallPath, contentSmall)).rejects.toThrow( - `File "${fullFilenameSmallPath}" already uploaded to the network`, + `File "${fullFilenameSmallPath}" already listed in the parent directory list`, ) const dataSmall = wrapBytesWithHelpers(await fdp.file.downloadData(pod, fullFilenameSmallPath)) expect(dataSmall.text()).toEqual(contentSmall) @@ -797,10 +863,10 @@ describe('Fair Data Protocol class', () => { jest.clearAllMocks() await fdpNoCache.file.delete(pod1, fullFilename) - // update root dir metadata - expect(writeFeedDataSpy).toBeCalledTimes(1) - // get pods info + update root dir metadata - expect(getFeedDataSpy).toBeCalledTimes(2) + // update root dir metadata + write magic word instead of the file + expect(writeFeedDataSpy).toBeCalledTimes(2) + // get pods info + check is file available + update root dir metadata + expect(getFeedDataSpy).toBeCalledTimes(3) // calc the pod wallet expect(getWalletByIndexSpy).toBeCalledTimes(1) @@ -878,10 +944,10 @@ describe('Fair Data Protocol class', () => { jest.clearAllMocks() await fdpWithCache.file.delete(pod1, fullFilename) - // update root dir metadata - expect(writeFeedDataSpy).toBeCalledTimes(1) - // update root dir metadata only. should not get pods info - expect(getFeedDataSpy).toBeCalledTimes(1) + // update root dir metadata + write magic word instead of the file + expect(writeFeedDataSpy).toBeCalledTimes(2) + // update root dir metadata + check is file deleted. should not get pods info + expect(getFeedDataSpy).toBeCalledTimes(2) // the pod wallet is cached expect(getWalletByIndexSpy).toBeCalledTimes(0)