Skip to content

feat: database-backed artifact blob storage #8992

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

Merged
merged 94 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
90c3d35
feat: blobdb app
jennifer-richards Mar 5, 2025
1343930
feat: blob model
jennifer-richards Mar 5, 2025
d281690
fix: app name
jennifer-richards Mar 5, 2025
239b52d
feat: admin for blob model
jennifer-richards Mar 5, 2025
a448c6c
feat: database router
jennifer-richards Mar 6, 2025
975a7fd
feat: BlobdbStorage storage class
jennifer-richards Mar 6, 2025
5d6bd49
refactor: storage metadata via File subclass
jennifer-richards Mar 6, 2025
8a6a2dc
refactor: eliminate in_flight_custom_metadata
jennifer-richards Mar 6, 2025
30d794d
chore: raise blobstore exceptions in development mode
jennifer-richards Mar 6, 2025
06fe011
feat: StagedBlobStore + StorageObjectStorageMixin
jennifer-richards Mar 6, 2025
71616cd
feat: StoredObject.committed timestamp
jennifer-richards Mar 6, 2025
1fe6fe5
refactor: some refactoring (wip)
jennifer-richards Mar 7, 2025
27fb764
refactor: better separation of StorageObject and Storage
jennifer-richards Mar 7, 2025
da7964f
feat: basic 2-blobstore commit
jennifer-richards Mar 7, 2025
02d2129
fix: StorageObject should have been StoredObject
jennifer-richards Mar 7, 2025
977bd91
fix: propagate metadata to final_storage
jennifer-richards Mar 7, 2025
1874dae
feat: easier config of StagedBlobStorage
jennifer-richards Mar 7, 2025
faab18b
refactor: save-specific commit
jennifer-richards Mar 7, 2025
c60e4e2
chore(dev): add dev database for blob storage
jennifer-richards Mar 7, 2025
8d21067
chore(dev): migrate blobdb in app-init.sh
jennifer-richards Mar 7, 2025
f7ec352
chore(dev): use staged blob storages for dev env
jennifer-richards Mar 7, 2025
9b0b0e1
feat: deletion for StagedBlobStorage
jennifer-richards Mar 7, 2025
b3a1a0a
refactor: single-query update of committed field
jennifer-richards Mar 7, 2025
f78f678
fix: better commit behavior for celery
jennifer-richards Mar 8, 2025
bea00b6
style: black + copyright stmts
jennifer-richards Mar 13, 2025
363bbc9
ci: Merge pull request #8641 from jennifer-richards/db-storage
rjsparks Mar 14, 2025
4d68976
fix: dont test for v1 apis into the blobdb app models
rjsparks Mar 14, 2025
1583284
Merge pull request #8663 from rjsparks/no_blobdb_api
jennifer-richards Mar 14, 2025
061f71b
chore: fix storage configs (esp for tests) (#8664)
jennifer-richards Mar 14, 2025
0d5e52b
chore: remove empty blobdb.views
jennifer-richards Mar 17, 2025
a2bf595
feat: mtime/content-type for blobdb
jennifer-richards Mar 20, 2025
cf1158c
test: test BlobdbStorage
jennifer-richards Mar 20, 2025
867606e
refactor: split storage utils from Storage class
jennifer-richards Mar 20, 2025
ba1caf2
fix: circular import
jennifer-richards Mar 20, 2025
70cd0fa
chore: CRLFs in ISSUE_TEMPLATE/config.yml
jennifer-richards Mar 20, 2025
e93fd7d
chore: add resources.py
jennifer-richards Mar 20, 2025
5a25dff
refactor: extract metadata from BlobFile
jennifer-richards Mar 21, 2025
23d610f
refactor: reconcile dueling MetadataFiles
jennifer-richards Mar 21, 2025
96f99d1
refactor: plumb metadata and reorg code
jennifer-richards Mar 21, 2025
b896c5e
fix: handle content type properly
jennifer-richards Mar 21, 2025
83e2177
chore: remove blobdb/resources.py
jennifer-richards Apr 2, 2025
b7d2d78
test: ietf.blobdb->OMITTED_APPS; drop ad hoc ignore
jennifer-richards Apr 2, 2025
48af1d7
chore: copyright
jennifer-richards Apr 2, 2025
35558d8
chore: Remove CR
rjsparks Apr 3, 2025
c047b50
Merge remote-tracking branch 'ietf-tools/main' into feat/blobstage
rjsparks Apr 3, 2025
1dbfac8
Merge branch 'feat/blobstage' into refactor-metadata-handling
jennifer-richards Apr 3, 2025
1400af1
Merge pull request #8713 from jennifer-richards/refactor-metadata-han…
jennifer-richards Apr 7, 2025
0aa516b
refactor: storage_backends.py -> storage.py
jennifer-richards May 9, 2025
098392c
refactor: remove StagedBlobStorage
jennifer-richards May 9, 2025
5e1dd89
chore: fix mypy lint
jennifer-richards May 9, 2025
412b436
chore: remove StagedBlobStorage tasks
jennifer-richards May 14, 2025
d4c636c
refactor: committed -> replicated
jennifer-richards May 15, 2025
956e77f
feat: admin improvements
jennifer-richards May 15, 2025
34307ad
chore: configure storages for dev
jennifer-richards May 15, 2025
bd16621
feat: copy blobs to "replica" storage when stored
jennifer-richards May 15, 2025
bbb034a
feat: propagate blob deletion, too
jennifer-richards May 16, 2025
2c51dc2
fix: reset replicated flag on delete
jennifer-richards May 16, 2025
2292d21
chore: remove replication
jennifer-richards May 16, 2025
e9d0f12
refactor: unify artifact storage configs
jennifer-richards May 16, 2025
11748c6
fix: mypy lint
jennifer-richards May 16, 2025
7242416
fix: more mypy lint...
jennifer-richards May 16, 2025
2e0b1d5
fix: overwrite, don't rename, in BlobdbStorage
jennifer-richards May 16, 2025
f79a94d
fix: more lint
jennifer-richards May 16, 2025
6bec4db
feat: hooks for Blobdb watcher notifications
jennifer-richards May 21, 2025
b0a7f39
feat: pybob the blob replicator
jennifer-richards May 21, 2025
313f13b
chore: fix quoting in celery docker-init.sh
jennifer-richards May 21, 2025
7b2011e
fix: adjust replicator/storage to work together
jennifer-richards May 21, 2025
6cd69ed
chore: concurrency=1 worker for blobdb queue
jennifer-richards May 21, 2025
eb3487a
feat: remove blob staging; blobdb is primary artifact storage; replic…
jennifer-richards May 21, 2025
9197984
feat: blobdb replication module + settings
jennifer-richards May 23, 2025
2548207
feat: en/disable replication per settings
jennifer-richards May 23, 2025
bbe9dcd
feat: per-bucket replication en/disable
jennifer-richards May 23, 2025
8a83dcb
fix: use correct type for EXCLUDE_BUCKETS
jennifer-richards May 23, 2025
3c99d0f
style: reorder methods
jennifer-richards May 23, 2025
a2a3803
feat: logging/exceptions in replication.py
jennifer-richards May 23, 2025
7d70f9a
feat: retry replication
jennifer-richards May 23, 2025
8357f21
fix: valid storage pattern setting
jennifer-richards May 23, 2025
613056e
chore: consistent naming
jennifer-richards May 23, 2025
1149dbb
feat: detect missing replica storage configs
jennifer-richards May 23, 2025
0f08503
fix: ensure mtime is set for the replica
jennifer-richards May 23, 2025
155d45b
fix: log message grammar
jennifer-richards May 24, 2025
66658f6
Merge pull request #8915 from jennifer-richards/mature-replication
jennifer-richards May 24, 2025
d8a055f
Merge pull request #8945 from ietf-tools/main
jennifer-richards May 30, 2025
9ab630a
feat: blobdb + k8s deployment (#8946)
jennifer-richards May 30, 2025
eff7627
ci: typo in k8s/settings_local.py
jennifer-richards Jun 1, 2025
d794f78
chore: additional blobdb replicator logging (#8950)
jennifer-richards Jun 2, 2025
d0715b5
fix: db transactions for blobdb save/delete (#8951)
jennifer-richards Jun 2, 2025
bc6dcf1
fix: use correct DB for transaction (#8952)
jennifer-richards Jun 3, 2025
e68e590
fix: on_commit needs "using" also (#8953)
jennifer-richards Jun 3, 2025
78fc4ef
Merge branch 'main' into feat/blobstage
jennifer-richards Jun 12, 2025
4f8ca72
chore: unused imports in settings_locals
jennifer-richards Jun 12, 2025
e461426
refactor: typing-friendly blob handling in replication.py
jennifer-richards Jun 12, 2025
b4899ef
chore: more lint
jennifer-richards Jun 12, 2025
21fa4d8
chore: fix lint affecting GHA tests
jennifer-richards Jun 12, 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
3 changes: 3 additions & 0 deletions dev/build/migration-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
echo "Running Datatracker migrations..."
./ietf/manage.py migrate --settings=settings_local

echo "Running Blobdb migrations ..."
./ietf/manage.py migrate --settings=settings_local --database=blobdb

echo "Done!"
5 changes: 4 additions & 1 deletion dev/build/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# Environment config:
#
# CONTAINER_ROLE - datatracker, celery, or beat (defaults to datatracker)
# CONTAINER_ROLE - datatracker, celery, beat, migrations, or replicator (defaults to datatracker)
#
case "${CONTAINER_ROLE:-datatracker}" in
auth)
Expand All @@ -20,6 +20,9 @@ case "${CONTAINER_ROLE:-datatracker}" in
migrations)
exec ./migration-start.sh
;;
replicator)
exec ./celery-start.sh --app=ietf worker --queues=blobdb --concurrency=1
;;
*)
echo "Unknown role '${CONTAINER_ROLE}'"
exit 255
Expand Down
2 changes: 1 addition & 1 deletion dev/celery/docker-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ if [[ -n "${DEV_MODE}" ]]; then
--recursive \
--debounce-interval 5 \
-- \
celery --app="${CELERY_APP:-ietf}" "${CELERY_OPTS[@]}" "$@" &
celery --app="${CELERY_APP:-ietf}" "${CELERY_OPTS[@]}" $@ &
celery_pid=$!
else
celery --app="${CELERY_APP:-ietf}" "${CELERY_OPTS[@]}" "$@" &
Expand Down
21 changes: 0 additions & 21 deletions dev/deploy-to-container/settings_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# -*- coding: utf-8 -*-

from ietf.settings import * # pyflakes:ignore
from ietf.settings import STORAGES, MORE_STORAGE_NAMES, BLOBSTORAGE_CONNECT_TIMEOUT, BLOBSTORAGE_READ_TIMEOUT, BLOBSTORAGE_MAX_ATTEMPTS
import botocore.config

ALLOWED_HOSTS = ['*']

Expand Down Expand Up @@ -81,22 +79,3 @@

# OIDC configuration
SITE_URL = 'https://__HOSTNAME__'

for storagename in MORE_STORAGE_NAMES:
STORAGES[storagename] = {
"BACKEND": "ietf.doc.storage_backends.CustomS3Storage",
"OPTIONS": dict(
endpoint_url="http://blobstore:9000",
access_key="minio_root",
secret_key="minio_pass",
security_token=None,
client_config=botocore.config.Config(
signature_version="s3v4",
connect_timeout=BLOBSTORAGE_CONNECT_TIMEOUT,
read_timeout=BLOBSTORAGE_READ_TIMEOUT,
retries={"total_max_attempts": BLOBSTORAGE_MAX_ATTEMPTS},
),
verify=False,
bucket_name=f"test-{storagename}",
),
}
21 changes: 0 additions & 21 deletions dev/diff/settings_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# -*- coding: utf-8 -*-

from ietf.settings import * # pyflakes:ignore
from ietf.settings import STORAGES, MORE_STORAGE_NAMES, BLOBSTORAGE_CONNECT_TIMEOUT, BLOBSTORAGE_READ_TIMEOUT, BLOBSTORAGE_MAX_ATTEMPTS
import botocore.config

ALLOWED_HOSTS = ['*']

Expand Down Expand Up @@ -68,22 +66,3 @@
SLIDE_STAGING_PATH = 'test/staging/'

DE_GFM_BINARY = '/usr/local/bin/de-gfm'

for storagename in MORE_STORAGE_NAMES:
STORAGES[storagename] = {
"BACKEND": "ietf.doc.storage_backends.CustomS3Storage",
"OPTIONS": dict(
endpoint_url="http://blobstore:9000",
access_key="minio_root",
secret_key="minio_pass",
security_token=None,
client_config=botocore.config.Config(
signature_version="s3v4",
connect_timeout=BLOBSTORAGE_CONNECT_TIMEOUT,
read_timeout=BLOBSTORAGE_READ_TIMEOUT,
retries={"total_max_attempts": BLOBSTORAGE_MAX_ATTEMPTS},
),
verify=False,
bucket_name=f"test-{storagename}",
),
}
21 changes: 0 additions & 21 deletions dev/tests/settings_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
# -*- coding: utf-8 -*-

from ietf.settings import * # pyflakes:ignore
from ietf.settings import STORAGES, MORE_STORAGE_NAMES, BLOBSTORAGE_CONNECT_TIMEOUT, BLOBSTORAGE_READ_TIMEOUT, BLOBSTORAGE_MAX_ATTEMPTS
import botocore.config

ALLOWED_HOSTS = ['*']

Expand Down Expand Up @@ -67,22 +65,3 @@
SLIDE_STAGING_PATH = 'test/staging/'

DE_GFM_BINARY = '/usr/local/bin/de-gfm'

for storagename in MORE_STORAGE_NAMES:
STORAGES[storagename] = {
"BACKEND": "ietf.doc.storage_backends.CustomS3Storage",
"OPTIONS": dict(
endpoint_url="http://blobstore:9000",
access_key="minio_root",
secret_key="minio_pass",
security_token=None,
client_config=botocore.config.Config(
signature_version="s3v4",
connect_timeout=BLOBSTORAGE_CONNECT_TIMEOUT,
read_timeout=BLOBSTORAGE_READ_TIMEOUT,
retries={"total_max_attempts": BLOBSTORAGE_MAX_ATTEMPTS},
),
verify=False,
bucket_name=f"test-{storagename}",
),
}
34 changes: 33 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,44 @@ services:
- .:/workspace
- app-assets:/assets

replicator:
build:
context: .
dockerfile: docker/celery.Dockerfile
init: true
environment:
CELERY_APP: ietf
CELERY_ROLE: worker
UPDATE_REQUIREMENTS_FROM: requirements.txt
DEV_MODE: "yes"
command:
- '--loglevel=INFO'
- '--queues=blobdb'
- '--concurrency=1'

depends_on:
- db
restart: unless-stopped
stop_grace_period: 1m
volumes:
- .:/workspace
- app-assets:/assets

blobstore:
image: ghcr.io/ietf-tools/datatracker-devblobstore:latest
restart: unless-stopped
volumes:
- "minio-data:/data"


blobdb:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: blob
POSTGRES_USER: dt
POSTGRES_PASSWORD: abcd1234
volumes:
- blobdb-data:/var/lib/postgresql/data

# Celery Beat is a periodic task runner. It is not normally needed for development,
# but can be enabled by uncommenting the following.
Expand All @@ -118,3 +149,4 @@ volumes:
postgresdb-data:
app-assets:
minio-data:
blobdb-data:
60 changes: 39 additions & 21 deletions docker/configs/settings_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@
# -*- coding: utf-8 -*-

from ietf.settings import * # pyflakes:ignore
from ietf.settings import STORAGES, MORE_STORAGE_NAMES, BLOBSTORAGE_CONNECT_TIMEOUT, BLOBSTORAGE_READ_TIMEOUT, BLOBSTORAGE_MAX_ATTEMPTS
import botocore.config
from ietf.settings import (
ARTIFACT_STORAGE_NAMES,
STORAGES,
BLOBSTORAGE_MAX_ATTEMPTS,
BLOBSTORAGE_READ_TIMEOUT,
BLOBSTORAGE_CONNECT_TIMEOUT,
)

ALLOWED_HOSTS = ['*']

from ietf.settings_postgresqldb import DATABASES # pyflakes:ignore
DATABASE_ROUTERS = ["ietf.blobdb.routers.BlobdbStorageRouter"]
BLOBDB_DATABASE = "blobdb"
BLOBDB_REPLICATION = {
"ENABLED": True,
"DEST_STORAGE_PATTERN": "r2-{bucket}",
"INCLUDE_BUCKETS": ARTIFACT_STORAGE_NAMES,
"EXCLUDE_BUCKETS": ["staging"],
"VERBOSE_LOGGING": True,
}

IDSUBMIT_IDNITS_BINARY = "/usr/local/bin/idnits"
IDSUBMIT_STAGING_PATH = "/assets/www6s/staging/"
Expand Down Expand Up @@ -39,25 +53,6 @@
# DEV_TEMPLATE_CONTEXT_PROCESSORS = [
# 'ietf.context_processors.sql_debug',
# ]
for storagename in MORE_STORAGE_NAMES:
STORAGES[storagename] = {
"BACKEND": "ietf.doc.storage_backends.CustomS3Storage",
"OPTIONS": dict(
endpoint_url="http://blobstore:9000",
access_key="minio_root",
secret_key="minio_pass",
security_token=None,
client_config=botocore.config.Config(
signature_version="s3v4",
connect_timeout=BLOBSTORAGE_CONNECT_TIMEOUT,
read_timeout=BLOBSTORAGE_READ_TIMEOUT,
retries={"total_max_attempts": BLOBSTORAGE_MAX_ATTEMPTS},
),
verify=False,
bucket_name=storagename,
),
}


DOCUMENT_PATH_PATTERN = '/assets/ietfdata/doc/{doc.type_id}/'
INTERNET_DRAFT_PATH = '/assets/ietf-ftp/internet-drafts/'
Expand All @@ -80,3 +75,26 @@

STATIC_IETF_ORG = "/_static"
STATIC_IETF_ORG_INTERNAL = "http://static"


# Blob replication storage for dev
import botocore.config
for storagename in ARTIFACT_STORAGE_NAMES:
replica_storagename = f"r2-{storagename}"
STORAGES[replica_storagename] = {
"BACKEND": "ietf.doc.storage.MetadataS3Storage",
"OPTIONS": dict(
endpoint_url="http://blobstore:9000",
access_key="minio_root",
secret_key="minio_pass",
security_token=None,
client_config=botocore.config.Config(
signature_version="s3v4",
connect_timeout=BLOBSTORAGE_CONNECT_TIMEOUT,
read_timeout=BLOBSTORAGE_READ_TIMEOUT,
retries={"total_max_attempts": BLOBSTORAGE_MAX_ATTEMPTS},
),
verify=False,
bucket_name=f"{storagename}",
),
}
8 changes: 8 additions & 0 deletions docker/configs/settings_postgresqldb.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,12 @@
'USER': 'django',
'PASSWORD': 'RkTkDPFnKpko',
},
'blobdb': {
'HOST': 'blobdb',
'PORT': 5432,
'NAME': 'blob',
'ENGINE': 'django.db.backends.postgresql',
'USER': 'dt',
'PASSWORD': 'abcd1234',
},
}
4 changes: 2 additions & 2 deletions docker/scripts/app-configure-blobstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import os
import sys

