Skip to content

Commit 0405a67

Browse files
committed
Use django-storages to upload raster file to S3-like bucket + set names
as unique
1 parent 9be566b commit 0405a67

File tree

8 files changed

+105
-11
lines changed

8 files changed

+105
-11
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DJANGO_SECRET_KEY="local"
2+
DJANGO_AWS_ACCESS_KEY_ID=""
3+
DJANGO_AWS_SECRET_ACCESS_KEY=""
4+
DJANGO_AWS_STORAGE_BUCKET_NAME=""

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ VBOS Django application and data services. Check out the project's [documentatio
77

88
# Prerequisites
99

10-
- [Docker](https://docs.docker.com/docker-for-mac/install/)
10+
- [Docker](https://docs.docker.com/docker-for-mac/install/)
1111

1212
# Local Development
1313

@@ -21,3 +21,7 @@ Run a command inside the docker container:
2121
```bash
2222
docker-compose run --rm web [command]
2323
```
24+
25+
# Configuration
26+
27+
Copy the `.env.example` file to `.env` and edit the variables you need to configure the access to the DigitalOcean Spaces (S3 compatible).

docker-compose.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ services:
1616
web:
1717
restart: always
1818
environment:
19-
- DJANGO_SECRET_KEY=local
19+
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
20+
- DJANGO_AWS_ACCESS_KEY_ID=${DJANGO_AWS_ACCESS_KEY_ID}
21+
- DJANGO_AWS_SECRET_ACCESS_KEY=${DJANGO_AWS_SECRET_ACCESS_KEY}
22+
- DJANGO_AWS_STORAGE_BUCKET_NAME=${DJANGO_AWS_STORAGE_BUCKET_NAME}
2023
build: ./
2124
command: >
2225
bash -c "python3 wait_for_postgres.py &&
@@ -54,5 +57,5 @@ services:
5457
ports:
5558
- "8002:8000"
5659
volumes:
57-
- ./data:/data # Optional: mount local directory with your raster files
60+
- ./data:/data # Optional: mount local directory with your raster files
5861
restart: unless-stopped

requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ drf_spectacular==0.28.0
2020
django-cors-headers==4.7.0
2121
drf-excel==2.5.3
2222

23+
# Storage
24+
django-storages==1.14.6
25+
boto3==1.40.26
26+
whitenoise
27+
2328
# Developer Tools
2429
ipdb==0.13.13
2530
ipython==8.30.0

vbos/config/production.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44

55
class Production(Common):
6+
DEBUG = False
67
INSTALLED_APPS = Common.INSTALLED_APPS
78
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")
89
# Site
@@ -13,13 +14,26 @@ class Production(Common):
1314
# Static files (CSS, JavaScript, Images)
1415
# https://docs.djangoproject.com/en/2.0/howto/static-files/
1516
# http://django-storages.readthedocs.org/en/latest/index.html
17+
INSTALLED_APPS += ("storages",)
18+
STORAGES = {
19+
"default": {"BACKEND": "storages.backends.s3.S3Storage"},
20+
"staticfiles": {
21+
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
22+
},
23+
}
24+
AWS_S3_OBJECT_PARAMETERS = {
25+
"CacheControl": "max-age=2592000",
26+
}
27+
AWS_S3_ENDPOINT_URL = os.getenv(
28+
"DJANGO_AWS_S3_ENDPOINT_URL", "https://syd1.digitaloceanspaces.com"
29+
)
1630
AWS_ACCESS_KEY_ID = os.getenv("DJANGO_AWS_ACCESS_KEY_ID")
1731
AWS_SECRET_ACCESS_KEY = os.getenv("DJANGO_AWS_SECRET_ACCESS_KEY")
1832
AWS_STORAGE_BUCKET_NAME = os.getenv("DJANGO_AWS_STORAGE_BUCKET_NAME")
1933
AWS_DEFAULT_ACL = "public-read"
2034
AWS_AUTO_CREATE_BUCKET = True
2135
AWS_QUERYSTRING_AUTH = False
22-
MEDIA_URL = f"https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/"
36+
MEDIA_URL = f"https://{AWS_STORAGE_BUCKET_NAME}.syd1.digitaloceanspaces.com/"
2337

2438
# https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#cache-control
2539
# Response can be cached by browser and any intermediary caches (i.e. it is "public") for up to 1 day
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Generated by Django 5.2.5 on 2025-09-11 11:04
2+
3+
import django.core.validators
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("datasets", "0003_rasterfile_rasterdataset"),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name="rasterdataset",
16+
name="name",
17+
field=models.CharField(max_length=155, unique=True),
18+
),
19+
migrations.AlterField(
20+
model_name="rasterfile",
21+
name="file",
22+
field=models.FileField(
23+
unique=True,
24+
upload_to="staging/raster/",
25+
validators=[
26+
django.core.validators.FileExtensionValidator(
27+
allowed_extensions=["tiff", "tif", "geotiff", "gtiff"]
28+
)
29+
],
30+
),
31+
),
32+
migrations.AlterField(
33+
model_name="rasterfile",
34+
name="name",
35+
field=models.CharField(max_length=155, unique=True),
36+
),
37+
migrations.AlterField(
38+
model_name="tabulardataset",
39+
name="name",
40+
field=models.CharField(max_length=155, unique=True),
41+
),
42+
migrations.AlterField(
43+
model_name="vectordataset",
44+
name="name",
45+
field=models.CharField(max_length=155, unique=True),
46+
),
47+
]

