This is the second day of my participation in the August More text Challenge. For details, see:August is more challenging

The file upload function is often used in daily business writing. This article describes how to upload a file in file mode on the Eggjs Multipart module. The file md5 is used to check whether the file exists and the file is not generated repeatedly on the server. Suitable for profile picture and other small file upload.

The uploaded resources are stored in the static resource directory configured by the server. Nginx resolves the static resources to facilitate the whole package of various cloud OSS to be uploaded. The code returns resources without domain names by default. You can configure static resource domain name concatenation on the front end, or concatenate domain names in the Sequelize collector.

I. File upload process

  1. The framework will put the submitted files in the configurationtmpdirDirectory is created as a temporary file.
  2. And the basic information of the file and temporary file absolute path back, we use the frame returned temporary file file absolute path can passfsOperation file.
  3. Save the file to the configureduserConfig.file.diskDirectory. Through the nginxdiskThe directory resolves to a static resource domain name, and the uploaded file can be accessed.
// Obtain an example file upload request
 const files = this.ctx.request.files;
 console.log(files);
// The result prints the file information in the request
[{
  field: 'file'.filename: '2.png'.encoding: '7bit'.mime: 'image/png'.fieldname: 'file'.transferEncoding: '7bit'.mimeType: 'image/png'.filepath: '/myproject/tmp/egg-base-api/2021/07/27/15/3a809821-023a-4cbd-98ad-40787ac516d8.png'
}]
Copy the code

2. Configuration files

Modify the multipart configuration:

  • Mode File upload mode. The default file upload mode is STREAM.

  • Tmpdir Temporary directory path. You are advised to set this parameter to facilitate the clearing of uploaded temporary files.

  • FileExtensions uploadsupported fileExtensions. Using whitelist overrides the framework’s default whitelist.

  • CleanSchedule automatically cleans the configuration of temporary files. By default, the framework cleans temporary files before today at 4:30 am every day. You can configure cron expressions.

Added user-defined configuration:

  • userConfig.file.diskTo configure the directory for storing uploaded resources on the server, you need to give node write permission.
// config/config.default.js
const path = require('path');
module.exports = appInfo= > {
  const config = exports = {};
  // ...
  config.multipart = {
    // File upload mode: adopt temporary file mode.
    mode: 'file'.// Configure the directory of temporary files and change it to TMP in the root directory of the project for convenient management
    tmpdir: path.join(__dirname, '.. '.'tmp', appInfo.name),
    // Whitelists of upload types are allowed
    fileExtensions: [ '.jpg'.'.jpeg'.'.png'.'.gif'.'.bmp'.'.webp',].// File size limit
    fileSize: '20mb'.cleanSchedule: { cron: '0 30 4 * * *',}};// Customize the configuration
  const userConfig = {
    file: {
      // The path where your static resources are stored
      disk: '/home/www/static/upload',}}; };Copy the code

Data table structure and model

  • Table structure

    Path Relative path of a file, which can be combined with a static resource domain name to form a complete accessible URL.

    Name The original name of the file;

    Ext file suffix;

    Size File size;

    Md5 Indicates the hash value of a file, which is used to determine the uniqueness of the file.

