Skip to content

Commit 03c3ec6

Browse files
authored
Merge pull request #9 from Imageomics/6-download-original
Fix hash value mismatch
2 parents f628cb2 + 18008a5 commit 03c3ec6

5 files changed

Lines changed: 108 additions & 21 deletions

File tree

.github/workflows/run-tests.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: run-tests
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: main
7+
8+
jobs:
9+
run-tests:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v2
15+
16+
- uses: actions/setup-python@v4
17+
with:
18+
python-version: '3.8'
19+
20+
- name: Install tox
21+
run: pip install tox
22+
shell: bash
23+
24+
- name: Run tests
25+
run: tox
26+
shell: bash

src/dva/api.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
import hashlib
34
from pyDataverse.api import NativeApi, DataAccessApi
45
from pyDataverse.models import Datafile
@@ -18,13 +19,28 @@ def get_files_for_doi(self, doi):
1819
dataset = self._api.get_dataset(doi).json()
1920
return dataset['data']['latestVersion']['files']
2021

21-
def download_file(self, dvfile, path):
22+
def _get_datafile_response(self, dvfile):
23+
dv_data_file = dvfile["dataFile"]
24+
# Retrieve the original file (that matches MD5 checksum) for files processed by Dataverse ingress
25+
data_format = None
26+
if dv_data_file.get("originalFileFormat"):
27+
data_format = "original"
28+
29+
# NOTE: the call below blocks until the entire file is retrieved (in memory)
30+
return self._data_api.get_datafile(dv_data_file["id"], data_format=data_format)
31+
32+
def download_file(self, dvfile, dest):
33+
response = self._get_datafile_response(dvfile)
34+
filename = self.get_download_filename(response)
35+
directory_label = dvfile.get("directoryLabel", "")
36+
if directory_label:
37+
path = os.path.join(dest, directory_label, filename)
38+
else:
39+
path = os.path.join(dest, filename)
2240
os.makedirs(os.path.dirname(path), exist_ok=True)
23-
file_id = dvfile["dataFile"]["id"]
2441
with open(path, "wb") as f:
25-
# NOTE: the call below blocks until the entire file is retrieved (in memory)
26-
response = self._data_api.get_datafile(file_id)
2742
f.write(response.content)
43+
return path
2844

2945
@staticmethod
3046
def verify_checksum(dvfile, path):
@@ -51,21 +67,28 @@ def upload_file(self, doi, path, dirname=""):
5167
raise APIException(f"Uploading failed with status {status}.")
5268

5369
@staticmethod
54-
def get_dvfile_path(dvfile, parent_dir=None):
70+
def get_remote_path(dvfile):
5571
path = dvfile["dataFile"]["filename"]
5672
directory_label = dvfile.get("directoryLabel", "")
5773
if directory_label:
5874
path = f"{directory_label}/{path}"
59-
if parent_dir:
60-
path = f"{parent_dir}/{path}"
6175
return path
6276

77+
@staticmethod
78+
def get_download_filename(response):
79+
content_disposition = response.headers['Content-disposition']
80+
regex = '^ *filename=(.*?)$'
81+
for part in content_disposition.split(';'):
82+
found_items = re.findall(regex, part)
83+
if found_items:
84+
return found_items[0].strip('"')
85+
raise APIException(f"Invalid Content-disposition {content_disposition}")
86+
6387
@staticmethod
6488
def get_dvfile_size(dvfile):
6589
return dvfile["dataFile"]["filesize"]
6690

6791

