Skip to content

Commit e74f00d

Browse files
authored
Merge pull request #62 from ScilifelabDataCentre/divbase-cli-files-improvements
SQ-768: Improve divbase-cli files subcommand
2 parents 84088e7 + 66e7553 commit e74f00d

30 files changed

Lines changed: 1467 additions & 173 deletions

File tree

docs/cli/_auto_generated/files.md

Lines changed: 119 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,74 @@ $ divbase-cli files [OPTIONS] COMMAND [ARGS]...
1616

1717
**Commands**:
1818

19-
* `list`: list all files in the project's DivBase...
19+
* `ls`: list all currently available files in the...
20+
* `info`: Get detailed information about a specific...
2021
* `download`: Download files from the project's store on...
22+
* `stream`: Stream a file's content to standard output.
2123
* `upload`: Upload files to your project's store on...
22-
* `remove`: Remove files from the project's store on...
24+
* `rm`: Soft delete files from the project's store...
25+
* `restore`: Restore soft deleted files from the...
2326

24-
## `divbase-cli files list`
27+
## `divbase-cli files ls`
2528

26-
list all files in the project's DivBase store.
29+
list all currently available files in the project's DivBase store.
2730

28-
To see files at a user specified project version (controlled by the 'divbase-cli version' subcommand),
29-
you can instead use the 'divbase-cli version info [VERSION_NAME]' command.
31+
You can optionally filter the listed files by providing a prefix.
32+
By default, DivBase query results files are hidden from the listing. Use the --include-results-files option to include them.
33+
To see information about the versions of each file, use the 'divbase-cli files info [FILE_NAME]' command instead
3034

3135
**Usage**:
3236

3337
```console
34-
$ divbase-cli files list [OPTIONS]
38+
$ divbase-cli files ls [OPTIONS]
3539
```
3640

3741
**Options**:
3842

43+
* `--tsv`: If set, will print the output in .TSV format for easier programmatic parsing.
44+
* `-p, --prefix TEXT`: Optional prefix to filter the listed files by name (only list files starting with this prefix).
45+
* `-r, --include-results-files`: If set, will also show DivBase query results files which are hidden by default.
46+
* `--project TEXT`: Name of the DivBase project, if not provided uses the default in your DivBase config file
47+
* `-c, --config PATH`: Path to your user configuration file. If you didn't specify a custom path when you created it, you don't need to set this. [default: /home/roryc/.config/divbase/config.yaml]
48+
* `--help`: Show this message and exit.
49+
50+
## `divbase-cli files info`
51+
52+
Get detailed information about a specific file in the project's DivBase store.
53+
54+
This includes all versions of the file and whether the file is currently marked as soft deleted.
55+
56+
**Usage**:
57+
58+
```console
59+
$ divbase-cli files info [OPTIONS] FILE_NAME
60+
```
61+
62+
**Arguments**:
63+
64+
* `FILE_NAME`: Name of the file to get information about. [required]
65+
66+
**Options**:
67+
68+
* `--tsv`: If set, will print the output in .TSV format for easier programmatic parsing.
3969
* `--project TEXT`: Name of the DivBase project, if not provided uses the default in your DivBase config file
4070
* `-c, --config PATH`: Path to your user configuration file. If you didn't specify a custom path when you created it, you don't need to set this. [default: /home/roryc/.config/divbase/config.yaml]
4171
* `--help`: Show this message and exit.
4272

4373
## `divbase-cli files download`
4474

45-
Download files from the project's store on DivBase. This can be done by either:
75+
Download files from the project's store on DivBase.
76+
77+
This can be done by either:
4678
1. providing a list of files paths directly in the command line
47-
2. providing a directory to download the files to.
79+
2. providing a text file with a list of files to download (new file on each line).
80+
81+
To download the latest version of a file, just provide its name. "file1" "file2" etc.
82+
To download a specific/older version of a file, use the format: "file_name:version_id"
83+
You can get a file's version id using the 'divbase-cli file info [FILE_NAME]' command.
84+
You can mix and match latest and specific versions in the same command.
85+
E.g. to download the latest version of file1 and version "3xcdsdsdiw829x"
86+
of file2: 'divbase-cli files download file1 file2:3xcdsdsdiw829x'
4887

4988
**Usage**:
5089

@@ -69,12 +108,42 @@ You can also specify "." to download to the current directory.
69108
* `-c, --config PATH`: Path to your user configuration file. If you didn't specify a custom path when you created it, you don't need to set this. [default: /home/roryc/.config/divbase/config.yaml]
70109
* `--help`: Show this message and exit.
71110

