Skip to content

Commit f30bed9

Browse files
Merge pull request #73 from SchrodingersGat/file-downloader
Adds API function for downloading file
2 parents cf81ea7 + 4232ad8 commit f30bed9

6 files changed

Lines changed: 150 additions & 15 deletions

File tree

inventree/api.py

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ def __init__(self, base_url, **kwargs):
3535
""" Initialize class with initial parameters
3636
3737
Args:
38-
base_url - Base API URL
38+
base_url - Base URL for the InvenTree server, including port (if required)
39+
e.g. "http://inventree.server.com:8000"
3940
4041
kwargs:
4142
username - Login username
@@ -45,18 +46,17 @@ def __init__(self, base_url, **kwargs):
4546
verbose - Print extra debug messages (default = False)
4647
"""
4748

48-
if not base_url.endswith('/'):
49-
base_url += '/'
50-
51-
# Server address *must* end with /api/
52-
if not base_url.endswith('/api/'):
53-
base_url = os.path.join(base_url, 'api')
49+
# Strip out trailing "/api/" (if provided)
50+
if base_url.endswith("/api/"):
51+
base_url = base_url[:-5]
5452

5553
if not base_url.endswith('/'):
5654
base_url += '/'
5755

5856
self.base_url = base_url
5957

58+
self.api_url = os.path.join(self.base_url, 'api/')
59+
6060
logger.info("Connecting to server: " + str(self.base_url))
6161

6262
self.username = kwargs.get('username', None)
@@ -78,7 +78,7 @@ def __init__(self, base_url, **kwargs):
7878

7979
def clean_url(self, url):
8080

81-
url = os.path.join(self.base_url, url)
81+
url = os.path.join(self.api_url, url)
8282

8383
if not url.endswith('/'):
8484
url += '/'
@@ -97,7 +97,7 @@ def testServer(self):
9797
logger.info("Checking InvenTree server connection...")
9898

9999
try:
100-
response = requests.get(self.base_url)
100+
response = requests.get(self.api_url)
101101
except requests.exceptions.ConnectionError:
102102
logger.error("Server connection refused - check server address")
103103
return False
@@ -144,7 +144,7 @@ def requestToken(self):
144144
logger.info("Requesting auth token from server...")
145145

146146
# Request an auth token from the server
147-
token_url = os.path.join(self.base_url, 'user/token/')
147+
token_url = os.path.join(self.api_url, 'user/token/')
148148

149149
reply = requests.get(token_url, auth=self.auth)
150150

@@ -176,7 +176,7 @@ def request(self, url, **kwargs):
176176
if url.startswith('/'):
177177
url = url[1:]
178178

179-
api_url = os.path.join(self.base_url, url)
179+
api_url = os.path.join(self.api_url, url)
180180

181181
if not api_url.endswith('/'):
182182
api_url += '/'
@@ -426,3 +426,55 @@ def get(self, url, **kwargs):
426426
return None
427427

428428
return data
429+
430+
def downloadFile(self, url, destination):
431+
"""
432+
Download a file from the InvenTree server.
433+
434+
- If the "destination" is a directory, use the filename of the remote URL
435+
"""
436+
437+
# Check that the provided URL is "absolute"
438+
if not url.startswith(self.base_url):
439+
440+
if url.startswith('/'):
441+
url = url[1:]
442+
443+
url = os.path.join(self.base_url, url)
444+
445+
if os.path.exists(destination) and os.path.isdir(destination):
446+
447+
destination = os.path.join(
448+
destination,
449+
os.path.basename(url)
450+
)
451+
452+
destination = os.path.abspath(destination)
453+
454+
headers = {
455+
'AUTHORIZATION': f"Token {self.token}"
456+
}
457+
458+
with requests.get(url, stream=True, headers=headers) as request:
459+
460+
if not request.status_code == 200:
461+
logger.error(
462+
f"Error downloading file '{url}': Server returned status {request.status_code}"
463+
)
464+
return False
465+
466+
headers = request.headers
467+
468+
if 'text/html' in headers['Content-Type']:
469+
logger.error(
470+
f"Error downloading file '{url}': Server return invalid response (text/html)"
471+
)
472+
return False
473+
474+
with open(destination, 'wb') as f:
475+
476+
for chunk in request.iter_content(chunk_size=16 * 1024):
477+
f.write(chunk)
478+
479+
logger.info(f"Downloaded '{url}' to '{destination}'")
480+
return True

inventree/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,13 @@ def upload(cls, api, filename, comment, **kwargs):
226226
else:
227227
logger.warning("File upload failed")
228228

229+
def download(self, destination):
230+
"""
231+
Download the attachment file to the specified location
232+
"""
233+
234+
return self._api.downloadFile(self.attachment, destination)
235+
229236

230237
class Currency(InventreeObject):
231238
""" Class representing the Currency database model """

inventree/company.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
# -*- coding: utf-8 -*-
22

