Skip to content

Commit 25520a1

Browse files
authored
Merge pull request #267 from IQSS/feat/258-replace-file-use-case
Replace File use case
2 parents 351cffc + b681b76 commit 25520a1

File tree

10 files changed

+601
-5
lines changed

10 files changed

+601
-5
lines changed

docs/useCases.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ The different use cases currently available in the package are classified below,
5252
- [Files write use cases](#files-write-use-cases)
5353
- [File Uploading Use Cases](#file-uploading-use-cases)
5454
- [Delete a File](#delete-a-file)
55+
- [Replace a File](#replace-a-file)
5556
- [Restrict or Unrestrict a File](#restrict-or-unrestrict-a-file)
5657
- [Metadata Blocks](#metadata-blocks)
5758
- [Metadata Blocks read use cases](#metadata-blocks-read-use-cases)
@@ -1284,6 +1285,37 @@ Note that the behavior of deleting files depends on if the dataset has ever been
12841285
- If the dataset has published, the file is deleted from the draft (and future published versions).
12851286
- If the dataset has published, the deleted file can still be downloaded because it was part of a published version.
12861287

1288+
#### Replace a File
1289+
1290+
Replaces a File. Currently working for already uploaded S3 bucket files.
1291+
1292+
##### Example call:
1293+
1294+
```typescript
1295+
import { replaceFile } from '@iqss/dataverse-client-javascript'
1296+
1297+
/* ... */
1298+
1299+
const fileId = 12345
1300+
const uploadedFileDTO: UploadedFileDTO = {
1301+
fileName: 'the-file-name',
1302+
storageId: 'localstack1://mybucket:19121faf7e7-2c40322ff54e',
1303+
checksumType: 'md5',
1304+
checksumValue: 'ede3d3b685b4e137ba4cb2521329a75e',
1305+
mimeType: 'text/plain'
1306+
}
1307+
1308+
replaceFile.execute(fileId, uploadedFileDTO)
1309+
1310+
/* ... */
1311+
```
1312+
1313+
_See [use case](../src/files/domain/useCases/ReplaceFile.ts) implementation_.
1314+
1315+
The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.
1316+
1317+
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.
1318+
12871319
#### Restrict or Unrestrict a File
12881320

12891321
Restrict or unrestrict an existing file.

src/files/domain/dtos/UploadedFileDTO.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export interface UploadedFileDTO {
88
checksumValue: string
99
checksumType: string
1010
mimeType: string
11+
forceReplace?: boolean // Only used in the ReplaceFile use case, whether to allow the mimetype to change
1112
}

src/files/domain/repositories/IFilesRepository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,7 @@ export interface IFilesRepository {
6262

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

65+
replaceFile(fileId: number | string, uploadedFileDTO: UploadedFileDTO): Promise<undefined>
66+
6567
restrictFile(fileId: number | string, restrict: boolean): Promise<undefined>
6668
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { UploadedFileDTO } from '../dtos/UploadedFileDTO'
3+
import { IFilesRepository } from '../repositories/IFilesRepository'
4+
5+
export class ReplaceFile implements UseCase<void> {
6+
private filesRepository: IFilesRepository
7+
8+
constructor(filesRepository: IFilesRepository) {
9+
this.filesRepository = filesRepository
10+
}
11+
12+
/**
13+
* Replaces an existing file.
14+
*
15+
* This method completes the flow initiated by the UploadFile use case, which involves replacing an existing file with a new one just uploaded.
16+
* (https://guides.dataverse.org/en/latest/developers/s3-direct-upload-api.html#replacing-an-existing-file-in-the-dataset)
17+
*
18+
* 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.
19+
*
20+
* @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
21+
* @param {UploadedFileDTO} [uploadedFileDTO] - File DTO associated with the uploaded file.
22+
* @returns {Promise<void>} A promise that resolves when the file has been successfully replaced.
23+
* @throws {WriteError} - If there are errors while writing data.
24+
*/
25+
async execute(fileId: number | string, uploadedFileDTO: UploadedFileDTO): Promise<void> {
26+
await this.filesRepository.replaceFile(fileId, uploadedFileDTO)
27+
}
28+
}

src/files/domain/useCases/UploadFile.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export class UploadFile implements UseCase<string> {
1212
* Uploads a file to remote storage and returns the storage identifier.
1313
*
1414
* This use case is based on the Direct Upload API, particularly the first part of the flow, "Requesting Direct Upload of a DataFile".
15-
* To fulfill the flow, you will need to call the AddUploadedFileToDataset Use Case to add the uploaded file to the dataset.
15+
* To fulfill the flow, you could:
16+
* - Call the AddUploadedFilesToDataset Use Case to add the uploaded file to the dataset.
17+
* - Call the ReplaceFile Use Case to replace an existing file with the uploaded one.
1618
* (https://guides.dataverse.org/en/latest/developers/s3-direct-upload-api.html#requesting-direct-upload-of-a-datafile)
1719
*
1820
* @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers) or a number (for numeric identifiers).

src/files/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { UploadFile } from './domain/useCases/UploadFile'
1212
import { DirectUploadClient } from './infra/clients/DirectUploadClient'
1313
import { AddUploadedFilesToDataset } from './domain/useCases/AddUploadedFilesToDataset'
1414
import { DeleteFile } from './domain/useCases/DeleteFile'
15+
import { ReplaceFile } from './domain/useCases/ReplaceFile'
1516
import { RestrictFile } from './domain/useCases/RestrictFile'
1617

1718
const filesRepository = new FilesRepository()
@@ -29,6 +30,7 @@ const getFileCitation = new GetFileCitation(filesRepository)
2930
const uploadFile = new UploadFile(directUploadClient)
3031
const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository)
3132
const deleteFile = new DeleteFile(filesRepository)
33+
const replaceFile = new ReplaceFile(filesRepository)
3234
const restrictFile = new RestrictFile(filesRepository)
3335

3436
export {
@@ -44,6 +46,7 @@ export {
4446
uploadFile,
4547
addUploadedFilesToDataset,
4648
deleteFile,
49+
replaceFile,
4750
restrictFile
4851
}
4952

src/files/infra/repositories/FilesRepository.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface UploadedFileRequestBody {
5151
directoryLabel?: string
5252
categories?: string[]
5353
restrict?: boolean
54+
forceReplace?: boolean
5455
}
5556

5657
export interface ChecksumRequestBody {
@@ -302,6 +303,40 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
302303
})
303304
}
304305

306+
public async replaceFile(
307+
fileId: number | string,
308+
uploadedFileDTO: UploadedFileDTO
309+
): Promise<undefined> {
310+
const requestBody: UploadedFileRequestBody = {
311+
fileName: uploadedFileDTO.fileName,
312+
checksum: {
313+
'@value': uploadedFileDTO.checksumValue,
314+
'@type': uploadedFileDTO.checksumType.toUpperCase()
315+
},
316+
mimeType: uploadedFileDTO.mimeType,
317+
storageIdentifier: uploadedFileDTO.storageId,
318+
forceReplace: uploadedFileDTO.forceReplace,
319+
...(uploadedFileDTO.description && { description: uploadedFileDTO.description }),
320+
...(uploadedFileDTO.categories && { categories: uploadedFileDTO.categories }),
321+
...(uploadedFileDTO.restrict && { restrict: uploadedFileDTO.restrict }),
322+
...(uploadedFileDTO.directoryLabel && { directoryLabel: uploadedFileDTO.directoryLabel })
323+
}
324+
325+
const formData = new FormData()
326+
formData.append('jsonData', JSON.stringify(requestBody))
327+
328+
return this.doPost(
329+
this.buildApiEndpoint(this.filesResourceName, 'replace', fileId),
330+
formData,
331+
{},
332+
ApiConstants.CONTENT_TYPE_MULTIPART_FORM_DATA
333+
)
334+
.then(() => undefined)
335+
.catch((error) => {
336+
throw error
337+
})
338+
}
339+
305340
public async restrictFile(fileId: number | string, restrict: boolean): Promise<undefined> {
306341
return this.doPut(this.buildApiEndpoint(this.filesResourceName, 'restrict', fileId), restrict)
307342
.then(() => undefined)

0 commit comments

Comments
 (0)