111+
## `divbase-cli files stream`
112+
113+
Stream a file's content to standard output.
114+
115+
This allows your to pipe the output to other tools like 'less', 'head', 'zcat' and 'bcftools'.
116+
117+
Examples:
118+
- View a file: divbase-cli files stream my_file.tsv | less
119+
- View a gzipped file: divbase-cli files stream my_file.vcf.gz | zcat | less
120+
- Run a bcftools command: divbase-cli files stream my_file.vcf.gz | bcftools view -h - # The "-" tells bcftools to read from standard input
121+
122+
**Usage**:
123+
124+
```console
125+
$ divbase-cli files stream [OPTIONS] FILE_NAME
126+
```
127+
128+
**Arguments**:
129+
130+
* `FILE_NAME`: Name of the file you want to stream. [required]
131+
132+
**Options**:
133+
134+
* `--version-id TEXT`: Specify this if you want to look at an older/specific version of the file. If not provided, the latest version of the file is used. To get a file's version ids, use the 'divbase-cli file info [FILE_NAME]' command.
135+
* `--project TEXT`: Name of the DivBase project, if not provided uses the default in your DivBase config file
136+
* `-c, --config PATH`: Path to your user configuration file. If you didn't specify a custom path when you created it, you don't need to set this. [default: /home/roryc/.config/divbase/config.yaml]
137+
* `--help`: Show this message and exit.
138+
72139
## `divbase-cli files upload`
73140

74-
Upload files to your project's store on DivBase by either:
75-
1. providing a list of files paths directly in the command line
76-
2. providing a directory to upload
77-
3. providing a text file with or a file list.
141+
Upload files to your project's store on DivBase:
142+
143+
To provide files to upload you can either:
144+
1. provide a list of files paths directly in the command line
145+
2. provide a directory to upload
146+
3. provide a text file with or a file list.
78147

79148
**Usage**:
80149

@@ -84,7 +153,7 @@ $ divbase-cli files upload [OPTIONS] [FILES]...
84153

85154
**Arguments**:
86155

87-
* `[FILES]...`: Space seperated list of files to upload.
156+
* `[FILES]...`: Space separated list of files to upload.
88157

89158
**Options**:
90159

@@ -95,18 +164,20 @@ $ divbase-cli files upload [OPTIONS] [FILES]...
95164
* `-c, --config PATH`: Path to your user configuration file. If you didn't specify a custom path when you created it, you don't need to set this. [default: /home/roryc/.config/divbase/config.yaml]
96165
* `--help`: Show this message and exit.
97166

98-
## `divbase-cli files remove`
167+
## `divbase-cli files rm`
99168

100-
Remove files from the project's store on DivBase by either:
101-
1. providing a list of files paths directly in the command line
102-
2. providing a text file with or a file list.
169+
Soft delete files from the project's store on DivBase
103170

104-
'dry_run' mode will not actually delete the files, just print what would be deleted.
171+
To provide files to delete you can either:
172+
1. provide a list of file names directly in the command line
173+
2. provide a text file with a list of files to delete.
174+
175+
Note that deleting a non existent file will be treated as a successful deletion.
105176

106177
**Usage**:
107178

108179
```console
109-
$ divbase-cli files remove [OPTIONS] [FILES]...
180+
$ divbase-cli files rm [OPTIONS] [FILES]...
110181
```
111182

112183
**Arguments**:
@@ -115,8 +186,35 @@ $ divbase-cli files remove [OPTIONS] [FILES]...
115186

116187
**Options**:
117188

118-
* `--file-list PATH`: Text file with list of files to upload.
189+
* `--file-list PATH`: Text file with list of files to delete.
119190
* `--dry-run`: If set, will not actually delete the files, just print what would be deleted.
120191
* `--project TEXT`: Name of the DivBase project, if not provided uses the default in your DivBase config file
121192
* `-c, --config PATH`: Path to your user configuration file. If you didn't specify a custom path when you created it, you don't need to set this. [default: /home/roryc/.config/divbase/config.yaml]
122193
* `--help`: Show this message and exit.
194+
195+
## `divbase-cli files restore`
196+
197+
Restore soft deleted files from the project's store on DivBase
198+
199+
To provide files to restore you can either:
200+
1. provide a list of files directly in the command line.
201+
2. provide a text file with a list of files to restore (new file on each line).
202+
203+
NOTE: Attempts to restore a file that is not soft deleted will be considered successful and the file will remain live. This means you can repeatedly run this command on the same file and get the same response.
204+
205+
**Usage**:
206+
207+
```console
208+
$ divbase-cli files restore [OPTIONS] [FILES]...
209+
```
210+
211+
**Arguments**:
212+
213+
* `[FILES]...`: Space seperated list of files/objects in the project's store on DivBase to restore.
214+
215+
**Options**:
216+
217+
* `--file-list PATH`: Text file with list of files to restore.
218+
* `--project TEXT`: Name of the DivBase project, if not provided uses the default in your DivBase config file
219+
* `-c, --config PATH`: Path to your user configuration file. If you didn't specify a custom path when you created it, you don't need to set this. [default: /home/roryc/.config/divbase/config.yaml]
220+
* `--help`: Show this message and exit.

