使用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的方案

有几种可能的方法。

    1. 使用django的minio兼容库。

 

    1. 开发适用于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是可以推荐的。

上述内容

广告
将在 10 秒后关闭
bannerAds