Merge lp:~abentley/charmworld/icon-as-file into lp:~juju-jitsu/charmworld/trunk

Proposed by Aaron Bentley
Status: Merged
Approved by: Aaron Bentley
Approved revision: 232
Merged at revision: 229
Proposed branch: lp:~abentley/charmworld/icon-as-file
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 194 lines (+74/-11)
4 files modified
charmworld/models.py (+16/-1)
charmworld/tests/test_models.py (+19/-0)
charmworld/views/api.py (+13/-10)
charmworld/views/tests/test_api.py (+26/-0)
To merge this branch: bzr merge lp:~abentley/charmworld/icon-as-file
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+164262@code.launchpad.net

Commit message

Store icon.svg

Description of the change

This branch enables using charm/file to retrieve the icon for a charm.

As well as ensuring it's provided, it also ensures the HTTP headers are suitable-- the MIME type is provided, and Cache-Control ensures that icons are cached for 24 hours. It is save to cache them this long because they change rarely, and when they do change, their default URL also changes.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmworld/models.py'
2--- charmworld/models.py 2013-05-03 15:47:38 +0000
3+++ charmworld/models.py 2013-05-16 20:49:26 +0000
4@@ -4,6 +4,7 @@
5
6 import errno
7 import logging
8+from mimetypes import guess_type
9 from os import listdir
10 from os.path import dirname
11 from os.path import exists
12@@ -260,6 +261,7 @@
13 'metadata.yaml',
14 'readme.*',
15 'revision',
16+ 'icon.svg',
17 ]
18
19 store_directories = [
20@@ -270,11 +272,13 @@
21 def make_charm_file(cls, fs, charm_data, file_path, filename):
22 charmid = cls.gen_charmid(charm_data)
23 fileid = cls.gen_fileid(charm_data, file_path)
24+ content_type = guess_type(filename)[0]
25 return CharmFile(
26 fs,
27 fileid,
28 charmid,
29 filename=filename,
30+ contentType=content_type,
31 )
32
33 @classmethod
34@@ -406,6 +410,7 @@
35 attributes = [
36 'fileid',
37 'charmid',
38+ 'contentType',
39 'subdir',
40 'filename',
41 'length',
42@@ -448,8 +453,11 @@
43 else:
44 return self._id.split('/')[-1]
45
46+ # value to use as default to distinguish between None/null and unset.
47+ undefined = object()
48+
49 def __init__(self, fs, fileid, charmid, filename=None,
50- instance=None, metadata=None):
51+ instance=None, metadata=None, contentType=undefined):
52 """Get a file object started.
53
54 :param fs: the gridfs client to use.
55@@ -466,6 +474,12 @@
56 self._id = fileid
57 self.charmid = charmid
58 self._filename = None
59+ if contentType is not self.undefined:
60+ self.contentType = contentType
61+ elif instance is not None:
62+ self.contentType = instance.contentType
63+ else:
64+ self.contentType = None
65
66 if instance:
67 self._gridout = instance
68@@ -506,6 +520,7 @@
69 self.fs.put(
70 contents,
71 filename=self.filename,
72+ contentType=self.contentType,
73 **self.metadata
74 )
75
76
77=== modified file 'charmworld/tests/test_models.py'
78--- charmworld/tests/test_models.py 2013-05-03 19:46:57 +0000
79+++ charmworld/tests/test_models.py 2013-05-16 20:49:26 +0000
80@@ -223,6 +223,7 @@
81 quote_key('config.yaml'),
82 quote_key('metadata.yaml'),
83 quote_key('README.md'),
84+ quote_key('icon.svg'),
85 'hooks/config-changed',
86 'hooks/database-relation-broken',
87 'hooks/database-relation-changed',
88@@ -352,6 +353,24 @@
89 self.assertEqual(expected, messages)
90 self.assertEqual([], files)
91
92+ def test_provides_content_type(self):
93+ path = self.use_context(temp_dir())
94+ with file(join(path, 'icon.svg'), 'w') as f:
95+ f.write('foo')
96+ with file(join(path, 'readme.txt'), 'w') as f:
97+ f.write('foo')
98+ CharmFileSet.save_files(
99+ self.fs,
100+ self.charm_data,
101+ path,
102+ self.logger)
103+ icon_id = self.base_fileid + quote_key('icon.svg')
104+ cf = CharmFileSet.get_by_id(self.fs, icon_id)
105+ self.assertEqual('image/svg+xml', cf.contentType)
106+ readme_id = self.base_fileid + quote_key('readme.txt')
107+ cf = CharmFileSet.get_by_id(self.fs, readme_id)
108+ self.assertEqual('text/plain', cf.contentType)
109+
110
111 class TestCharmFile(MongoTestBase):
112
113
114=== modified file 'charmworld/views/api.py'
115--- charmworld/views/api.py 2013-05-16 15:44:30 +0000
116+++ charmworld/views/api.py 2013-05-16 20:49:26 +0000
117@@ -5,7 +5,6 @@
118 from inspect import getargspec
119 import json
120 from os.path import (
121- basename,
122 join,
123 )
124 import re
125@@ -311,19 +310,23 @@
126
127 def _charm_file(self, charm, trailing):
128 path = trailing.split('/', 1)[1]
129+ file_id = CharmFileSet.gen_fileid(charm, path)
130 fs = getfs(self.request.db)
131- charm_file = CharmFileSet.make_charm_file(fs, charm, path,
132- basename(path))
133 try:
134- return Response(
135- charm_file.read(),
136- headerlist=[
137- ("Access-Control-Allow-Origin", "*"),
138- ("Access-Control-Allow-Headers", "X-Requested-With"),
139- ],
140- status_code=200)
141+ charm_file = CharmFileSet.get_by_id(fs, file_id)
142 except NoFile:
143 return json_response(404, {'type': 'no_such_file', 'path': path})
144+ headerlist = [
145+ ("Access-Control-Allow-Origin", "*"),
146+ ("Access-Control-Allow-Headers", "X-Requested-With"),
147+ ("Cache-Control", "max-age=86400, public"),
148+ ]
149+ if charm_file.contentType is not None:
150+ headerlist.append(('Content-Type', charm_file.contentType))
151+ return Response(
152+ charm_file.read(),
153+ headerlist=headerlist,
154+ status_code=200)
155
156 @staticmethod
157 def _format_category(category):
158
159=== modified file 'charmworld/views/tests/test_api.py'
160--- charmworld/views/tests/test_api.py 2013-05-16 14:46:25 +0000
161+++ charmworld/views/tests/test_api.py 2013-05-16 20:49:26 +0000
162@@ -1015,6 +1015,32 @@
163 self.assertEqual({'type': 'no_such_file', 'path': 'subdir/README'},
164 response.json_body)
165
166+ def test_file_includes_content_type(self):
167+ charm = factory.makeCharm(self.db)[1]
168+ factory.make_charm_file(self.db, charm, 'readme.txt')
169+ factory.make_charm_file(self.db, charm, 'readme')
170+ factory.make_charm_file(self.db, charm, 'readme.svg')
171+ charm_id = self.api_class._get_api_id(charm)
172+ response = self.get_response(
173+ 'charm', remainder='/' + charm_id + '/file/readme.txt')
174+ self.assertEqual('text/plain', response.content_type)
175+ response = self.get_response(
176+ 'charm', remainder='/' + charm_id + '/file/readme')
177+ response = self.get_response(
178+ 'charm', remainder='/' + charm_id + '/file/readme.svg')
179+ self.assertEqual('image/svg+xml', response.content_type)
180+
181+ def test_file_includes_cache_instructions(self):
182+ # The files can be cached a long time because they rarely change, and
183+ # when they do, their default URL will also change.
184+ charm = factory.makeCharm(self.db)[1]
185+ factory.make_charm_file(self.db, charm, 'readme.txt')
186+ charm_id = self.api_class._get_api_id(charm)
187+ response = self.get_response(
188+ 'charm', remainder='/' + charm_id + '/file/readme.txt')
189+ self.assertIn(("Cache-Control", "max-age=86400, public"),
190+ response.headerlist)
191+
192 def test_unknown_sub_endpoint(self):
193 charm = factory.makeCharm(self.db)[1]
194 charm_id = self.api_class._get_api_id(charm)

Subscribers

People subscribed via source and target branches