Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
25 changes: 25 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The different use cases currently available in the package are classified below,
- [Get the size of Downloading all the files of a Dataset Version](#get-the-size-of-downloading-all-the-files-of-a-dataset-version)
- [Get User Permissions on a File](#get-user-permissions-on-a-file)
- [List Files in a Dataset](#list-files-in-a-dataset)
- [Is File Deleted](#is-file-deleted)
- [Files write use cases](#files-write-use-cases)
- [File Uploading Use Cases](#file-uploading-use-cases)
- [Delete a File](#delete-a-file)
Expand Down Expand Up @@ -1571,6 +1572,30 @@ If restrict is false then enableAccessRequest and termsOfAccess are ignored
If restrict is true and enableAccessRequest is false then termsOfAccess is required.
The enableAccessRequest and termsOfAccess are applied to the Draft version of the Dataset and affect all of the restricted files in said Draft version.

#### Is File Deleted

Check if the file has been deleted, return a boolean.

##### Example call:

```typescript
import { isFileDeleted } from '@iqss/dataverse-client-javascript'

/* ... */

const fileId = 12345

await isFileDeleted.execute(fileId).then((isDeleted: boolean) => {
/* ... */
})

/* ... */
```

_See [use case](../src/files/domain/useCases/isFileDeleted.ts) implementation_.

The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.

## Metadata Blocks

### Metadata Blocks read use cases
Expand Down
2 changes: 2 additions & 0 deletions src/files/domain/repositories/IFilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,6 @@ export interface IFilesRepository {
categories: string[],
replace?: boolean
): Promise<void>

isFileDeleted(fileId: number | string): Promise<boolean>
}
16 changes: 16 additions & 0 deletions src/files/domain/useCases/IsFileDeleted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IFilesRepository } from '../repositories/IFilesRepository'
import { UseCase } from '../../../core/domain/useCases/UseCase'

export class IsFileDeleted implements UseCase<boolean> {
constructor(private readonly filesRepository: IFilesRepository) {}

/**
* Returns a boolean, indicating whether the file has been deleted or not.
*
* @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
* @returns {Promise<boolean>} - A boolean indicating whether the file has been deleted or not.
*/
async execute(fileId: number | string): Promise<boolean> {
return await this.filesRepository.isFileDeleted(fileId)
}
}
5 changes: 4 additions & 1 deletion src/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { RestrictFile } from './domain/useCases/RestrictFile'
import { UpdateFileMetadata } from './domain/useCases/UpdateFileMetadata'
import { UpdateFileTabularTags } from './domain/useCases/UpdateFileTabularTags'
import { UpdateFileCategories } from './domain/useCases/UpdateFileCategories'
import { IsFileDeleted } from './domain/useCases/IsFileDeleted'

const filesRepository = new FilesRepository()
const directUploadClient = new DirectUploadClient(filesRepository)
Expand All @@ -38,6 +39,7 @@ const restrictFile = new RestrictFile(filesRepository)
const updateFileMetadata = new UpdateFileMetadata(filesRepository)
const updateFileTabularTags = new UpdateFileTabularTags(filesRepository)
const updateFileCategories = new UpdateFileCategories(filesRepository)
const isFileDeleted = new IsFileDeleted(filesRepository)

export {
getDatasetFiles,
Expand All @@ -56,7 +58,8 @@ export {
updateFileMetadata,
updateFileTabularTags,
updateFileCategories,
replaceFile
replaceFile,
isFileDeleted
}

export { FileModel as File, FileEmbargo, FileChecksum } from './domain/models/FileModel'
Expand Down
8 changes: 8 additions & 0 deletions src/files/infra/repositories/FilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,12 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
throw error
})
}

public async isFileDeleted(fileId: number | string): Promise<boolean> {
return this.doGet(this.buildApiEndpoint(this.filesResourceName, 'hasBeenDeleted', fileId), true)
.then((response) => response.data.data)
.catch((error) => {
throw error
})
}
}
166 changes: 166 additions & 0 deletions test/integration/files/FilesRepository.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as crypto from 'crypto'
import { FilesRepository } from '../../../src/files/infra/repositories/FilesRepository'
import {
ApiConfig,
Expand Down Expand Up @@ -39,10 +40,12 @@ import {
import {
createCollectionViaApi,
deleteCollectionViaApi,
publishCollectionViaApi,
setStorageDriverViaApi
} from '../../testHelpers/collections/collectionHelper'
import { RestrictFileDTO } from '../../../src/files/domain/dtos/RestrictFileDTO'
import { DatasetsRepository } from '../../../src/datasets/infra/repositories/DatasetsRepository'
import { DirectUploadClient } from '../../../src/files/infra/clients/DirectUploadClient'

describe('FilesRepository', () => {
const sut: FilesRepository = new FilesRepository()
Expand Down Expand Up @@ -840,6 +843,169 @@ describe('FilesRepository', () => {
})
})

describe('isFileDeleted', () => {
const testTextFile1Name = 'test-file-1.txt'
const testTextFile2Name = 'test-file-2.txt'
const testCollectionAlias = 'isFileDeletedTestCollection'

let deleFileTestDatasetIds: CreatedDatasetIdentifiers
let fileId: number
let singlepartFile: File

const createTestFileUploadDestination = async (file: File, datasetId: number) => {
const destination = await sut.getFileUploadDestination(datasetId, file)
destination.urls = destination.urls.map((url) => url.replace('localstack', 'localhost'))
return destination
}

const calculateBlobChecksum = (blob: Buffer): string => {
return crypto.createHash('md5').update(blob).digest('hex')
}

beforeAll(async () => {
await createCollectionViaApi(testCollectionAlias)
await setStorageDriverViaApi(testCollectionAlias, 'LocalStack')
await publishCollectionViaApi(testCollectionAlias)

deleFileTestDatasetIds = await createDataset.execute(
TestConstants.TEST_NEW_DATASET_DTO,
testCollectionAlias
)

singlepartFile = await createSinglepartFileBlob()

await uploadFileViaApi(deleFileTestDatasetIds.numericId, testTextFile1Name)

const datasetFiles = await sut.getDatasetFiles(
deleFileTestDatasetIds.numericId,
latestDatasetVersionId,
false,
FileOrderCriteria.NAME_AZ
)
fileId = datasetFiles.files[0].id
})

describe('Basic deletion scenarios', () => {
test('should return False if a file has not been deleted', async () => {
const hasBeenDeleted = await sut.isFileDeleted(fileId)
expect(hasBeenDeleted).toBe(false)
})

test('should return error if the dataset is unpublished and the file has been deleted', async () => {
await sut.deleteFile(fileId)

const expectedError = new ReadError(`[404] File with ID ${nonExistentFiledId} not found.`)
await expect(sut.isFileDeleted(nonExistentFiledId)).rejects.toThrow(expectedError)
})

test('should return correctly when the file has or has not been deleted, in a published dataset', async () => {
await uploadFileViaApi(deleFileTestDatasetIds.numericId, testTextFile1Name)
await publishDatasetViaApi(deleFileTestDatasetIds.numericId)
await waitForNoLocks(deleFileTestDatasetIds.numericId, 10)

const datasetFiles = await sut.getDatasetFiles(
deleFileTestDatasetIds.numericId,
latestDatasetVersionId,
false,
FileOrderCriteria.NAME_AZ
)
fileId = datasetFiles.files[0].id

const fileHasNotBeenDeleted = await sut.isFileDeleted(fileId)
expect(fileHasNotBeenDeleted).toBe(false)

await sut.deleteFile(fileId)

const fileHasBeenDeleted = await sut.isFileDeleted(fileId)
expect(fileHasBeenDeleted).toBe(true)
})

test('should return error when file does not exist', async () => {
const expectedError = new ReadError(`[404] File with ID ${nonExistentFiledId} not found.`)
await expect(sut.isFileDeleted(nonExistentFiledId)).rejects.toThrow(expectedError)
})
})

describe('File replacement scenario', () => {
test('should return True when file has been replaced', async () => {
const directUploadSut = new DirectUploadClient(sut)
const progressMock = jest.fn()
const abortController = new AbortController()

// Upload original file
const originalBuffer = Buffer.from(await singlepartFile.arrayBuffer())
const originalDestination = await createTestFileUploadDestination(
singlepartFile,
deleFileTestDatasetIds.numericId
)

const originalStorageId = await directUploadSut.uploadFile(
deleFileTestDatasetIds.numericId,
singlepartFile,
progressMock,
abortController,
originalDestination
)

const originalUploadedFileDTO = {
fileName: singlepartFile.name,
storageId: originalStorageId,
checksumType: 'md5',
checksumValue: calculateBlobChecksum(originalBuffer),
mimeType: singlepartFile.type
}

await sut.addUploadedFilesToDataset(deleFileTestDatasetIds.numericId, [
originalUploadedFileDTO
])

const originalFileId = (
await sut.getDatasetFiles(
deleFileTestDatasetIds.numericId,
DatasetNotNumberedVersion.LATEST,
true,
FileOrderCriteria.NAME_AZ
)
).files[0].id

// Create and upload replacement file
const newFileBlob = await createSinglepartFileBlob(testTextFile2Name, 2000)
const newBuffer = Buffer.from(await newFileBlob.arrayBuffer())
const newDestination = await createTestFileUploadDestination(
newFileBlob,
deleFileTestDatasetIds.numericId
)

const newStorageId = await directUploadSut.uploadFile(
deleFileTestDatasetIds.numericId,
newFileBlob,
progressMock,
abortController,
newDestination
)

const newUploadedFileDTO = {
fileName: newFileBlob.name,
storageId: newStorageId,
checksumType: 'md5',
checksumValue: calculateBlobChecksum(newBuffer),
mimeType: newFileBlob.type
}

await publishDatasetViaApi(deleFileTestDatasetIds.numericId)
await waitForNoLocks(deleFileTestDatasetIds.numericId, 10)

await sut.replaceFile(originalFileId, newUploadedFileDTO)

const isDeleted = await sut.isFileDeleted(originalFileId)
expect(isDeleted).toBe(true)

await deletePublishedDatasetViaApi(deleFileTestDatasetIds.persistentId)
await deleteCollectionViaApi(testCollectionAlias)
})
})
})

describe('restrictFile', () => {
let restrictFileDatasetIds: CreatedDatasetIdentifiers
const testTextFile1Name = 'test-file-1.txt'
Expand Down
33 changes: 33 additions & 0 deletions test/unit/files/IsFileDeleted.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { IFilesRepository } from '../../../src/files/domain/repositories/IFilesRepository'
import { IsFileDeleted } from '../../../src/files/domain/useCases/IsFileDeleted'
import { ReadError } from '../../../src'

describe('execute', () => {
test('should return true when file has been deleted', async () => {
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
filesRepositoryStub.isFileDeleted = jest.fn().mockResolvedValue(true)
const sut = new IsFileDeleted(filesRepositoryStub)

const result = await sut.execute(1)

expect(result).toBe(true)
})

test('should return false when file has not been deleted', async () => {
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
filesRepositoryStub.isFileDeleted = jest.fn().mockResolvedValue(false)
const sut = new IsFileDeleted(filesRepositoryStub)

const result = await sut.execute(1)

expect(result).toBe(false)
})

test('should return error result on repository error', async () => {
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
filesRepositoryStub.isFileDeleted = jest.fn().mockRejectedValue(new ReadError())
const sut = new IsFileDeleted(filesRepositoryStub)

await expect(sut.execute(1)).rejects.toThrow(ReadError)
})
})
Loading