diff --git a/files_storage/migrations/0005_minioconfiguration_public_read_write_prefix_and_more.py b/files_storage/migrations/0005_minioconfiguration_public_read_write_prefix_and_more.py
new file mode 100644
index 000000000..69be79c94
--- /dev/null
+++ b/files_storage/migrations/0005_minioconfiguration_public_read_write_prefix_and_more.py
@@ -0,0 +1,37 @@
+# Generated by Codex on 2026-06-19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("files_storage", "0004_remove_minioconfiguration_bucket_app_subdir_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="minioconfiguration",
+ name="public_base_url",
+ field=models.URLField(
+ blank=True,
+ max_length=500,
+ null=True,
+ verbose_name="Public base URL",
+ ),
+ ),
+ migrations.AddField(
+ model_name="minioconfiguration",
+ name="write_prefix",
+ field=models.CharField(
+ blank=True,
+ max_length=128,
+ null=True,
+ verbose_name="Write prefix",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="filelocation",
+ name="uri",
+ field=models.URLField(blank=True, max_length=500, null=True, verbose_name="URI"),
+ ),
+ ]
diff --git a/files_storage/minio.py b/files_storage/minio.py
index 0cd8139ed..e30f59cc8 100644
--- a/files_storage/minio.py
+++ b/files_storage/minio.py
@@ -3,8 +3,10 @@
import json
import logging
import os
+import posixpath
from mimetypes import types_map
from tempfile import NamedTemporaryFile, TemporaryDirectory
+from urllib.parse import urljoin
from minio import Minio
from minio.error import S3Error
@@ -69,9 +71,12 @@ def __init__(
minio_access_key,
minio_secret_key,
bucket_root,
- location,
+ location=None,
minio_secure=True,
minio_http_client=None,
+ write_prefix="",
+ public_base_url="",
+ bucket_subdir=None,
):
self.bucket_root = bucket_root
self.POLICY_READ_ONLY = {
@@ -97,7 +102,22 @@ def __init__(
self.minio_secure = minio_secure
self.http_client = minio_http_client
self._client_instance = None
- self.location = location
+ self.location = location or bucket_subdir
+ self.write_prefix = self._normalize_path(write_prefix)
+ self.public_base_url = (public_base_url or "").strip()
+
+ def _normalize_path(self, path):
+ return (path or "").strip("/")
+
+ def _object_name_for_storage(self, object_name):
+ object_name = self._normalize_path(object_name)
+ if not self.write_prefix:
+ return object_name
+ return posixpath.join(self.write_prefix, object_name)
+
+ def _public_uri(self, object_name):
+ object_name = self._normalize_path(object_name)
+ return urljoin(f"{self.public_base_url.rstrip('/')}/", object_name)
@property
def _client(self):
@@ -173,7 +193,12 @@ def get_uri(self, object_name: str) -> str:
MinioStorageGetUriError
"""
try:
- url = self._client.presigned_get_object(self.bucket_root, object_name)
+ if self.public_base_url:
+ return self._public_uri(object_name)
+
+ url = self._client.presigned_get_object(
+ self.bucket_root, self._object_name_for_storage(object_name)
+ )
return url.split("?")[0]
except Exception as e:
raise MinioStorageGetUriError(
@@ -257,10 +282,11 @@ def _fput_object(self, file_path, object_name, mimetype) -> str:
------
MinioStorageNoSuchBucketError
"""
+ storage_object_name = self._object_name_for_storage(object_name)
try:
self._client.fput_object(
self.bucket_root,
- object_name=object_name,
+ object_name=storage_object_name,
file_path=file_path,
content_type=mimetype,
)
diff --git a/files_storage/models.py b/files_storage/models.py
index d1edda553..d09091c5e 100644
--- a/files_storage/models.py
+++ b/files_storage/models.py
@@ -38,6 +38,12 @@ class MinioConfiguration(CommonControlField):
bucket_root = models.CharField(
_("Bucket root"), max_length=32, null=True, blank=True
)
+ write_prefix = models.CharField(
+ _("Write prefix"), max_length=128, null=True, blank=True
+ )
+ public_base_url = models.URLField(
+ _("Public base URL"), max_length=500, null=True, blank=True
+ )
location = models.CharField(
_("Location"),
max_length=16,
@@ -62,6 +68,8 @@ class Meta:
FieldPanel("name"),
FieldPanel("host"),
FieldPanel("bucket_root"),
+ FieldPanel("write_prefix"),
+ FieldPanel("public_base_url"),
# FieldPanel("location"),
FieldPanel("access_key"),
FieldPanel("secret_key"),
@@ -85,6 +93,8 @@ def get_or_create(
secret_key=None,
secure=None,
bucket_root=None,
+ write_prefix=None,
+ public_base_url=None,
location=None,
user=None,
):
@@ -98,6 +108,8 @@ def get_or_create(
files_storage.access_key = access_key
files_storage.secret_key = secret_key
files_storage.bucket_root = bucket_root
+ files_storage.write_prefix = write_prefix
+ files_storage.public_base_url = public_base_url
files_storage.location = location
files_storage.creator = user
files_storage.save()
@@ -125,12 +137,14 @@ def get_files_storage(cls, name, minio_http_client=None):
location=obj.location,
minio_secure=obj.secure,
minio_http_client=minio_http_client,
+ write_prefix=obj.write_prefix,
+ public_base_url=obj.public_base_url,
)
class FileLocation(CommonControlField):
basename = models.CharField(_("Basename"), max_length=100, null=True, blank=True)
- uri = models.URLField(_("URI"), null=True, blank=True, max_length=200)
+ uri = models.URLField(_("URI"), null=True, blank=True, max_length=500)
autocomplete_search_field = "uri"
diff --git a/files_storage/test_minio.py b/files_storage/test_minio.py
index ae603d810..08a63901a 100644
--- a/files_storage/test_minio.py
+++ b/files_storage/test_minio.py
@@ -1,6 +1,6 @@
# Create your tests here.
import json
-from unittest.mock import Mock, patch
+from unittest.mock import ANY, patch
from django.test import TestCase
from minio.error import S3Error
@@ -90,6 +90,21 @@ def test_get_uri(self, mock_presigned_get_object):
"app_name",
)
+ def test_get_uri_with_public_base_url(self):
+ minio_storage = MinioStorage(
+ minio_host="s3.wasabisys.com",
+ minio_access_key="minio_access_key",
+ minio_secret_key="minio_secret_key",
+ bucket_root="scielo",
+ location="sa-east-1",
+ write_prefix="upload",
+ public_base_url="https://minio.scielo.br/upload",
+ )
+
+ uri = minio_storage.get_uri("journal/article.xml")
+
+ self.assertEqual("https://minio.scielo.br/upload/journal/article.xml", uri)
+
@patch("files_storage.minio.MinioStorage.fput")
def test_register(self, mock_fput):
metadata = self.minio_storage.register(
@@ -120,6 +135,35 @@ def test__fput_object(self, mock_client_fput_object, mock_get_uri):
"subdir1/subdir2/filename.xml",
)
+ @patch("files_storage.minio.MinioStorage.get_uri")
+ @patch("files_storage.minio.Minio.fput_object")
+ def test__fput_object_uses_write_prefix(self, mock_client_fput_object, mock_get_uri):
+ minio_storage = MinioStorage(
+ minio_host="s3.wasabisys.com",
+ minio_access_key="minio_access_key",
+ minio_secret_key="minio_secret_key",
+ bucket_root="scielo",
+ location="sa-east-1",
+ write_prefix="upload",
+ public_base_url="https://minio.scielo.br/upload",
+ )
+
+ minio_storage._fput_object(
+ "/root/folder1/folder2/filename.xml",
+ "subdir1/subdir2/filename.xml",
+ mimetype="mimetype_informado",
+ )
+
+ mock_client_fput_object.assert_called_with(
+ "scielo",
+ object_name="upload/subdir1/subdir2/filename.xml",
+ file_path="/root/folder1/folder2/filename.xml",
+ content_type="mimetype_informado",
+ )
+ mock_get_uri.assert_called_with(
+ "subdir1/subdir2/filename.xml",
+ )
+
@patch("files_storage.minio.MinioStorage.get_uri")
@patch("files_storage.minio.Minio.fput_object")
def test__fput_object_raises_exception(
@@ -212,25 +256,23 @@ def test_remove(self, mock_remove):
def test_fput_content(self, mock_fput_object):
mock_fput_object.return_value = "uri"
uri = self.minio_storage.fput_content(
- content="",
+ content=b"",
mimetype="text/xml",
object_name="object_name",
)
self.assertEqual("uri", uri)
- @patch("files_storage.minio.MinioStorage._create_tmp_file")
@patch("files_storage.minio.MinioStorage._fput_object")
def test_fput_content_calls_fput_object(
- self, mock_fput_object, mock_create_tmp_file
+ self, mock_fput_object
):
- mock_create_tmp_file.return_value = "/tmp/file.xml"
uri = self.minio_storage.fput_content(
- content="",
+ content=b"",
mimetype="text/xml",
object_name="object_name",
)
mock_fput_object.assert_called_with(
- file_path="/tmp/file.xml",
+ file_path=ANY,
object_name="object_name",
mimetype="text/xml",
)
diff --git a/files_storage/wagtail_hooks.py b/files_storage/wagtail_hooks.py
index bfd68bd44..38209df8c 100644
--- a/files_storage/wagtail_hooks.py
+++ b/files_storage/wagtail_hooks.py
@@ -18,10 +18,13 @@ class MinioConfigurationViewSet(CommonControlFieldViewSet):
"name",
"host",
"bucket_root",
+ "write_prefix",
+ "public_base_url",
)
search_fields = (
"name",
"host",
"bucket_root",
+ "write_prefix",
+ "public_base_url",
)
-