127. 文件上传与管理(上)

   对于一个CMS而言,文件上传不难,文件的CURD也不难,难点是碎片处理:多篇信息可以使用到同一个文件(如图片),同一篇信息也可以使用到多个文件,当删除一篇信息的时候是否同时删除引用到的文件呢?如果删除,那么在该文件有其他信息引用的情况下,其他信息将出现引用缺失,如果不删除,那么在没有其他信息引用的情况下,文件将永存系统,形成不再被使用的僵尸文件,白白浪费了宝贵的储存空间,由此可见,记录文件信息和文件使用情况追踪是必须的,但使用追踪却并不那么简单,这涉及到进入系统的所有信息,要严格追踪有时甚至是不可能的,比如在提交的信息中,用户可以手动插入一个已存文件的链接,这常见于富文本编辑器,要做到严格追踪就必须要分析提交的所有字符,这在性能上是不可接受的,批量删除时性能消耗更是巨大。规模越大、历史越悠久的系统伴随的碎片问题也越严重,那么如何避免呢?这要求在开发上使用地需有意识的主动去记录维护文件使用信息,在使用上必须规范以满足文件追踪条件。

drupal中,文件上传与管理主要由核心模块“file”负责,该模块提出了一个“ManagedFile”的概念,这里云客将其翻译为“托管文件”,是什么意思呢?即通过托管文件API上传的每一个文件都会被跟踪管理,系统为每一个托管文件对应的建立保存一个文件实体来记录该文件本身的信息,之后不必再扫描文件系统,且建立了文件的使用追踪系统,使用地需要主动维护使用信息,有使用记录的文件属于永久文件,系统不会主动删除,当没有任何使用记录时,文件处于临时状态,会被系统自动任务删除。如果模块没有采用托管文件API,那么文件的管理与上传需要自行负责。

接下来我们从文件实体开始讲解整个托管文件的实现

 

文件实体:

这是一个内容实体,支持多语言,但不支持版本化、bundle,用于一对一的保存和托管文件相关的信息,其并不操作文件,换句话说该实体并不具备文件的新建、复制、移动、重命名功能,仅是对文件信息的记录(删除操作除外,但仅在实体被删除时会一并删除使用记录和文件本身),可到以下页面查看已保存的托管文件:
  
/admin/content/files
文件实体类:\Drupal\file\Entity\File
继承自内容实体基类:\Drupal\Core\Entity\ContentEntityBase
实现接口:\Drupal\file\FileInterface

该接口继承以下接口:
  内容实体接口:\Drupal\Core\Entity\ContentEntityInterface
  可修改实体接口:\Drupal\Core\Entity\EntityChangedInterface
  有所有者实体接口:\Drupal\user\EntityOwnerInterface

主要方法说明如下(更多请见本系列实体相关内容):

public function getFilename()
public function setFilename($filename)

获取、设置文件名,不带路径,但带扩展名

 

public function getFileUri()
public function setFileUri($uri)

获取、设置文件的URI,注意文件的URIURL不一样:

URI是文件在文件系统中的路径,包括流包装器格式的路径,供文件操作函数使用

URL是外部访问文件的地址(网址),用于下载或显示,URL中的文件名可能是经过编码的

 

public function createFileUrl($relative = TRUE)

返回文件的URL(不是uri),参数是一个布尔值,表示是否返回相对地址,换句话说true将不带域名,注意不要使用toUrlurl方法

 

public function getMimeType()
public function setMimeType($mime)

获取、设置文件的MIME类型,详见MIME类型猜测器

 

public function getSize()
public function setSize($size)

获取、设置文件的尺寸,单位字节

 

public function getCreatedTime()

获取创建时间

 

public function isPermanent()
public function isTemporary()
public function setPermanent()
public function setTemporary()