from ietf.settings import MORE_STORAGE_NAMES
from ietf.settings import ARTIFACT_STORAGE_NAMES


def init_blobstore():
Expand All @@ -19,7 +19,7 @@ def init_blobstore():
aws_session_token=None,
config=botocore.config.Config(signature_version="s3v4"),
)
for bucketname in MORE_STORAGE_NAMES:
for bucketname in ARTIFACT_STORAGE_NAMES:
try:
blobstore.create_bucket(
Bucket=f"{os.environ.get('BLOB_STORE_BUCKET_PREFIX', '')}{bucketname}".strip()
Expand Down
4 changes: 3 additions & 1 deletion docker/scripts/app-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,11 @@ echo "Running initial checks..."
/usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check --settings=settings_local

# Migrate, adjusting to what the current state of the underlying database might be:

/usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --fake-initial --settings=settings_local

# Apply migrations to the blobdb database as well (most are skipped)
/usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --settings=settings_local --database=blobdb

if [ -z "$EDITOR_VSCODE" ]; then
CODE=0
python -m smtpd -n -c DebuggingServer localhost:2025 &
Expand Down
1 change: 1 addition & 0 deletions ietf/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'ietf.secr.proceedings',
'ietf.ipr',
'ietf.status',
'ietf.blobdb',
)

