Skip to content

Commit

Permalink
Merge pull request #23 from jonls/regex-matches
Browse files Browse the repository at this point in the history
Support regular expression matches for cache_rules
  • Loading branch information
jonls authored Oct 21, 2018
2 parents 04d236f + e26e0fd commit eeb0deb
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 25 deletions.
9 changes: 7 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ The configuration is stored in a YAML file like this:
- match: "/assets/*"
maxage: 30 days
- match_regexp: '^assets/image-\d{3}-.*\.png$'
maxage: 90 days
- match: "/css/*"
maxage: 30 days
Expand Down Expand Up @@ -81,7 +84,7 @@ Configuration file
``arn:aws:s3:::example.com`` and ``arn:aws:s3:::example.com/*``.

**s3_reduced_redundancy**
An optional boolean to indicate whether the files should be uploaded
An optional boolean to indicate whether the files should be uploaded
to `reduced redundancy`_ storage.

**cloudfront_distribution_id**
Expand All @@ -91,7 +94,9 @@ Configuration file

**cache_rules**
A list of rules to determine the cache configuration of the uploaded files.
The ``match`` key specifies a pattern that the rule applies to. Only the
The ``match`` key specifies a pattern that the rule applies to. This uses
glob-style matching (with ``*`` and ``?``). Matching can also be performed
with regular expressions by using ``match_regexp``. Only the
first rule to match a given key will be used. The ``maxage`` key
specifies the time to cache the file. The value should be either a number
of seconds or a string like ``30 days``, ``5 minutes, 30 seconds``, etc.
Expand Down
11 changes: 10 additions & 1 deletion s3_deploy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,16 @@ def resolve_cache_rules(key_name, rules):
"""Returns the value of the Cache-Control header after applying rules."""

for rule in rules:
if match_key(rule['match'], key_name):
has_match = 'match' in rule
has_match_regexp = 'match_regexp' in rule
if has_match == has_match_regexp:
raise ValueError(
'Cache rule must have either match or match_regexp key'
)

pattern = rule['match'] if has_match else rule['match_regexp']

if match_key(pattern, key_name, regexp=has_match_regexp):
cache_control = None
if 'cache_control' in rule:
cache_control = rule['cache_control']
Expand Down
9 changes: 6 additions & 3 deletions s3_deploy/filematch.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ def compile_re(pattern):
return prefix + ''.join(re_pattern) + '$'


def match_key(pattern, key):
"""Match key to a glob (gitignore-style) pattern."""
def match_key(pattern, key, regexp=False):
"""Match key to a glob (gitignore-style) or regexp pattern."""

if pattern == '':
raise ValueError('Empty pattern is invalid')

return bool(re.search(compile_re(pattern), key))
if not regexp:
pattern = compile_re(pattern)

return bool(re.search(pattern, key))
18 changes: 18 additions & 0 deletions s3_deploy/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ def test_resolve_empty(self):
cache = config.resolve_cache_rules('test', [])
self.assertIsNone(cache)

def test_resolve_no_match_key(self):
with self.assertRaises(ValueError):
config.resolve_cache_rules('test', [
{'maxage': 3000},
])

def test_resolve_catch_all_to_cache_control(self):
cache = config.resolve_cache_rules('test', [
{'match': '*', 'cache_control': 'public, maxage=500'}
Expand Down Expand Up @@ -103,6 +109,18 @@ def test_resolve_second_rule(self):
])
self.assertEqual(cache, 'max-age=200')

def test_resolve_multiple_match_keys(self):
with self.assertRaises(ValueError):
config.resolve_cache_rules('test', [
{'match': 'test*', 'match_regexp': 'test.*'},
])

def test_resolve_regexp_match_key(self):
cache = config.resolve_cache_rules('test', [
{'match_regexp': 't[es]{2}(t|a)$', 'maxage': 100},
])
self.assertEqual(cache, 'max-age=100')


class LoadConfigFileTest(unittest.TestCase):
def setUp(self):
Expand Down
75 changes: 56 additions & 19 deletions s3_deploy/tests/test_filematch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,64 +6,101 @@

class FileMatchTest(unittest.TestCase):
def test_match_exact_ok(self):
self.assertTrue(filematch.match_key('/index.html', 'index.html'))
self.assertTrue(filematch.match_key(
'/index.html', 'index.html', regexp=False))

def test_match_exact_fail_different_name(self):
self.assertFalse(filematch.match_key('/index.html', 'image.png'))
self.assertFalse(filematch.match_key(
'/index.html', 'image.png', regexp=False))

def test_match_exact_fail_different_path(self):
self.assertFalse(filematch.match_key('/index.html', 'dir/index.html'))
self.assertFalse(filematch.match_key(
'/index.html', 'dir/index.html', regexp=False))

def test_match_exact_with_path_ok(self):
self.assertTrue(filematch.match_key(
'/dir/index.html', 'dir/index.html'))
'/dir/index.html', 'dir/index.html', regexp=False))

def test_match_exact_with_path_fail_different_name(self):
self.assertFalse(filematch.match_key(
'/dir/index.html', 'dir/image.png'))
'/dir/index.html', 'dir/image.png', regexp=False))

def test_match_exact_with_path_fail_different_path(self):
self.assertFalse(filematch.match_key(
'/dir/index.html', 'altdir/image.png'))
'/dir/index.html', 'altdir/image.png', regexp=False))

def test_match_base_ok_simple(self):
self.assertTrue(filematch.match_key('index.html', 'index.html'))
self.assertTrue(filematch.match_key(
'index.html', 'index.html', regexp=False))

def test_match_base_ok_path(self):
self.assertTrue(filematch.match_key('index.html', 'dir/index.html'))
self.assertTrue(filematch.match_key(
'index.html', 'dir/index.html', regexp=False))

def test_match_base_fail_different_name(self):
self.assertFalse(filematch.match_key('ok.html', 'book.html'))
self.assertFalse(filematch.match_key(
'ok.html', 'book.html', regexp=False))

def test_match_base_fail_different_path(self):
self.assertFalse(filematch.match_key('ok.html', 'dir/book.html'))
self.assertFalse(filematch.match_key(
'ok.html', 'dir/book.html', regexp=False))

def test_match_variable_base_ok(self):
self.assertTrue(filematch.match_key('*.html', 'index.html'))
self.assertTrue(filematch.match_key(
'*.html', 'index.html', regexp=False))

def test_match_variable_base_within_ok(self):
self.assertTrue(filematch.match_key('image-*.png', 'image-999.png'))
self.assertTrue(filematch.match_key(
'image-*.png', 'image-999.png', regexp=False))

def test_match_variable_with_path_ok(self):
self.assertTrue(filematch.match_key('images/*.png', 'images/999.png'))
self.assertTrue(filematch.match_key(
'images/*.png', 'images/999.png', regexp=False))

def test_match_variable_with_path_fail(self):
self.assertFalse(filematch.match_key('images/*.png', 'images/README'))
self.assertFalse(filematch.match_key(
'images/*.png', 'images/README', regexp=False))

def test_match_multilevel_fail(self):
self.assertFalse(filematch.match_key(
'/dir/*.html', 'dir/second/file.html'))
'/dir/*.html', 'dir/second/file.html', regexp=False))

def test_match_one_ok(self):
self.assertTrue(filematch.match_key('image?.png', 'image1.png'))
self.assertTrue(filematch.match_key(
'image?.png', 'image1.png', regexp=False))

def test_match_one_fail(self):
self.assertFalse(filematch.match_key('image?.png', 'image10.png'))
self.assertFalse(filematch.match_key(
'image?.png', 'image10.png', regexp=False))

def test_match_empty_on_file(self):
with self.assertRaises(ValueError):
filematch.match_key('', 'index.html')
filematch.match_key('', 'index.html', regexp=False)

def test_match_empty_on_empty(self):
with self.assertRaises(ValueError):
filematch.match_key('', '')
filematch.match_key('', '', regexp=False)

def test_match_regex_dollar_suffix_ok(self):
self.assertTrue(filematch.match_key(
'image-\d+\.png$', 'image-999.png', regexp=True))

def test_match_regex_dollar_suffix_fail(self):
self.assertFalse(filematch.match_key(
'image-\d+\.png$', 'image-name.png', regexp=True))

def test_match_regex_caret_prefix_ok(self):
self.assertTrue(filematch.match_key(
'^dir\/index\.html', 'dir/index.html', regexp=True))

def test_match_regex_caret_prefix_fail(self):
self.assertFalse(filematch.match_key(
'^dir\/index\.html', 'altdir/index.html', regexp=True))

def test_match_regex_caret_and_dollar_ok(self):
self.assertTrue(filematch.match_key(
'^dir\/index\.html$', 'dir/index.html', regexp=True))

def test_match_regex_caret_and_dollar_fail(self):
self.assertFalse(filematch.match_key(
'^dir\/index\.html$', 'dir/index.htm', regexp=True))

0 comments on commit eeb0deb

Please sign in to comment.