68-
6992
def get_api(url):
7093
config = Config(url)
7194
return API(

src/dva/cli.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def ls(doi, json_format, url):
2727
click.echo(json.dumps(dvfiles, indent=4))
2828
else:
2929
for dvfile in dvfiles:
30-
path = api.get_dvfile_path(dvfile)
31-
click.echo(path)
30+
remote_path = api.get_remote_path(dvfile)
31+
click.echo(remote_path)
3232

3333

3434
@click.command()
@@ -43,11 +43,12 @@ def download(doi, dest, url):
4343
"""
4444
api = get_api(url)
4545
for dvfile in api.get_files_for_doi(doi):
46-
path = api.get_dvfile_path(dvfile, dest)
47-
click.echo(f"Downloading {path}")
48-
api.download_file(dvfile, path)
46+
file_id = dvfile["dataFile"]["id"]
47+
click.echo(f"Downloading file {file_id}")
48+
path = api.download_file(dvfile, dest)
49+
click.echo(f"Downloaded file {file_id} to {path}")
4950
api.verify_checksum(dvfile, path)
50-
click.echo(f"Verified file checksum for {path}.")
51+
click.echo(f"Verified file checksum for {path}")
5152

5253

5354
@click.command()

tests/test_api.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,17 @@ def test_get_files_for_doi(self, mock_data_api, mock_native_api, mock_config):
4141
self.assertEqual(len(result), 1)
4242
self.assertEqual(result[0]["dataFile"]["id"], 2222)
4343

44-
def test_get_dvfile_path(self):
44+
def test_get_remote_path(self):
4545
dvfile = {
4646
"dataFile": {
4747
"filename": "data.txt"
4848
}
4949
}
50-
result = API.get_dvfile_path(dvfile, parent_dir="/tmp")
51-
self.assertEqual(result, "/tmp/data.txt")
50+
result = API.get_remote_path(dvfile)
51+
self.assertEqual(result, "data.txt")
5252
dvfile["directoryLabel"] = "results"
53-
result = API.get_dvfile_path(dvfile, parent_dir="/tmp")
54-
self.assertEqual(result, "/tmp/results/data.txt")
53+
result = API.get_remote_path(dvfile)
54+
self.assertEqual(result, "results/data.txt")
5555

5656
@patch('dva.api.Config')
5757
@patch('dva.api.NativeApi')
@@ -62,9 +62,35 @@ def test_download_file(self, mock_data_api, mock_native_api, mock_config):
6262
"id": 2222
6363
}
6464
}
65+
mock_data_api.return_value.get_datafile.return_value = Mock(headers={
66+
'Content-disposition': 'attachment; filename="data.txt"'
67+
})
6568
api = get_api(url=None)
6669
with patch("builtins.open", mock_open()) as mock_file:
67-
api.download_file(dvfile, path="/tmp/data.txt")
70+
api.download_file(dvfile, dest="/tmp")
71+
mock_data_api.return_value.get_datafile.assert_called_with(2222, data_format=None)
72+
mock_file.assert_called_with("/tmp/data.txt", "wb")
73+
mock_file.return_value.write.assert_called_with(
74+
mock_data_api.return_value.get_datafile.return_value.content
75+
)
76+
77+
@patch('dva.api.Config')
78+
@patch('dva.api.NativeApi')
79+
@patch('dva.api.DataAccessApi')
80+
def test_download_file_original(self, mock_data_api, mock_native_api, mock_config):
81+
dvfile = {
82+
"dataFile": {
83+
"id": 2222,
84+
"originalFileFormat": "CSV"
85+
}
86+
}
87+
mock_data_api.return_value.get_datafile.return_value = Mock(headers={
88+
'Content-disposition': 'attachment; filename="data.txt"'
89+
})
90+
api = get_api(url=None)
91+
with patch("builtins.open", mock_open()) as mock_file:
92+
api.download_file(dvfile, dest="/tmp")
93+
mock_data_api.return_value.get_datafile.assert_called_with(2222, data_format='original')
6894
mock_file.assert_called_with("/tmp/data.txt", "wb")
6995
mock_file.return_value.write.assert_called_with(
7096
mock_data_api.return_value.get_datafile.return_value.content
@@ -108,3 +134,14 @@ def test_verify_checksum(self, mock_datafile, mock_data_api, mock_native_api, mo
108134
with patch("builtins.open", mock_open(read_data=b"123")) as mock_file:
109135
api.upload_file(doi='doi:10.70122/FK2/WUU4DM', path="/tmp/data.txt")
110136
self.assertEqual(str(raised_exception.exception), "Uploading failed with status bad.")
137+
138+
def test_get_download_filename(self):
139+
good_values = [
140+
('attachment; filename=bob.txt', 'bob.txt'),
141+
('attachment; filename="tom.txt"', 'tom.txt'),
142+
('attachment; filename="file1.txt"; other="a"', 'file1.txt'),
143+
]
144+
response = Mock()
145+
for content_disposition, expected_result in good_values:
146+
response.headers = {'Content-disposition': content_disposition}
147+
self.assertEqual(expected_result, API.get_download_filename(response))

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py36
2+
envlist = py38
33

44
[testenv]
55
deps = pytest

0 commit comments

Comments
 (0)