获取或设置文件的状态,只有两种:永久(1)或临时(0

 

public static function preDelete(EntityStorageInterface $storage, array $entities)

文件实体删除前方法,该方法将联动删除文件和文件的使用记录,但不会影响到使用文件的地方,这将导致在使用的地方出现无效链接

bug提示:该方法在清理使用记录时存在bug,更正如下:

\Drupal::service('file.usage')->delete($entity, $module, null, null, 0);

 

文件实体储存处理器:

和普通实体储存处理器一样,但专门提供了计算文件存储空间的方法:

        $fileStorage=\Drupal::entityTypeManager()->getStorage('file');
        $size=$fileStorage->spaceUsed($uid = NULL, $status = FILE_STATUS_PERMANENT);

当提供了UID时,将仅计算属于该UID的文件尺寸之和,否则计算所有文件的尺寸,这里的尺寸是依据数据库记录而来,并非实时的扫描文件;状态参数指示计算临时还是永久的文件

 

创建一个文件实体示例:

  $values = [
    'uid' => $user->id(),
    'status' => 0,
    'filename' => $file_info->getClientOriginalName(),
    'uri' => $file_info->getRealPath(),
    'filesize' => $file_info->getSize(),
  ];
  $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']);
  $file = \Drupal\file\Entity\File::create($values);

 

“文件使用”服务:

用于记录文件的使用情况:

服务idfile.usage

类:Drupal\file\FileUsage\DatabaseFileUsageBackend

获取方式:\Drupal::service('file.usage')

该服务仅记录文件的使用情况,并不主动追踪,换句话说,文件的使用记录需要其他组件在使用地主动调用该服务以添加、删除或更新,严格说,这是一个记录器而不是追踪分析器,文件使用情况记录在数据库表“file_usage”中,该表在文件模块安装时建立(见file.install 文件中的file_schema方法),有如下字段:

fid:文件实体的id,整数
module:使用该文件的模块名
type:使用该文件的对象类型名,如node、media等(通常是实体类型名)
id:使用该文件的对象的首选主键,如节点实体对象的id(通常是实体id)
count:该文件被以上id对象使用的次数

假设是某个节点使用了文件,那么节点对象可以使用多次,因为该节点可能是可版本化的,每个版本都会增加相应次数,如果同时是可翻译的,那么会增加更多。

该服务方法介绍如下:

public function add(FileInterface $file, $module, $type, $id, $count = 1)

添加文件使用次数,无返回值,参数依次为:文件实体、模块名、对象类型、对象ID、使用次数,如果尚无记录将插入,如果已有记录将累加,该方法还将同时把临时状态的文件实体转变为永久状态并保存,这样文件就不会被自动任务删除了,由此也可知使用次数不为0那么一定是永久状态的文件。

public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $count = 1)

删除或减少文件使用次数,无返回值,参数依次为文件实体、模块名、对象类型、对象ID、使用次数,其中类型和id是可选的,没有传递相当于所有类型和id,如果需要传递就应该都有值,否则相当于没有传递;使用次数如果设置为0,那么将删除所有使用记录,以下配置项影响该方法的行为:

$configFactory->get('file.settings')->get('make_unused_managed_files_temporary')

配置含义为“当一个文件被使用次数减为0时,是否将其标记为临时文件”,是一个布尔值,默认为false,如果为true则表示:在文件没有使用次数时,将把文件的永久状态改为临时状态,注意:这将导致在系统运行自动任务时删除该文件,因此默认为false,由于该默认值不会自动删除文件,因此为了节约空间,我们可以定时删除使用为0的文件,由此也可知使用次数为0也可能是永久状态的文件。

public function listUsage(FileInterface $file)

列出文件的使用情况信息,参数为文件实体,返回一个多维数组,第一级键名为模块名,第二级键名为类型,第三级键名为对象id,值为使用次数,如果没有被使用过,将返回空数组

 

Drupal文件上传:

系统默认提供了两个元素类型插件来进行文件上传,如下:

通用文件上传:

插件IDfile

类:Drupal\Core\Render\Element\File

示例:

        $form['file_a'] = array(
            '#type'        => 'file',
            '#multiple'    => TRUE,  //单个控件是否允许上传多个文件
            '#title'       => '文件A提交',
            '#description' => "基础文件控件,可选择单值多值",
        );

