Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(static): implement Last-Modified header for static route #2426

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bf8e72e
add last-modified support for static
Cycloctane Jan 11, 2025
8b0992d
add mtime attr to static test fakestat
Cycloctane Jan 11, 2025
6165bef
update _open_range docstring
Cycloctane Jan 11, 2025
613fce6
close file handler before raising exception
Cycloctane Jan 14, 2025
4635a61
fix `resp.last_modified` type
Cycloctane Jan 14, 2025
cea552a
add tests
Cycloctane Jan 14, 2025
1e3220a
Merge branch 'master' into last-modified_support
vytas7 Jan 14, 2025
ccd7b5e
replace `os.fstat` with `os.stat`
Cycloctane Jan 15, 2025
cf105d5
add tests for read error
Cycloctane Jan 15, 2025
c5e5318
remove useless st_mode in fakestat
Cycloctane Jan 15, 2025
8f9930c
handle permission error for os.stat
Cycloctane Jan 15, 2025
7c794f2
add more tests for permission error
Cycloctane Jan 15, 2025
6b7dd21
format
Cycloctane Jan 15, 2025
673db30
revert "replace `os.fstat` with `os.stat`"
Cycloctane Jan 15, 2025
c728e8f
add test for coverage
Cycloctane Jan 15, 2025
5b48366
update tests
Cycloctane Jan 15, 2025
4885c8b
fix pep8 warning
Cycloctane Jan 17, 2025
79242b1
Merge branch 'master' into last-modified_support
Cycloctane Jan 20, 2025
c3f1d58
Format with `ruff`
Cycloctane Jan 20, 2025
6b74ed7
Merge branch 'master' into last-modified_support
vytas7 Feb 8, 2025
0714e20
Merge branch 'master' into last-modified_support
vytas7 Feb 8, 2025
1edba97
Merge branch 'master' into last-modified_support
Cycloctane Feb 21, 2025
34b7a46
Merge branch 'master' into last-modified_support
vytas7 Feb 21, 2025
b2217a1
add docstring for _open_file
Cycloctane Feb 22, 2025
1898b39
remove PermissionError handler
Cycloctane Feb 22, 2025
34ea7d8
add changelog
Cycloctane Feb 22, 2025
94b1253
fix test
Cycloctane Feb 22, 2025
cd890aa
Merge branch 'master' into last-modified_support
vytas7 Mar 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions falcon/routing/static.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
from datetime import datetime, timezone
from functools import partial
import io
import os
Expand All @@ -18,12 +19,13 @@


def _open_range(
file_path: Union[str, Path], req_range: Optional[Tuple[int, int]]
fh: io.BufferedReader, st: os.stat_result, req_range: Optional[Tuple[int, int]]
) -> Tuple[ReadableIO, int, Optional[Tuple[int, int, int]]]:
"""Open a file for a ranged request.

Args:
file_path (str): Path to the file to open.
fh (io.BufferedReader): file handler of the file.
st (os.stat_result): fs stat result of the file.
req_range (Optional[Tuple[int, int]]): Request.range value.
Returns:
tuple: Three-member tuple of (stream, content-length, content-range).
Expand All @@ -32,8 +34,7 @@
possibly bounded, and the content-range will be a tuple of
(start, end, size).
"""
fh = io.open(file_path, 'rb')
size = os.fstat(fh.fileno()).st_size
size = st.st_size
if req_range is None:
return fh, size, None

Expand Down Expand Up @@ -217,23 +218,32 @@
if '..' in file_path or not file_path.startswith(self._directory):
raise falcon.HTTPNotFound()

req_range = req.range
if req.range_unit != 'bytes':
req_range = None
try:
stream, length, content_range = _open_range(file_path, req_range)
resp.set_stream(stream, length)
fh = io.open(file_path, 'rb')
st = os.fstat(fh.fileno())
except IOError:
if self._fallback_filename is None:
raise falcon.HTTPNotFound()
try:
stream, length, content_range = _open_range(
self._fallback_filename, req_range
)
resp.set_stream(stream, length)
file_path = self._fallback_filename
fh = io.open(self._fallback_filename, 'rb')
st = os.fstat(fh.fileno())
except IOError:
raise falcon.HTTPNotFound()
else:
file_path = self._fallback_filename

resp.last_modified = datetime.fromtimestamp(st.st_mtime, timezone.utc)
if (req.if_modified_since is not None and
resp.last_modified <= req.if_modified_since):
resp.status = falcon.HTTP_304
return

Check warning on line 239 in falcon/routing/static.py

View check run for this annotation

Codecov / codecov/patch

falcon/routing/static.py#L238-L239

Added lines #L238 - L239 were not covered by tests

req_range = req.range if req.range_unit == 'bytes' else None
try:
stream, length, content_range = _open_range(fh, st, req_range)
resp.set_stream(stream, length)
except IOError:
raise falcon.HTTPNotFound()

Check warning on line 246 in falcon/routing/static.py

View check run for this annotation

Codecov / codecov/patch

falcon/routing/static.py#L246

Added line #L246 was not covered by tests

suffix = os.path.splitext(file_path)[1]
resp.content_type = resp.options.static_media_types.get(
Expand Down
5 changes: 3 additions & 2 deletions tests/test_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,17 @@ class FakeFD(int):
pass

class FakeStat:
def __init__(self, size):
def __init__(self, size, mtime):
self.st_size = size
self.st_mtime = mtime

if validate:
validate(path)

data = path.encode() if content is None else content
fake_file = io.BytesIO(data)
fd = FakeFD(1337)
fd._stat = FakeStat(len(data))
fd._stat = FakeStat(len(data), 1736617934)
fake_file.fileno = lambda: fd

patch.current_file = fake_file
Expand Down
Loading