Skip to content

Commit fd59b2b

Browse files
authored
Merge pull request #86 from octue/fix-maxsize-bytes
Fix signature mismatch bug due to max size bytes header
2 parents c322454 + a04387c commit fd59b2b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+949
-882
lines changed

.devcontainer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM windpioneers/gdal-python:little-gecko-gdal-2.4.1-python-3.9-dev
1+
FROM windpioneers/gdal-python:rational-swordtail-gdal-3.10.0-python-3.13-dev
22

33
# Bust any pre-existing apt cache
44
# Solves: https://askubuntu.com/questions/1388000/failed-to-fetch-deb-debian-org-debian-pool-main-l-linux-linux-libc-dev-5-10-70-1

.pre-commit-config.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,18 @@ repos:
2020
- id: check-json5
2121

2222
- repo: https://github.com/astral-sh/ruff-pre-commit
23-
rev: v0.6.9
23+
rev: v0.11.0
2424
hooks:
2525
- id: ruff
26+
name: Ruff lint
2627
args: [--fix, --exit-non-zero-on-fix]
28+
29+
- id: ruff
30+
name: Ruff isort
31+
args: [check, --select, I, --fix]
32+
2733
- id: ruff-format
34+
name: Ruff format
2835

2936
- repo: https://github.com/windpioneers/pre-commit-hooks
3037
rev: 0.0.5

django_gcp/events/signals.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
from django.dispatch import Signal
22

3-
43
event_received = Signal()

django_gcp/events/utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import base64
2+
from datetime import timezone
23
import json
34
import logging
4-
from datetime import timezone
5+
56
from dateutil.parser import isoparse
67
from django.conf import settings
78
from django.urls import reverse
89
from django.utils.http import urlencode
9-
from django_gcp.exceptions import InvalidPubSubMessageError
1010

11+
from django_gcp.exceptions import InvalidPubSubMessageError
1112

1213
logger = logging.getLogger(__name__)
1314

django_gcp/events/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import logging
3+
34
from django.conf import settings
45
from django.http import HttpResponse
56
from django.utils.decorators import method_decorator
@@ -8,7 +9,6 @@
89

910
from .signals import event_received
1011

11-
1212
logger = logging.getLogger(__name__)
1313

1414

django_gcp/logs/error_reporting.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# pylint: disable=broad-except
33

44
from logging import StreamHandler
5+
56
from django.conf import settings
67
from google.cloud import error_reporting
78

django_gcp/logs/structured_logs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
23
from google.cloud.logging_v2.handlers import StructuredLogHandler
34

45

django_gcp/management/commands/_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
23
from django.apps import apps
34
from django.core.management.base import BaseCommand as DjangoBaseCommand
45

django_gcp/management/commands/cleanup_tmp_files.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from datetime import datetime, timedelta
2+
23
from django.core.management.base import BaseCommand
4+
35
from django_gcp.storage import GoogleCloudStorage
46

57

django_gcp/metadata/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
from .metadata import ENDPOINTS, CloudRunMetadata
22

3-
43
__all__ = ["ENDPOINTS", "CloudRunMetadata"]

django_gcp/metadata/metadata.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from ..exceptions import NotOnCloudRunError
66

