Строим блог на основе Node.js MVC фреймворка Areto

Модель загружаемого файла

Модель File обеспечивает загрузку файлов на сервер. Она реализована для поддержки асинхронной загрузки с форм. Загруженные файлы не используются напрямую, а лишь служат источником данных в других моделях.

В поведение timestamp отключите атрибут updatedAttr. Потому что однажды загруженный файл не может быть отредактирован, а только либо удален, либо использован для пересохранения в какой-либо модели.

Константа STORE_DIR формирует абсолютный путь до директории размещения файлов.

modules/admin/models/File.js

'use strict';
const Base = require('areto/db/ActiveRecord');
const path = require('path');

module.exports = class File extends Base {
  static getConstants () {
    return {
      TABLE: 'file',
      STORED_ATTRS: ['userId','originalName','filename','mime','extension','size','ip','createdAt'],
      RULES: [
        ['file', 'required'],
        ['file', 'file']
      ],
      BEHAVIORS: {
        timestamp: {
          Class: require('areto/behaviors/Timestamp'),
          updatedAttr: false
        }
      },
      STORE_DIR: path.join(__dirname, '../uploads/temp')
    };
  }     
};
module.exports.init(module);

const helper = require('areto/helpers/MainHelper');
const fs = require('fs');
const multer = require('multer');
const mkdirp = require('mkdirp');

Метод findExpired ищет все файлы старше указанного периода.

modules/admin/models/File.js

...
static findExpired (elapsedSeconds = 3600) {
  let expired = new Date;
  expired.setSeconds(expired.getSeconds() - elapsedSeconds);
  return this.find(['<', 'createdAt', expired]);
}
...

Название модели формируется из исходного имени файла и имени хранения.

modules/admin/models/File.js

...
getTitle () {
  return `${this.get('originalName')} (${this.get('filename')})`;
}
...

Метод isImage определяет является ли файл изображением.

modules/admin/models/File.js

...
isImage () {
  return this.get('mime').indexOf('image') === 0;
}
...

Метод getPath возвращает полный путь до сохраненного файла.

modules/admin/models/File.js

...
getPath () {
  return path.join(this.STORE_DIR, this.get('filename'));
}
...

Метод upload сохраняет файл на сервер. Для это используется npm-модуль multer.

modules/admin/models/File.js

...
upload (controller, cb) {
  let req = controller.req;
  multer({
    storage: multer.diskStorage({
      destination: this.generateStoreDir.bind(this),
      filename: this.generateFilename.bind(this)
    })
  }).single('file')(req, controller.res, err => {
    if (err) {
      cb(err);
    } else {
      this.populateFileStats(req.file, controller);
      this.set('file', this.getFileStats());
      this.save(cb);
    }
  });
}
...

Методы generateStoreDir и generateFilename создают директорию и генерируют имя, под которым файл будет сохранен на сервере.

modules/admin/models/File.js

...
generateStoreDir (req, file, cb) {
  mkdirp(this.STORE_DIR, err => {
    cb(err, this.STORE_DIR);
  });
}

generateFilename (req, file, cb) {
  cb(null, Date.now().toString() + helper.getRandom(11, 99));
}
...

Метод populateFileStats извлекает значения для атрибутов модели из параметров файла и окружения.

modules/admin/models/File.js

...
populateFileStats (file, controller) {
  this.set('userId', controller.user.getId());
  this.set('originalName', file.originalname);
  this.set('filename', file.filename);
  this.set('mime', file.mimetype);
  this.set('extension', path.extname(file.originalname).substring(1).toLowerCase());
  this.set('size', file.size);
  this.set('ip', controller.req.ip);
}
...

Метод getFileStats возвращает структуру необходимую для валидации загруженного файла.

modules/admin/models/File.js

...
getFileStats () {
  return {
    model: this,
    path: this.getPath(),
    size: this.get('size'),
    extension: this.get('extension'),
    mime: this.get('mime')
  };
}
...

Обработчик afterDelete вызывается после удаления модели и удаляет файл из файловой системы.

modules/admin/models/File.js

...
afterDelete (cb) {
  super.afterDelete(err => {
    err ? cb(err) : fs.unlink(this.getPath(), cb);
  });
}
...