vbos/datasets/models.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
from django.contrib.gis.db import models
2+
from django.conf import settings
23
from django.core.validators import FileExtensionValidator
34
from django.db.models.fields.files import default_storage
45
from django.db.models.signals import pre_delete
56
from django.dispatch import receiver
67

8+
UPLOAD_TO = "staging/raster/" if settings.DEBUG else "production/raster/"
9+
710

811
class RasterFile(models.Model):
9-
name = models.CharField(max_length=155)
12+
name = models.CharField(max_length=155, unique=True)
1013
created = models.DateTimeField(auto_now_add=True)
1114
file = models.FileField(
12-
upload_to="raster/",
15+
upload_to=UPLOAD_TO,
16+
unique=True,
1317
validators=[
1418
FileExtensionValidator(
1519
allowed_extensions=["tiff", "tif", "geotiff", "gtiff"]
@@ -36,7 +40,7 @@ def delete_raster_file(sender, instance, **kwargs):
3640

3741

3842
class RasterDataset(models.Model):
39-
name = models.CharField(max_length=155)
43+
name = models.CharField(max_length=155, unique=True)
4044
created = models.DateTimeField(auto_now_add=True)
4145
updated = models.DateTimeField(auto_now=True)
4246
file = models.ForeignKey(RasterFile, on_delete=models.PROTECT)
@@ -49,7 +53,7 @@ class Meta:
4953

5054

5155
class VectorDataset(models.Model):
52-
name = models.CharField(max_length=155)
56+
name = models.CharField(max_length=155, unique=True)
5357
created = models.DateTimeField(auto_now_add=True)
5458
updated = models.DateTimeField(auto_now=True)
5559

@@ -73,7 +77,7 @@ class Meta:
7377

7478

7579
class TabularDataset(models.Model):
76-
name = models.CharField(max_length=155)
80+
name = models.CharField(max_length=155, unique=True)
7781
created = models.DateTimeField(auto_now_add=True)
7882
updated = models.DateTimeField(auto_now=True)
7983

vbos/datasets/test/test_raster_models.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
from django.core.exceptions import ValidationError
55

66
from vbos.datasets.models import RasterDataset, RasterFile
7-
from genericpath import exists
87

98

109
class TestRasterModels(TestCase):
1110
def setUp(self):
1211
self.valid_file = SimpleUploadedFile(
13-
"rainfall.tif", b"file_content", content_type="image/tiff"
12+
"rainfall.tiff", b"file_content", content_type="image/tiff"
1413
)
1514
self.r_1 = RasterFile.objects.create(name="Rainfall COG", file=self.valid_file)
1615
self.r_2 = RasterFile.objects.create(
@@ -22,6 +21,17 @@ def test_deletion(self):
2221
# RasterFile can't be deleted if it's associates with a dataset
2322
with self.assertRaises(ProtectedError):
2423
self.r_1.delete()
24+
25+
# name should be unique
26+
raster = RasterFile(name="Rainfall COG 2", file="raster/coastline.tiff")
27+
with self.assertRaises(ValidationError):
28+
raster.full_clean()
29+
30+
# file path should be unique
31+
raster = RasterFile(name="Rainfall COG", file="newfile.tif")
32+
with self.assertRaises(ValidationError):
33+
raster.full_clean()
34+
2535
# modify dataset
2636
self.dataset.file = self.r_2
2737
self.dataset.save()
@@ -42,3 +52,6 @@ def test_deletion(self):
4252
raster = RasterFile(name="Test", file=invalid_file)
4353
with self.assertRaises(ValidationError):
4454
raster.full_clean()
55+
56+
def tearDown(self):
57+
RasterFile.objects.all().delete()

0 commit comments

Comments
 (0)