Skip to content

Commit

Permalink
fix: ability to create deleted file/directory again (#216)
Browse files Browse the repository at this point in the history
* fix: ability to create deleted file/directory again

* fix: rebased

* fix: correct calculation of an epoch

* refactor: moved `deleteFeedData` to content-items

* fix: test data
  • Loading branch information
IgorShadurin authored Mar 13, 2023
1 parent 9997e63 commit 988f9e8
Show file tree
Hide file tree
Showing 16 changed files with 428 additions and 69 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/account/mnemonic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
62 changes: 53 additions & 9 deletions src/content-items/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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<void> {
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
*
Expand Down Expand Up @@ -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),
)
}
11 changes: 11 additions & 0 deletions src/content-items/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 72 additions & 5 deletions src/content-items/utils.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -56,9 +61,7 @@ export async function isItemExists(
requestOptions: RequestOptions | undefined,
): Promise<boolean> {
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
}
Expand Down Expand Up @@ -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<PathInformation> {
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<PathInformation | undefined> {
// 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<Reference> {
return writeFeedData(connection, topic, stringToBytes(DELETE_FEED_MAGIC_WORD), wallet, podPassword, epoch)
}
43 changes: 32 additions & 11 deletions src/directory/handler.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -107,34 +114,36 @@ 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<Reference> {
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)
}

/**
* Creates root directory for the pod that tied to the private key
*
* @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<Reference> {
return createDirectoryInfo(connection, '', '/', podPassword, privateKey)
return createDirectoryInfo(connection, '', '/', podPassword, wallet)
}

/**
Expand All @@ -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),
)
}
Loading

0 comments on commit 988f9e8

Please sign in to comment.