7-
87
ENDPOINTS = {
98
"project_id": "computeMetadata/v1/project/project-id",
109
"project_number": "/computeMetadata/v1/project/numeric-project-id",

django_gcp/signals.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import logging
22

3-
43
logger = logging.getLogger(__name__)
54

65

django_gcp/static/django_gcp/cloud_object_widget.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,17 @@ $(document).ready(function($) {
191191
// var field = wrapper.find("input[type=hidden]");
192192
var file = wrapper.find(".gcp-file-input").get(0).files[0];
193193
const signed_ingress_url = wrapper.data("signed-ingress-url")
194-
// const ingress_path = wrapper.data("ingress-path")
194+
const max_size_bytes = wrapper.data("max-size-bytes")
195+
const headers = {}
196+
if (max_size_bytes !== 0) {
197+
headers["X-Goog-Content-Length-Range"] = `0,${max_size_bytes}`
198+
}
195199
$.ajax({
196200
url: signed_ingress_url,
197201
type: "PUT",
198202
data: file,
199203
contentType: "application/octet-stream",
204+
headers: headers,
200205
success: function() {
201206
// field.val(JSON.stringify({_tmp_path: ingress_path, name: file.name, content_type: file.type}));
202207
window.gcp_number_of_files--;

django_gcp/storage/compress.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import io
2-
import zlib
31
from gzip import GzipFile
2+
import io
43
from typing import Optional
4+
import zlib
55

66
from .utils import to_bytes
77

django_gcp/storage/fields.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,11 @@
1212

1313
from .forms import CloudObjectFormField
1414
from .gcloud import GoogleCloudStorage
15-
from .operations import blob_exists, copy_blob, get_signed_upload_url
15+
from .operations import UNLIMITED_MAX_SIZE, blob_exists, copy_blob, get_signed_upload_url
1616
from .widgets import DEFAULT_ACCEPT_MIMETYPE, CloudObjectWidget
1717

1818
logger = logging.getLogger(__name__)
1919

20-
21-
# 32 megabyte default size chosen to be consistent with Cloud Run's
22-
# largest request size
23-
DEFAULT_MAX_SIZE_BYTES = 32 * 1024 * 1024
24-
2520
DEFAULT_OVERWRITE_MODE = "never"
2621

2722
OVERWRITE_MODES = [
@@ -102,8 +97,8 @@ def __init__(
10297
ingress_to="_tmp/",
10398
store_key="media",
10499
accept_mimetype=DEFAULT_ACCEPT_MIMETYPE,
105-
max_size_bytes=DEFAULT_MAX_SIZE_BYTES,
106100
overwrite_mode=DEFAULT_OVERWRITE_MODE,
101+
max_size_bytes=None,
107102
on_change=None,
108103
update_attributes=None,
109104
**kwargs,
@@ -114,6 +109,7 @@ def __init__(
114109
self._on_commit_valid = None
115110
self._versioning_enabled = None
116111
self._temporary_path = None
112+
self._max_size_bytes = max_size_bytes
117113
self._primary_key_set_explicitly = "primary_key" in kwargs
118114
self._choices_set_explicitly = "choices" in kwargs
119115
self.overwrite_mode = overwrite_mode
@@ -123,7 +119,6 @@ def __init__(
123119
self.ingress_path = None
124120
self.store_key = store_key
125121
self.accept_mimetype = accept_mimetype
126-
self.max_size_bytes = max_size_bytes
127122
self.on_change = on_change
128123
kwargs["default"] = kwargs.pop("default", None)
129124
kwargs["help_text"] = kwargs.pop("help_text", "GCP cloud storage object")
@@ -178,6 +173,7 @@ def formfield(self, **kwargs):
178173
widget = CloudObjectWidget(
179174
accept_mimetype=self.accept_mimetype,
180175
signed_ingress_url=self._get_signed_ingress_url(),
176+
max_size_bytes=self.max_size_bytes,
181177
ingress_path=self._get_temporary_path(),
182178
)
183179

@@ -195,6 +191,23 @@ def override_blobfield_value(self):
195191
DEFAULT_OVERRIDE_BLOBFIELD_VALUE,
196192
)
197193

194+
@property
195+
def max_size_bytes(self):
196+
"""Shortcut to determine the max size in bytes allowable as an upload
197+
198+
Defaults to unlimited upload size (which is dangerous, but since you're
199+
signing a URL for the user to upload we assume you've established trust
200+
in the user and know what you're doing!).
201+
"""
202+
if self._max_size_bytes is not None:
203+
return self._max_size_bytes
204+
205+
return getattr(
206+
settings,
207+
"GCP_STORAGE_BLOBFIELD_MAX_SIZE_BYTES",
208+
UNLIMITED_MAX_SIZE,
209+
)
210+
198211
def clean(self, value, model_instance, skip_validation=False):
199212
"""Clean the value to ensure correct state and on_commit hooks.
200213
This method is called during clean_fields on model save.

django_gcp/storage/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import logging
2+
23
from django.forms.fields import JSONField
34

45
from .widgets import CloudObjectWidget
56

6-
77
logger = logging.getLogger(__name__)
88

99

django_gcp/storage/gcloud.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import mimetypes
33
from tempfile import SpooledTemporaryFile
4+
45
from django.conf import settings
56
from django.core.exceptions import SuspiciousOperation
67
from django.core.files.base import File
@@ -15,7 +16,6 @@
1516
from .settings import StorageSettings
1617
from .utils import clean_name, get_available_overwrite_name, safe_join, to_bytes
1718

18-
1919
CONTENT_ENCODING = "content_encoding"
2020
CONTENT_TYPE = "content_type"
2121

django_gcp/storage/operations.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
logger = logging.getLogger(__name__)
1313

14+
UNLIMITED_MAX_SIZE = 0
15+
1416

1517
def blob_exists(bucket, blob_name):
1618
"""Quick check that a blob with a given name exists in a bucket"""
@@ -182,7 +184,7 @@ def get_generations(bucket, blob_name):
182184
return list(bucket.client.list_blobs(bucket.name, versions=True, prefix=blob_name))
183185

184186

185-
def get_signed_upload_url(bucket, blob_name, timedelta=None, max_size_bytes=None, **kwargs):
187+
def get_signed_upload_url(bucket, blob_name, timedelta=None, max_size_bytes=UNLIMITED_MAX_SIZE, **kwargs):
186188
"""Get a signed URL for uploading a blob to GCS
187189
188190
:param google.cloud.storage.Bucket bucket: The bucket to which the blob will be uploaded
@@ -195,7 +197,8 @@ def get_signed_upload_url(bucket, blob_name, timedelta=None, max_size_bytes=None
195197
timedelta = datetime.timedelta(minutes=60)
196198

197199
blob = bucket.blob(blob_name)
198-
if max_size_bytes is not None:
200+
201+
if max_size_bytes != 0:
199202
content_length_range = f"0,{max_size_bytes}"
200203
headers = kwargs.pop("headers", {})
201204
headers["X-Goog-Content-Length-Range"] = content_length_range

django_gcp/storage/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from datetime import timedelta
2+
23
from django.conf import settings as django_settings
34
from django.core.exceptions import ImproperlyConfigured
45
from django.core.signals import setting_changed
56

6-
77
DEFAULT_GZIP_CONTENT_TYPES = (
88
"text/css",
99
"text/javascript",

django_gcp/storage/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import posixpath
3+
34
from django.core.exceptions import SuspiciousFileOperation
45
from django.utils.encoding import force_bytes
56

django_gcp/storage/widgets.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.conf import settings
44
from django.forms import Widget
55

6+
from .operations import UNLIMITED_MAX_SIZE
7+
68
DEFAULT_ACCEPT_MIMETYPE = "*/*"
79

810

@@ -28,6 +30,7 @@ def __init__(
2830
self,
2931
*args,
3032
signed_ingress_url=None,
33+
max_size_bytes=UNLIMITED_MAX_SIZE,
3134
ingress_path=None,
3235
accept_mimetype=DEFAULT_ACCEPT_MIMETYPE,
3336
**kwargs,
@@ -37,14 +40,15 @@ def __init__(
3740
self._kwargs = kwargs
3841
self.accept_mimetype = accept_mimetype
3942
self.signed_ingress_url = signed_ingress_url
43+
self.max_size_bytes = max_size_bytes
4044
self.ingress_path = ingress_path
4145

4246
if "unfold" in settings.INSTALLED_APPS:
4347
self.template_name = "django_gcp/contrib/unfold/cloud_object_widget.html"
4448

4549
def get_context(self, name, value, attrs):
4650
context = super().get_context(name, value, attrs)
47-
51+
context["max_size_bytes"] = self.max_size_bytes
4852
context["signed_ingress_url"] = self.signed_ingress_url
4953
context["ingress_path"] = self.ingress_path
5054
context["accept_mimetype"] = self.accept_mimetype

django_gcp/tasks/_pilot/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import os
55
from typing import Any, Callable, Dict, Generator, List, Tuple, Union
6+
67
from google import auth
78
from google.auth import iam
89
from google.auth.credentials import Credentials
@@ -16,7 +17,6 @@
1617

1718
from . import exceptions
1819

19-
2020
DEFAULT_PROJECT = os.environ.get("GCP_PROJECT", None)
2121
DEFAULT_LOCATION = os.environ.get("GCP_LOCATION", None)
2222
DEFAULT_SERVICE_ACCOUNT = os.environ.get("GCP_SERVICE_ACCOUNT", None)

django_gcp/tasks/_pilot/mocker.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from contextlib import ExitStack
22
from functools import wraps
33
from unittest.mock import patch
4+
45
from google.oauth2.service_account import Credentials
56

67

django_gcp/tasks/_pilot/pubsub.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# https://googleapis.dev/python/pubsub/latest/index.html
22
import base64
3-
import json
43
from dataclasses import dataclass
4+
import json
55
from typing import Any, AsyncIterator, Callable, Dict, Union
6+
67
from google.api_core.exceptions import AlreadyExists, NotFound
78
from google.cloud import pubsub_v1
89
from google.protobuf.field_mask_pb2 import FieldMask

django_gcp/tasks/_pilot/scheduler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# More Information <https://cloud.google.com/scheduler/docs/reference/rest>
22
import os
33
from typing import Dict, Generator
4+
45
from google.api_core.exceptions import NotFound
56
from google.cloud import scheduler
67

78
from .base import AppEngineBasedService, GoogleCloudPilotAPI
89

9-
1010
DEFAULT_TIMEZONE = os.environ.get("TIMEZONE", "Europe/London") # UTC
1111
MAX_TIMEOUT = 30 * 60 # max allowed to HTTP endpoints is 30 minutes
1212

django_gcp/tasks/_pilot/tasks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Reference: https://googleapis.dev/python/cloudtasks/latest/tasks_v2/cloud_tasks.html
2-
import uuid
32
from datetime import datetime, timedelta
43
from typing import Dict
4+
import uuid
5+
56
from google.api_core.exceptions import FailedPrecondition
67
from google.cloud import tasks_v2
78
from google.protobuf import timestamp_pb2

django_gcp/tasks/helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
23
from asgiref.sync import async_to_sync
34

45

0 commit comments

Comments
 (0)