CREATE TABLE `file` (
  `id` int NOT NULL AUTO_INCREMENT,
  `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT 'File path',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT 'Original file name',
  `ext` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT 'File suffix',
  `size` int NOT NULL DEFAULT '0' COMMENT 'File size',
  `md5` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ' ' COMMENT 'file md5',
  `create_time` datetime DEFAULT NULL COMMENT 'Creation time',
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `delete_time` datetime DEFAULT NULL COMMENT 'Delete logical delete'.PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
Copy the code
  • model

    The model uses logical deletes and retrieves. Refer to the previous article.

// app/model/file.js
const dayjs = require('dayjs');
module.exports = app= > {
  const { STRING, INTEGER, DATE } = app.Sequelize;
  return app.model.define('file', {
    id: { type: INTEGER, primaryKey: true.autoIncrement: true,},path: STRING,
    name: STRING,
    ext: STRING,
    size: STRING,
    md5: STRING,
    create_time: {
      type: DATE,
      get () {
        const time = this.getDataValue('create_time');
        return time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : null; }},update_time: {
      type: DATE,
      get () { // Obtain the data when the format time
        const time = this.getDataValue('update_time');
        return time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : null; }},delete_time: DATE,
  }, {
    freezeTableName: false.tableName: 'file'.underscored: false.paranoid: true.timestamps: true.// Enable automatic time to use logical delete
    createdAt: false.updatedAt: false.deletedAt: 'delete_time'.// Delete the field logically
  });
};
Copy the code

Routing, controllers, and services

  • router
// app/router.js
module.exports = app= > {
  const { router, controller } = app;
  router.post('/admin/file', controller.file.upload);
};
Copy the code
  • controller
// app/controller/file.js
const BaseController = require('./base');
class FileController extends BaseController {
  async upload () {
    / / if you use multiple file upload can be introduced to false (CTX) service) file. The uploadFile (false))
    const data = await this.ctx.service.file.uploadFile(true);
    this.success(data); }}module.exports = FileController;
Copy the code
  • service

    The specific logic is as follows, which can also be implemented in other languages.

    1. Get all the files in the request, loop.

    2. Basic file information, including file size, MD5, and fileBuffer, was obtained.

    3. Run the MD5 command to check whether the file exists in the database.

    4. If it exists, file information is returned directly. No file is saved to a static resource directory, file information is stored in the database, and file information is returned.

// app/service/file.js
const Service = require('egg').Service;
const crypto = require('crypto');
const UUID = require('uuid').v4;
const dayjs = require('dayjs');
const fs = require('fs');
const path = require('path');
// Failed to upload the custom file
const FileException = require('.. /exception/file');

class FileService extends Service {
  // File upload method: single:true Single file single:false multi-file
  async uploadFile (single = false) {
    if (!this.ctx.request.files || this.ctx.request.files.length === 0) {
      throw new FileException('Please select file');
    }
    const rets = [];
    // Get temporary file loop operation
    const files = this.ctx.request.files;
    for (let i = 0; i < files.length; i++) {
      if (single && i > 0) { // If only the first file is processed for single file upload, then the loop is closed
        break;
      }
      const file = files[i];
      try {
        // Get the basic information about the file: size, hash, fileBuffer
        const { size, md5, data } = await this.checkFileInfo(file);
        // Check whether the file exists in the database. If it exists, return the file information directly, without saving the file.
        const exists = await this.checkFileExists(md5);
        if (exists) {
          rets.push({ key: i, ... exists }); }else { // If not, save the file to the server and create a record in the database.
          // Save the file to the hard disk
          const res = await this.putFile(data, file.filename);
          // Save the file to the database
          await this.app.model.File.create({
            path: res.path,
            name: file.filename,
            ext: res.ext,
            size,
            md5,
            create_time: new Date()}); rets.push({key: i,
            path: res.path,
            url: '/'+ res.path, }); }}catch (e) {
        throw new FileException('File upload failed'); }}return single ? rets[0] : rets;
  }
  
  // Get the file information
  async checkFileInfo (file) {
    return new Promise((resolve, reject) = > {
      const fsHash = crypto.createHash('md5');
      fs.readFile(file.filepath, (err, data) = > { // Read the file
        if (err) {
          reject(err);
        } else {
          // Obtain the file md5
          const md5 = fsHash.update(data).digest('hex');
          // Get the file size
          constsize = data.length; resolve({ size, md5, data }); }}); }); }// Save the file to the hard disk
  async putFile (flieBuffer, filename) {
    return new Promise((resolve, reject) = > {
      const { dir, local } = this.createUploadPath();
      const ext = this.getFileExt(filename);
      // Save the file name, use the UUID
      const targetName = UUID() + '. ' + ext;
      / / write file
      fs.writeFile(dir + '/' + targetName, flieBuffer, err= > {
        if (err) {
          reject(err);
        } else {
          resolve({
            path: local + '/'+ targetName, ext, }); }}); }); }// The database uses the MD5 of the file to find whether it exists
  async checkFileExists (md5) {
    const exists = await this.app.model.File.findOne({ where: { md5 } });
    // Return the file information if it exists
    if(exists ! = =null) {
      return {
        id: exists.id,
        path: exists.path,
        url: '/' + exists.path,
      };
    }
    return null;
  }
  
  // Obtain without taking. The suffix
  getFileExt (filename) {
    const ext = path.extname(filename);
    if (this.app._.startsWith(ext, '. ')) {
      return ext.substr(1);
    }
    return ext;
  }
  
  // Create a directory for saving files. Default is one directory per day
  createUploadPath () {
    // Obtain the file location in the configuration
    const disk = this.app.config.file.disk;
    const local = dayjs().format('YYYYMMDD');
    const dir = disk + '/' + local;
    if(! fs.existsSync(dir)) {// Create a directory if it does not exist
      fs.mkdirSync(dir);
    }
    return{ dir, local }; }}module.exports = FileService;
Copy the code

4. Upload test

  • Service. UploadFile (true)

  • Upload multiple files (service.uploadFile(false))