使用Django-fobi的表单将上传的文件保存到Minio中
首先
出於必要的迫切需求,我正在使用django-fobi作為表單設計工具。
由于回答截止日期接近,访问表单的负荷可能会集中,因此我们决定将ORM(对象关系映射)的连接目标设定为MySQL集群,同时使用兼容AWS S3的Minio对象存储来保存BLOB文件,以实现负载均衡。
对于MySQL集群而言,几乎没有问题,仅需更改连接信息即可完成。
关于Minio,如果我们尝试切换所有的/media和/static使用情况,会变得非常麻烦。但是,我们可以决定将/static用于每个Docker容器内保存的信息,而放弃使用/media。这样,我们只需要针对每个应用程序进行适配就可以了。
如果想要将默认存储位置从/media和/static更改为Minio,可以考虑使用诸如django-minio-backend等库,但根据我稍微尝试的印象,根据每个应用程序的使用风格可能需要考虑一些问题,因此可能需要花费一些时间。
我打算只研究django-fobi,专注于将上传的文件保存到Minio的方法。
请查阅相关资料。
-
- https://django-fobi.readthedocs.io/en/latest/
- https://github.com/minio/minio-py/tree/release/examples
考虑使用django-fobi来使用Minio的方案
有几种可能的方法。
-
- 使用django的minio兼容库。
-
- 开发适用于django-fobi的handler插件(修改db_store handler)。
- 开发适用于django-fobi的element插件(修改File element)。
我们正在考虑各自的事项。
使用适用于Django的minio库。
最初我尝试了适用于Django的MinIO库,但是它并不像我期望的那样容易使用。
在Django中,对/media的访问已经有了一定的标准化,但并不强制使用Storage类(django.core.files.storage.Storage),并且似乎也可以相对容易地用FileSystemStorage类替换,虽然变更内容不大,但影响范围较大,所以需要进行大量的验证工作,感觉有些力不从心。
然而,如果人力充足、预算和时间充裕的正式项目,考虑使用这种方法是明智的。
考虑开发Handler插件。
我意识到开发第二个handler插件的方法在目标方面是错误的一种途径。
Django-fobi的handler插件的操作从接收每个元素处理完成的结果作为对象开始。因此,上传的文件已经被放置在/media目录下。
考虑开发Element插件
最终我们决定复制整个venv/*/lib/python3.9/site-packages/fobi/contrib/plugins/form_elements/fields/file/目录,并创建具备所需功能的minio_form元素。
使用默认的文件元素保存文件数据的方法。
...
from fobi.helpers import handle_uploaded_file
...
__title__ = 'fobi.contrib.plugins.form_elements.fields.file.base'
__author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
__copyright__ = '2014-2019 Artur Barseghyan'
__license__ = 'GPL 2.0/LGPL 2.1'
__all__ = ('FileInputPlugin',)
class FileInputPlugin(FormFieldPlugin):
"""File field plugin."""
...
def prepare_plugin_form_data(self, cleaned_data):
"""Prepare plugin form data.
...
"""
# Get the file path
file_path = cleaned_data.get(self.data.name, None)
if file_path:
# Handle the upload
saved_file = handle_uploaded_file(FILES_UPLOAD_DIR, file_path)
...
最终文件存储操作将由handle_uploaded_file()函数完成。然而,该代码的实际内容保存在fobi/helpers.py文件中,因此还需要复制该helpers.py文件。
这一次主要是要修改 helpers.py 文件。
开发minio_file元素
首先,将file元素的文件复制到项目内的任意位置。
$ mkdir -p myapp/fobi_plugins/elements
$ cp -r venv/*/lib/python3.9/site-packages/fobi/contrib/plugins/form_elements/fields/file myapp/fobi_plugins/elements/minio_file
$ cp venv/*/lib/python3.9/site-packages/fobi/helpers.py myapp/fobi_plugins/elements/minio_file/
当复制完成时,整个目录结构如下所示。
.
├── Dockerfile
├── Makefile
├── manage.py
├── myapp
│ ├── asgi.py
...
│ ├── fobi_plugins
│ │ └── elements
│ │ └── minio_file
│ │ ├── apps.py
│ │ ├── base.py
│ │ ├── conf.py
│ │ ├── defaults.py
│ │ ├── fields.py
│ │ ├── fobi_form_elements.py
│ │ ├── forms.py
│ │ ├── helpers.py
│ │ ├── __init__.py
│ │ └── settings.py
在一定程度上,我会先进行一些工作,然后逐步修改myapp/settings.py。
每个文件的基本更改
对于除了已完成更改的 base.py 和 helpers.py 之外的文件,差异如下:
只是根据更改做了名称和路径的调整。
diff -ur venv/myapp/lib/python3.9/site-packages/fobi/contrib/plugins/form_elements/fields/file/./apps.py myapp/fobi_plugins/elements/minio_file/./apps.py
--- venv/myapp/lib/python3.9/site-packages/fobi/contrib/plugins/form_elements/fields/file/./apps.py 2022-08-05 21:50:03.485391378 +0900
+++ myapp/fobi_plugins/elements/minio_file/./apps.py 2022-08-17 21:59:26.193981052 +0900
@@ -1,14 +1,13 @@
-class Config(AppConfig):
- name = 'fobi.contrib.plugins.form_elements.fields.file'
- label = 'fobi_contrib_plugins_form_elements_fields_file'
+class MinioFileConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'myapp.fobi_plugins.elements.minio_file'
diff -ur venv/myapp/lib/python3.9/site-packages/fobi/contrib/plugins/form_elements/fields/file/./fobi_form_elements.py myapp/fobi_plugins/elements/minio_file/./fobi_form_elements.py
--- venv/myapp/lib/python3.9/site-packages/fobi/contrib/plugins/form_elements/fields/file/./fobi_form_elements.py 2022-08-05 21:50:03.485391378 +0900
+++ myapp/fobi_plugins/elements/minio_file/./fobi_form_elements.py 2022-08-17 23:26:28.863106266 +0900
@@ -1,14 +1,16 @@
-from .base import FileInputPlugin
-form_element_plugin_registry.register(FileInputPlugin)
+from .base import MinioInputPlugin
+form_element_plugin_registry.register(MinioInputPlugin)
diff -ur venv/myapp/lib/python3.9/site-packages/fobi/contrib/plugins/form_elements/fields/file/./__init__.py myapp/fobi_plugins/elements/minio_file/./__init__.py
--- venv/myapp/lib/python3.9/site-packages/fobi/contrib/plugins/form_elements/fields/file/./__init__.py 2022-08-05 21:50:03.485391378 +0900
+++ myapp/fobi_plugins/elements/minio_file/./__init__.py 2022-08-17 23:19:43.187465122 +0900
@@ -1,10 +1,11 @@
-UID = 'file'
+default_app_config = 'myapp.fobi_plugins.elements.minio_file.apps.MinioFileConfig'
+
+UID = 'minio_file'
接下来,我们要更改myapp/settings.py文件。
INSTALLED_APPS = [
...
'myapp.fobi_plugins.elements.minio_file',
...
]
由于已设置default_app_config,因此未直接指定MinioFileConfig。
我們要先定義連接到Minio所需的變數。
MINIO_ENDPOINT = env.str('MINIO_ENDPOINT', "")
MINIO_ACCESS_KEY = env.str('MINIO_ACCESS_KEY', "")
MINIO_SECRET_KEY = env.str('MINIO_SECRET_KEY', "")
MINIO_FOBIFILE_BUCKET = env.str('MINIO_FOBIFILE_BUCKET', "")
MINIO_SECURE_CONN = env.bool("MINIO_SECURE_CONN", False)
這個工具用於提供名字所示的連接所需的ENDPOINT資訊,以”IP:Port”的格式,或提供連接所需的Token資訊。請查看程式碼,立即就能明白如何使用。
minio_file/base.py 的修改
几乎没有任何变化,唯一需要做的是确保导入正确的helpers.py,并且不需要指定保存位置,只需提供适当的前缀即可。
--- venv/myapp/lib/python3.9/site-packages/fobi/contrib/plugins/form_elements/fields/file/base.py 2022-08-05 21:50:03.485391378 +0900
+++ myapp/fobi_plugins/elements/minio_file/base.py 2022-08-22 12:34:32.277474578 +0900
@@ -8,26 +15,19 @@
from django.utils.translation import gettext_lazy as _
from fobi.base import FormFieldPlugin
-from fobi.helpers import handle_uploaded_file
+from .helpers import handle_uploaded_file
from . import UID
from .fields import AllowedExtensionsFileField as FileField
from .forms import FileInputForm
from .settings import FILES_UPLOAD_DIR
-class FileInputPlugin(FormFieldPlugin):
+class MinioInputPlugin(FormFieldPlugin):
"""File field plugin."""
uid = UID
- name = _("File")
- group = _("Fields")
+ name = _("Minio File")
+ group = _("Custom")
form = FileInputForm
def get_form_field_instances(self, request=None, form_entry=None,
@@ -60,20 +63,23 @@
:return:
"""
# Get the file path
- file_path = cleaned_data.get(self.data.name, None)
- if file_path:
+ file_obj = cleaned_data.get(self.data.name, None)
+ if file_obj:
# Handle the upload
- saved_file = handle_uploaded_file(FILES_UPLOAD_DIR, file_path)
+ saved_file = handle_uploaded_file(str(self.request.user), file_obj)
# Overwrite ``cleaned_data`` of the ``form`` with path to moved
# file.
file_relative_url = saved_file.replace(os.path.sep, '/')
cleaned_data[self.data.name] = "{0}{1}".format(
- settings.MEDIA_URL,
- file_relative_url
+ "minio:///",
+ file_relative_url,
)
# It's critically important to return the ``form`` with updated
# ``cleaned_data``
return cleaned_data
+ pass
def submit_plugin_form_data(self,
form_entry,
创建 minio_file/helpers.py
base.py中调用的handle_uploaded_file()的内容如下:
## Original Information
## __title__ = 'fobi.helpers'
## __author__ = 'Artur Barseghyan <artur.barseghyan@gmail.com>'
## __copyright__ = '2014-2019 Artur Barseghyan'
## __license__ = 'GPL 2.0/LGPL 2.1'
from django.conf import settings
from minio import Minio
import io
import os
import uuid
import hashlib
## for the 'image_file' instance check
from django.core.files.base import File
def handle_uploaded_file(identifier, image_file):
client = Minio(settings.MINIO_ENDPOINT,
access_key=settings.MINIO_ACCESS_KEY,
secret_key=settings.MINIO_SECRET_KEY,
secure=settings.MINIO_SECURE_CONN,)
if isinstance(image_file, File):
destination_path = os.path.join("/", str(identifier), uuid.uuid4().hex, image_file.name)
try:
image_file_data = image_file.read()
sha256_msg = hashlib.sha256(image_file_data).hexdigest()
client.put_object(settings.MINIO_FOBIFILE_BUCKET, destination_path,
io.BytesIO(image_file_data), length=len(image_file_data),
metadata={ 'sha256_digest': sha256_msg }, )
except ResponseError as err:
print("response error")
print(err)
pass
return destination_path
else:
print("image_file is not instance of File class")
pass
return image_file
这段代码并没有考虑效率,直接处理了复制到内存中的数据以获取SHA256哈希(image_file.read()),因此根据上传文件的大小,可能会。
【閒話休題】在Django中的文件上傳最大尺寸。
使用Django应用程序时,如果要将反向代理服务器放置在外部网络和边界之间,还会出现其他考虑因素。
在Django应用程序中的设置
在Django应用程序中,可以在settings.py文件中指定上传文件的大小。
## 最大ファイルサイズを256MiB(256*1024*1024)に設定
DATA_UPLOAD_MAX_MEMORY_SIZE = 268435456
在Nginx上的配置
可以通过 client_max_body_size 参数来进行指定。将该值设置为0将意味着无限制,但是无限制不仅仅会束缚工作进程的处理时间,还会消耗内存,所以不建议使用。
如果不是专用于Django应用程序,将默认值设置为约1m,并根据需要进行扩展。
server {
client_max_body_size 1m;
location /myapp/ {
client_max_body_size 256m;
}
}
如果通过Kubernetes(k8s)的ingress-nginx进行访问的话
如果前端使用nginx能直接编辑配置文件是很好的,但如果后端是通过ingress(k8s)环境运行,则无法直接编辑配置文件。
在定义Ingress对象的YAML文件的注释中添加以下设置。
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
...
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "256m"
最后
我在2005年左右有过处理BPEL的经验,虽然工作流技术在需求上有一定的市场,但也是一项非常困难的技术领域。
由于账票是在商业与他人关系中建立的,所以无论在任何商业环境中,都必然存在着需求。
由于使用方法受到文化影响很大,实施一个普遍可用的框架变得很困难。
因此,具备通用性的报表解决方案非常复杂,并且除了噩梦之外没有其他什么。
Django-fobi是一个平台,它没有工作流,仅仅是用于设计表单并保存提交的表单。但它设计的表单和保存的数据都可以通过ORM进行访问,这一点非常方便。
尽管ORM会自动生成各种考虑周全的SQL语句,但一旦出现问题,需要检查复杂的SQL语句才能进行调试。与Ruby on Rails等类似,这让我在Docker/Kubernetes环境中运行有些犹豫。
如果使用方法相当简单的话,我认为django-fobi是可以推荐的。
上述内容