class CustomApiTests(TestCase):
Expand Down
Empty file added ietf/blobdb/__init__.py
Empty file.
31 changes: 31 additions & 0 deletions ietf/blobdb/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright The IETF Trust 2025, All Rights Reserved
from django.contrib import admin
from django.db.models.functions import Length
from rangefilter.filters import DateRangeQuickSelectListFilterBuilder

from .models import Blob


@admin.register(Blob)
class BlobAdmin(admin.ModelAdmin):
list_display = ["bucket", "name", "object_size", "modified", "mtime", "content_type"]
list_filter = [
"bucket",
"content_type",
("modified", DateRangeQuickSelectListFilterBuilder()),
("mtime", DateRangeQuickSelectListFilterBuilder()),
]
search_fields = ["name"]
list_display_links = ["name"]

def get_queryset(self, request):
return (
super().get_queryset(request)
.defer("content") # don't load this unless we want it
.annotate(object_size=Length("content")) # accessed via object_size()
)

@admin.display(ordering="object_size")
def object_size(self, instance):
"""Get the size of the object"""
return instance.object_size # annotation added in get_queryset()
30 changes: 30 additions & 0 deletions ietf/blobdb/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright The IETF Trust 2025, All Rights Reserved
from django.apps import AppConfig


class BlobdbConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "ietf.blobdb"

def ready(self):
"""Initialize app once the registries / settings are populated"""
from django.conf import settings

# Validate that the DB is set up
db = get_blobdb() # depends on settings.BLOBDB_DATABASE
if db is not None and db not in settings.DATABASES:
raise RuntimeError(
f"settings.BLOBDB_DATABASE is '{db}' but that is not present in settings.DATABASES"
)

# Validate replication settings
from .replication import validate_replication_settings

validate_replication_settings()


def get_blobdb():
"""Retrieve the blobdb setting from Django's settings"""
from django.conf import settings

return getattr(settings, "BLOBDB_DATABASE", None)
13 changes: 13 additions & 0 deletions ietf/blobdb/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright The IETF Trust 2025, All Rights Reserved
import factory

from .models import Blob


class BlobFactory(factory.django.DjangoModelFactory):
class Meta:
model = Blob

name = factory.Faker("file_path")
bucket = factory.Faker("word")
content = factory.Faker("binary", length=32) # careful, default length is 1e6
Loading
Loading