3+
import os
4+
import logging
5+
36
import inventree.base
47
import inventree.order
58

69

10+
logger = logging.getLogger('inventree')
11+
12+
713
class Company(inventree.base.InventreeObject):
814
""" Class representing the Company database model """
915

@@ -57,6 +63,42 @@ def createSalesOrder(self, **kwargs):
5763
data=kwargs
5864
)
5965

66+
def uploadImage(self, image):
67+
"""
68+
Upload an image file against this Company
69+
70+
Returns the HTTP response object
71+
"""
72+
73+
files = {}
74+
75+
if image:
76+
if os.path.exists(image):
77+
f = os.path.basename(image)
78+
fo = open(image, 'rb')
79+
files['image'] = (f, fo)
80+
else:
81+
logger.error(f"Image '{image}' does not exist")
82+
return None
83+
84+
response = self.save(
85+
data={},
86+
files=files,
87+
)
88+
89+
return response
90+
91+
def downloadImage(self, destination):
92+
"""
93+
Download the image for this Company, to the specified destination
94+
"""
95+
96+
if self.image:
97+
return self._api.downloadFile(self.image, destination)
98+
else:
99+
logger.error(f"Company '{self.name}' does not have an associated image")
100+
return False
101+
60102

61103
class SupplierPart(inventree.base.InventreeObject):
62104
""" Class representing the SupplierPart database model """

inventree/part.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import inventree.build
1111

1212

13+
logger = logging.getLogger('inventree')
14+
15+
1316
class PartCategory(inventree.base.InventreeObject):
1417
""" Class representing the PartCategory database model """
1518

@@ -98,7 +101,7 @@ def uploadImage(self, image):
98101
fo = open(image, 'rb')
99102
files['image'] = (f, fo)
100103
else:
101-
logging.error("File does not exist: '{f}'".format(f=image))
104+
logger.error("File does not exist: '{f}'".format(f=image))
102105
return None
103106

104107
response = self.save(
@@ -108,6 +111,17 @@ def uploadImage(self, image):
108111

109112
return response
110113

114+
def downloadImage(self, destination):
115+
"""
116+
Download the image for this Part, to the specified destination
117+
"""
118+
119+
if self.image:
120+
return self._api.downloadFile(self.image, destination)
121+
else:
122+
logger.error(f"Part '{self.name}' does not have an associated image")
123+
return False
124+
111125

112126
class PartAttachment(inventree.base.Attachment):
113127
""" Class representing a file attachment for a Part """
@@ -139,7 +153,7 @@ def upload_attachment(cls, api, part, **kwargs):
139153
fo = open(attachment, 'rb')
140154
files['attachment'] = (f, fo)
141155
else:
142-
logging.error("File does not exist: '{f}'".format(f=attachment))
156+
logger.error("File does not exist: '{f}'".format(f=attachment))
143157

144158
comment = kwargs.get('comment', '')
145159

@@ -151,10 +165,10 @@ def upload_attachment(cls, api, part, **kwargs):
151165

152166
# Send the data to the server
153167
if api.post(cls.URL, data, files=files):
154-
logging.info("Uploaded attachment: '{f}'".format(f=attachment))
168+
logger.info("Uploaded attachment: '{f}'".format(f=attachment))
155169
ret = True
156170
else:
157-
logging.warning("Attachment upload failed")
171+
logger.warning("Attachment upload failed")
158172
ret = False
159173

160174
return ret

test/test_api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ def test_read_parts(self):
4646

4747
self.assertEqual(len(parts), 0)
4848

49+
def test_file_download(self):
50+
"""
51+
Attemtping to download a file while unauthenticated should return False
52+
"""
53+
54+
self.assertFalse(self.api.downloadFile('/media/part/files/1/test.pdf', 'test.pdf'))
55+
4956

5057
class InvenTreeTestCase(unittest.TestCase):
5158

test/test_part.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ def test_image_upload(self):
214214
# Grab the first part
215215
p = part.Part.list(self.api)[0]
216216

217+
# Ensure the part does *not* have an image associated with it
218+
p.save(data={'image': None})
219+
217220
# Create a dummy file (not an image)
218221
with open('dummy_image.jpg', 'w') as dummy_file:
219222
dummy_file.write("hello world")
@@ -231,3 +234,13 @@ def test_image_upload(self):
231234
self.assertIsNotNone(response)
232235
self.assertIsNotNone(p['image'])
233236
self.assertIn('dummy_image', p['image'])
237+
238+
# Re-download the image
239+
self.assertTrue(p.downloadImage("downloaded_image.png"))
240+
241+
self.assertTrue(os.path.exists("downloaded_image.png"))
242+
243+
with Image.open("downloaded_image.png") as img2:
244+
245+
self.assertEqual(img2.width, 128)
246+
self.assertEqual(img2.height, 128)

0 commit comments

Comments
 (0)