Skip to content

Commit

Permalink
Merge pull request #267 from IQSS/feat/258-replace-file-use-case
Browse files Browse the repository at this point in the history
Replace File use case
  • Loading branch information
ofahimIQSS authored Feb 26, 2025
2 parents 351cffc + b681b76 commit 25520a1
Show file tree
Hide file tree
Showing 10 changed files with 601 additions and 5 deletions.
32 changes: 32 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,
- [Files write use cases](#files-write-use-cases)
- [File Uploading Use Cases](#file-uploading-use-cases)
- [Delete a File](#delete-a-file)
- [Replace a File](#replace-a-file)
- [Restrict or Unrestrict a File](#restrict-or-unrestrict-a-file)
- [Metadata Blocks](#metadata-blocks)
- [Metadata Blocks read use cases](#metadata-blocks-read-use-cases)
Expand Down Expand Up @@ -1284,6 +1285,37 @@ Note that the behavior of deleting files depends on if the dataset has ever been
- If the dataset has published, the file is deleted from the draft (and future published versions).
- If the dataset has published, the deleted file can still be downloaded because it was part of a published version.

#### Replace a File

Replaces a File. Currently working for already uploaded S3 bucket files.

##### Example call:

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

/* ... */

const fileId = 12345
const uploadedFileDTO: UploadedFileDTO = {
fileName: 'the-file-name',
storageId: 'localstack1://mybucket:19121faf7e7-2c40322ff54e',
checksumType: 'md5',
checksumValue: 'ede3d3b685b4e137ba4cb2521329a75e',
mimeType: 'text/plain'
}

replaceFile.execute(fileId, uploadedFileDTO)

/* ... */
```

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

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

The `uploadedFileDTO` parameter is a [UploadedFileDTO](../src/files/domain/dtos/UploadedFileDTO.ts) and includes properties related to the uploaded files. Some of these properties should be calculated from the uploaded File Blob objects and the resulting storage identifiers from the Upload File use case.

#### Restrict or Unrestrict a File

Restrict or unrestrict an existing file.
Expand Down
1 change: 1 addition & 0 deletions src/files/domain/dtos/UploadedFileDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface UploadedFileDTO {
checksumValue: string
checksumType: string
mimeType: string
forceReplace?: boolean // Only used in the ReplaceFile use case, whether to allow the mimetype to change
}
2 changes: 2 additions & 0 deletions src/files/domain/repositories/IFilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,7 @@ export interface IFilesRepository {

deleteFile(fileId: number | string): Promise<undefined>

replaceFile(fileId: number | string, uploadedFileDTO: UploadedFileDTO): Promise<undefined>

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

export class ReplaceFile implements UseCase<void> {
private filesRepository: IFilesRepository

constructor(filesRepository: IFilesRepository) {
this.filesRepository = filesRepository
}

/**
* Replaces an existing file.
*
* This method completes the flow initiated by the UploadFile use case, which involves replacing an existing file with a new one just uploaded.
* (https://guides.dataverse.org/en/latest/developers/s3-direct-upload-api.html#replacing-an-existing-file-in-the-dataset)
*
* Note: This use case can be used independently of the UploadFile use case, e.g., supporting scenarios in which the files already exist in S3 or have been uploaded via some out-of-band method.
*
* @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
* @param {UploadedFileDTO} [uploadedFileDTO] - File DTO associated with the uploaded file.
* @returns {Promise<void>} A promise that resolves when the file has been successfully replaced.
* @throws {WriteError} - If there are errors while writing data.
*/
async execute(fileId: number | string, uploadedFileDTO: UploadedFileDTO): Promise<void> {
await this.filesRepository.replaceFile(fileId, uploadedFileDTO)
}
}
4 changes: 3 additions & 1 deletion src/files/domain/useCases/UploadFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export class UploadFile implements UseCase<string> {
* Uploads a file to remote storage and returns the storage identifier.
*
* This use case is based on the Direct Upload API, particularly the first part of the flow, "Requesting Direct Upload of a DataFile".
* To fulfill the flow, you will need to call the AddUploadedFileToDataset Use Case to add the uploaded file to the dataset.
* To fulfill the flow, you could:
* - Call the AddUploadedFilesToDataset Use Case to add the uploaded file to the dataset.
* - Call the ReplaceFile Use Case to replace an existing file with the uploaded one.
* (https://guides.dataverse.org/en/latest/developers/s3-direct-upload-api.html#requesting-direct-upload-of-a-datafile)
*
* @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers) or a number (for numeric identifiers).
Expand Down
3 changes: 3 additions & 0 deletions src/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { UploadFile } from './domain/useCases/UploadFile'
import { DirectUploadClient } from './infra/clients/DirectUploadClient'
import { AddUploadedFilesToDataset } from './domain/useCases/AddUploadedFilesToDataset'
import { DeleteFile } from './domain/useCases/DeleteFile'
import { ReplaceFile } from './domain/useCases/ReplaceFile'
import { RestrictFile } from './domain/useCases/RestrictFile'

const filesRepository = new FilesRepository()
Expand All @@ -29,6 +30,7 @@ const getFileCitation = new GetFileCitation(filesRepository)
const uploadFile = new UploadFile(directUploadClient)
const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository)
const deleteFile = new DeleteFile(filesRepository)
const replaceFile = new ReplaceFile(filesRepository)
const restrictFile = new RestrictFile(filesRepository)

export {
Expand All @@ -44,6 +46,7 @@ export {
uploadFile,
addUploadedFilesToDataset,
deleteFile,
replaceFile,
restrictFile
}

Expand Down
35 changes: 35 additions & 0 deletions src/files/infra/repositories/FilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface UploadedFileRequestBody {
directoryLabel?: string
categories?: string[]
restrict?: boolean
forceReplace?: boolean
}

export interface ChecksumRequestBody {
Expand Down Expand Up @@ -302,6 +303,40 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
})
}

public async replaceFile(
fileId: number | string,
uploadedFileDTO: UploadedFileDTO
): Promise<undefined> {
const requestBody: UploadedFileRequestBody = {
fileName: uploadedFileDTO.fileName,
checksum: {
'@value': uploadedFileDTO.checksumValue,
'@type': uploadedFileDTO.checksumType.toUpperCase()
},
mimeType: uploadedFileDTO.mimeType,
storageIdentifier: uploadedFileDTO.storageId,
forceReplace: uploadedFileDTO.forceReplace,
...(uploadedFileDTO.description && { description: uploadedFileDTO.description }),
...(uploadedFileDTO.categories && { categories: uploadedFileDTO.categories }),
...(uploadedFileDTO.restrict && { restrict: uploadedFileDTO.restrict }),
...(uploadedFileDTO.directoryLabel && { directoryLabel: uploadedFileDTO.directoryLabel })
}

const formData = new FormData()
formData.append('jsonData', JSON.stringify(requestBody))

return this.doPost(
this.buildApiEndpoint(this.filesResourceName, 'replace', fileId),
formData,
{},
ApiConstants.CONTENT_TYPE_MULTIPART_FORM_DATA
)
.then(() => undefined)
.catch((error) => {
throw error
})
}

public async restrictFile(fileId: number | string, restrict: boolean): Promise<undefined> {
return this.doPut(this.buildApiEndpoint(this.filesResourceName, 'restrict', fileId), restrict)
.then(() => undefined)
Expand Down
Loading

0 comments on commit 25520a1

Please sign in to comment.