Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ability to create deleted file/directory again #216

Merged
merged 5 commits into from
Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
64 changes: 54 additions & 10 deletions src/content-items/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import { Connection } from '../connection/connection'
import { utils } from 'ethers'
import { Reference, RequestOptions } from '@ethersphere/bee-js'
import { getUnixTimestamp } from '../utils/time'
import { writeFeedData } from '../feed/api'
import { deleteFeedData, writeFeedData } from '../feed/api'
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 { 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,
IgorShadurin marked this conversation as resolved.
Show resolved Hide resolved
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
60 changes: 55 additions & 5 deletions src/content-items/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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 } 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'

/**
* Get raw metadata by path
Expand Down Expand Up @@ -56,9 +57,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 +117,54 @@ 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
}
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