该插件是系统最基础的文件上传处理,仅准备一个文件上传表单,提交和验证需要开发者自行处理,可算是最底层的实现,如果不采用托管文件API则可以使用该类型上传。

 

托管文件上传:

插件IDmanaged_file

类:Drupal\file\Element\ManagedFile

这是系统最常用的文件上传方式,也是本主题后续的主要内容,封装了托管文件API,是基于前一个“file”元素类型的更加高级的实现。

 

托管文件上传:

元素插件IDmanaged_file

类:Drupal\file\Element\ManagedFile

概述:

托管文件上传基于AJAX API,文件默认通过AJAX上传和保存,上传时表单尚未整体提交,AJAX上传后系统为每个文件建立一个临时状态的文件实体并保存,再通过AJAX返回已上传的文件对应的实体id,最后表单整体提交时,提交处理器得到的是文件实体的id,此时可以添加使用记录,如果提交处理器不做处理,那么上传的这些文件将继续处于临时状态,直到被自动任务删除。托管文件API允许我们指定可上传的文件类型、大小、保存位置等。

 

使用示例:

为了有最直观的理解,先看一个完整的表单示例,读者可运行一下该示例:

<?php
/**
 * 演示托管文件上传
 */

namespace Drupal\yunke_help\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class YunkeForm extends FormBase
{
    public function getFormId()
    {
        return 'yunke_help_form';
    }

    public function buildForm(array $form, FormStateInterface $form_state)
    {
        $form['#title'] = '托管文件上传';
        $form['file']['yunke'] = array(
            '#type'               => 'managed_file',
            '#multiple'           => true,
            '#upload_location'    => 'public://yunke_managed_file/2019/',
            '#upload_validators'  => ['file_validate_extensions'  => ['jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'],
                //为空数组将允许全部扩展
                                      'file_validate_name_length' => [],
            ],
            '#progress_indicator' => 'throbber',
            '#progress_message'   => '正在上传,请稍后...',
            //'#extended'           => true,
            //'#default_value'     => ['fids' => [1]],
            //'#extended'          => false,
            //'#default_value'     => [1,17, 18],
            //'#value'             => ['fids' => [1, 17, 18]],
            '#title'              => '托管文件提交',
            '#description'        => "可单值上传或多值上传",
        );
        $form['#attributes']['target'] = "_blank";
        $form['actions']['#type'] = 'actions';
        $form['actions']['submit'] = array(
            '#type'        => 'submit',
            '#value'       => $this->t('Submit'),
            '#button_type' => 'primary',
        );
        $form['actions']['reset'] = array(
            '#type'       => 'html_tag',
            '#tag'        => 'input',
            '#attributes' => ['type' => 'reset', 'class' => ['button'], 'value' => $this->t('Reset'),],
        );
        return $form;
    }

    public function validateForm(array & $form, FormStateInterface $form_state)
    {
        //演示起见,无需验证
    }

    public function submitForm(array & $form, FormStateInterface $form_state)
    {
        $form_state->cleanValues();
        if ($form['file']['yunke']['#extended']) {
            $fids = $form_state->getValue('yunke')['fids'];
        } else {
            $fids = $form_state->getValue('yunke');
        }
        //保存文件实体id(fids),并在\Drupal::service('file.usage')中添加使用记录
        //不添加记录文件将仍处于临时状态,会被自动任务删除
        print_r($fids);
        print_r($form_state->getValues());
        print_r($_FILES);
        die;
    }
}

 

托管文件API选项:

各种选项含义如下:

#progress_indicator

进度条类型标识,默认为throbber,将赋值给AJAX设置数组中进度条设置的type,详见本系列ajax主题

#progress_message

进度条提示消息,默认为NULL,将赋值给AJAX设置数组中进度条设置的message项,详见本系列ajax主题

#upload_validators