docs/user-guides/quick-start.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ divbase-cli files upload --upload-dir /path/to/directory/
8484
Check your uploaded files:
8585

8686
```bash
87-
divbase-cli files list
87+
divbase-cli files ls
8888
```
8989

9090
## Step 7: Upload sample metadata

docs/user-guides/setup_divbase_cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ divbase-cli auth login [YOUR_EMAIL] # you'll be prompted for your password
4949
You will now be able to run commands against your DivBase projects. Here are some examples:
5050

5151
```bash
52-
divbase-cli files list # runs against the default project
52+
divbase-cli files ls # runs against the default project
5353
divbase-cli files download file1.vcf.gz file2.vcf.gz --project my-non-default-project
5454
divbase-cli files upload file1.vcf.gz file2.vcf.gz --project my-non-default-project
5555
```

packages/divbase-api/src/divbase_api/routes/s3.py

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import logging
1515
from typing import Annotated
1616

17-
from fastapi import APIRouter, Depends, HTTPException, status
17+
from fastapi import APIRouter, Body, Depends, HTTPException, status
1818
from fastapi.concurrency import run_in_threadpool
1919

2020
from divbase_api.api_config import settings
@@ -36,9 +36,13 @@
3636
DownloadObjectRequest,
3737
FileChecksumResponse,
3838
GetPresignedPartUrlsRequest,
39+
ListObjectsRequest,
40+
ListObjectsResponse,
41+
ObjectInfoResponse,
3942
PreSignedDownloadResponse,
4043
PreSignedSinglePartUploadResponse,
4144
PresignedUploadPartUrlResponse,
45+
RestoreObjectsResponse,
4246
UploadSinglePartObjectRequest,
4347
)
4448
from divbase_lib.divbase_constants import MAX_S3_API_BATCH_SIZE
@@ -68,7 +72,7 @@ def check_too_many_objects_in_request(
6872
@s3_router.post("/download", status_code=status.HTTP_200_OK, response_model=list[PreSignedDownloadResponse])
6973
async def generate_download_urls(
7074
project_name: str,
71-
objects_to_download: list[DownloadObjectRequest],
75+
objects_to_download: Annotated[list[DownloadObjectRequest], Body(min_length=1, max_length=MAX_S3_API_BATCH_SIZE)],
7276
s3_signer_service: Annotated[S3PreSignedService, Depends(get_pre_signed_service)],
7377
project_and_user_and_role: tuple[ProjectDB, UserDB, ProjectRoles] = Depends(get_project_member),
7478
):
@@ -89,12 +93,46 @@ async def generate_download_urls(
8993
return response
9094

9195

92-
@s3_router.get("/", status_code=status.HTTP_200_OK, response_model=list[str])
93-
async def list_files(
96+
# Post request instead of GET as GET doesn't support/encourage body content.
97+
@s3_router.post("/list", status_code=status.HTTP_200_OK, response_model=ListObjectsResponse)
98+
async def list_file_details(
99+
project_name: str,
100+
list_request: ListObjectsRequest,
101+
project_and_user_and_role: tuple[ProjectDB, UserDB, ProjectRoles] = Depends(get_project_member),
102+
):
103+
"""
104+
List all files in the project's bucket
105+
106+
You can provide a prefix to only list files that start with that prefix.
107+
The response object includes a next_token if there are more files to list.
108+
Which you can append to the next request to get the next page of results.
109+
1000 files are returned at most per request.
110+
"""
111+
project, current_user, role = project_and_user_and_role
112+
if not has_required_role(role, ProjectRoles.READ):
113+
raise AuthorizationError("You don't have permission to list files in this project.")
114+
115+
s3_file_manager = S3FileManager(
116+
url=settings.s3.endpoint_url,
117+
access_key=settings.s3.access_key.get_secret_value(),
118+
secret_key=settings.s3.secret_key.get_secret_value(),
119+
)
120+
121+
return await run_in_threadpool(
122+
s3_file_manager.list_files_detailed,
123+
bucket_name=project.bucket_name,
124+
prefix=list_request.prefix,
125+
next_token=list_request.next_token,
126+
)
127+
128+
129+
@s3_router.get("/info", status_code=status.HTTP_200_OK, response_model=ObjectInfoResponse)
130+
async def get_object_info(
94131
project_name: str,
132+
object_name: str,
95133
project_and_user_and_role: tuple[ProjectDB, UserDB, ProjectRoles] = Depends(get_project_member),
96134
):
97-
"""List all files in the project's bucket"""
135+
"""Get details about all versions of a specific object/file in the project's store."""
98136
project, current_user, role = project_and_user_and_role
99137
if not has_required_role(role, ProjectRoles.READ):
100138
raise AuthorizationError("You don't have permission to list files in this project.")
@@ -105,15 +143,19 @@ async def list_files(
105143
secret_key=settings.s3.secret_key.get_secret_value(),
106144
)
107145

108-
return await run_in_threadpool(s3_file_manager.list_files, bucket_name=project.bucket_name)
146+
return await run_in_threadpool(
147+
s3_file_manager.get_detailed_object_info,
148+
bucket_name=project.bucket_name,
149+
object_name=object_name,
150+
)
109151

110152

111153
@s3_router.post(
112154
"/upload/single-part", status_code=status.HTTP_200_OK, response_model=list[PreSignedSinglePartUploadResponse]
113155
)
114156
async def generate_single_part_upload_urls(
115157
project_name: str,
116-
objects: list[UploadSinglePartObjectRequest],
158+
objects: Annotated[list[UploadSinglePartObjectRequest], Body(min_length=1, max_length=MAX_S3_API_BATCH_SIZE)],
117159
s3_signer_service: Annotated[S3PreSignedService, Depends(get_pre_signed_service)],
118160
project_and_user_and_role: tuple[ProjectDB, UserDB, ProjectRoles] = Depends(get_project_member),
119161
):
@@ -243,11 +285,15 @@ async def abort_multipart_upload(
243285

244286
@s3_router.delete("/", status_code=status.HTTP_200_OK, response_model=list[str])
245287
async def soft_delete_files(
246-
objects: list[str],
247288
project_name: str,
289+
objects: Annotated[list[str], Body(min_length=1, max_length=MAX_S3_API_BATCH_SIZE)],
248290
project_and_user_and_role: tuple[ProjectDB, UserDB, ProjectRoles] = Depends(get_project_member),
249291
):
250-
"""Soft delete files in the project's bucket. This adds a deletion marker to the files, but does not actually delete them."""
292+
"""
293+
Soft delete files in the projects store.
294+
295+
This adds a deletion marker to the files, but does not actually delete them.
296+
"""
251297
project, current_user, role = project_and_user_and_role
252298
if not has_required_role(role, ProjectRoles.EDIT):
253299
raise AuthorizationError("You don't have permission to soft delete files in this project.")
@@ -265,11 +311,42 @@ async def soft_delete_files(
265311
)
266312

267313

314+
@s3_router.post("/restore", status_code=status.HTTP_200_OK, response_model=RestoreObjectsResponse)
315+
async def restore_soft_deleted_files(
316+
project_name: str,
317+
objects: Annotated[list[str], Body(min_length=1, max_length=MAX_S3_API_BATCH_SIZE)],
318+
project_and_user_and_role: tuple[ProjectDB, UserDB, ProjectRoles] = Depends(get_project_member),
319+
):
320+
"""
321+
Restore soft-deleted files from a project's store by removing the soft delete marker.
322+
323+
This finds the latest version of each file provided, and if it has a delete marker it removes it,
324+
effectively restoring the previous version of the file.
325+
326+
NOTE: If you want to restore an older version of a file, you need to either:
327+
1. download and upload the file again.
328+
2. Hard delete the versions after the version you want to restore (making the latest version, the version you want).
329+
"""
330+
project, current_user, role = project_and_user_and_role
331+
if not has_required_role(role, ProjectRoles.EDIT):
332+
raise AuthorizationError("You don't have permission to restore files in this project.")
333+
334+
check_too_many_objects_in_request(numb_objects=len(objects))
335+
336+
s3_file_manager = S3FileManager(
337+
url=settings.s3.endpoint_url,
338+
access_key=settings.s3.access_key.get_secret_value(),
339+
secret_key=settings.s3.secret_key.get_secret_value(),
340+
)
341+
342+
return await run_in_threadpool(s3_file_manager.restore_objects, objects=objects, bucket_name=project.bucket_name)
343+
344+
268345
# using POST as GET with body is not considered good practice
269346
@s3_router.post("/checksums", status_code=status.HTTP_200_OK, response_model=list[FileChecksumResponse])
270347
async def get_files_checksums(
271-
object_names: list[str],
272348
project_name: str,
349+
object_names: Annotated[list[str], Body(min_length=1, max_length=MAX_S3_API_BATCH_SIZE)],
273350
project_and_user_and_role: tuple[ProjectDB, UserDB, ProjectRoles] = Depends(get_project_member),
274351
):
275352
"""

0 commit comments

Comments
 (0)