上传验证器回调数组,每一个元素就是一个回调,可指定多个,键名为回调函数名,键值为一个数组,该数组的元素将和文件实体对象一起作为回调参数,文件实体对象作为第一个参数。

该项如无指定将使用以下默认设置:

'#upload_validators' => [
  'file_validate_extensions'  => ['jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'],                                     
  'file_validate_name_length' => [],
  ],

第一个回调file_validate_extensions验证允许上传的文件扩展名,仅列出的扩展被允许(空格分隔),参数为null将允许所有扩展名('file_validate_extensions'  => [NULL],);

第二个回调file_validate_name_length被强制执行(不管有无设置),用于验证文件名长度,仅允许小于等于240个字符(含扩展名);

此外模块可以实现“file_validate钩子进行验证:

function hook_file_validate(Drupal\file\FileInterface $file)

验证时文件尚未从临时目录移动,文件实体也尚未保存,此时文件实体对象被附加了两个动态属性:“source”属性保存着文件字段名(前端name属性的二级键名),“destination”属性保存着将要被移动到的目标地址(字符串值),验证回调如通过可以返回空数组,否则返回由错误消息(t函数返回的翻译对象)构成的数组,所有回调返回的数组会被合并,只要有一条错误消息,那么文件就不会被移动,文件实体不会被保存。

详见函数:file_validate(FileInterface $file, $validators = [])

系统还默认提供了以下验证函数:

file_validate_size:文件尺寸限制
file_validate_is_image:限制为图片,且仅能是被允许类型的图片
file_validate_image_resolution:图片像素限制,仅在允许的范围,用于非图片时将直接通过

#upload_location

文件上传后保存的位置,必须是流包装器格式的路径,如:public://yunke_managed_file/2019/,不能为相对或绝对目录路径(试图用file://将不成功),不能直接使用占位符token,需要经过token服务处理后再赋值给该属性

#multiple

布尔值,指示文件上传控件是否为多值的(一个文件控件可上传多个文件),默认为false,如果为多值则name属性会加“[]”后缀

#extended

布尔值,默认为false,用于表明值取回方式及默认值的赋值方式,见默认值设置

#default_value

默认值是一个由文件实体id构成的数组(文件实体须真实存在),但赋值方式由“#extended”选项控制:

如果为true,应该这样赋值:

'#default_value'=> ['fids' => [1, 17, 18]],

如果为false,应该这样赋值:

'#default_value'=> [1, 17, 18],

如果是单值表单应该仅有一个元素,多值表单才能有多个元素

#value

大多数情况下不能直接设置值属性(而应采用默认值设置项),因为这导致值回调不运行,结果就是用户不能上传新文件,但如果本意就不需要接收新文件则可以设置,应该这样:

'#value'      => ['fids' => [1, 17, 18]],

这不受“#extended”选项影响,同样如果是单值表单应该仅有一个元素,多值表单才能有多个元素

#accept

可选值,默认为NULL,用于设置前端文件上传表单的accept属性值,是用逗号隔开的 MIME 类型列表,详见HTML 文件类型<input>标签的accept 属性,示例如:

<input type="file" name="yunke" accept="image/png, image/jpeg" />

在该示例中,浏览器遇到该设置将仅允许上传pngjpeg的图片文件,该设置仅用于前端验证,安全起见后端验证不可缺少。

#file_value_callbacks

可选设置,一个由回调构成的数组(在PHP7.0前,该处回调不能是数组类型,可以是函数名或静态方法),接收以下三个参数:

$element:本表单元素渲染数组
$input:用户输入,应该以引用接收
$form_state:表单状态对象

回调在表单流程的值回调处理中运行,仅在非默认值处理,且无上传文件或上传文件全部处理失败时运行,回调可以修改$input['fids']的值(一个文件实体id构成的索引数组),以提供已经存在的文件等。

 

补充:

由于篇幅太长,本主题分为上下两集,上集就到这里,在下集中将介绍前后端的实现原理,以及其他注意点

 

 

本书共136小节:

评论 (写第一个评论)