Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Загрузка файлов и Drive

Добавим к статьям блога обложку — картинку, которую автор загружает через форму. Файл нужно принять, проверить и сохранить. Для хранения в AdonisJS есть пакет Drive: он сохраняет файлы на диск сервера или в облачное хранилище вроде S3, а код при этом не меняется.

Установка Drive

node ace add @adonisjs/drive

Команда спросит, какие хранилища настроить, — для начала выберите fs (локальный диск). Появится файл config/drive.ts:

// config/drive.ts
import env from '#start/env'
import app from '@adonisjs/core/services/app'
import { defineConfig, services } from '@adonisjs/drive'
 
const driveConfig = defineConfig({
  default: env.get('DRIVE_DISK'),
  services: {
    fs: services.fs({
      location: app.makePath('storage'),
      serveFiles: true,
      routeBasePath: '/uploads',
      visibility: 'public',
    }),
  },
})
 
export default driveConfig

Файлы будут лежать в папке storage/, а serveFiles открывает их по адресам вида /uploads/....

Форма с файлом

{{-- resources/views/pages/posts/create.edge --}}
<form method="POST" action="{{ route('posts.store') }}" enctype="multipart/form-data">
  {{ csrfField() }}
  <input type="text" name="title">
  <input type="file" name="cover" accept="image/*">
  <button type="submit">Опубликовать</button>
</form>

Атрибут enctype="multipart/form-data" обязателен: без него браузер отправит только имя файла, а не содержимое.

Приём и сохранение

// app/controllers/posts_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import { cuid } from '@adonisjs/core/helpers'
import Post from '#models/post'
 
export default class PostsController {
  async store({ request, response }: HttpContext) {
    const cover = request.file('cover', {
      size: '2mb',
      extnames: ['jpg', 'jpeg', 'png', 'webp'],
    })
 
    if (!cover || !cover.isValid) {
      return response.badRequest(cover?.errors ?? 'Файл не выбран')
    }
 
    const key = `covers/${cuid()}.${cover.extname}`
    await cover.moveToDisk(key)
 
    const post = await Post.create({ title: request.input('title'), coverKey: key })
    return response.redirect().toRoute('posts.show', { id: post.id })
  }
}

request.file возвращает загруженный файл и проверяет его размер и расширение. cuid() даёт уникальное имя, чтобы файлы разных авторов не затёрли друг друга. moveToDisk кладёт файл в хранилище по ключу covers/..., а ключ сохраняется в столбце статьи cover_key.

Ссылка на файл

// app/controllers/posts_controller.ts
import drive from '@adonisjs/drive/services/main'
 
const coverUrl = post.coverKey ? await drive.use().getUrl(post.coverKey) : null
return view.render('pages/posts/show', { post, coverUrl })
{{-- resources/views/pages/posts/show.edge --}}
@if(coverUrl)
  <img src="{{ coverUrl }}" alt="{{ post.title }}">
@end

На странице статьи появится обложка с адресом вида /uploads/covers/...jpg.

Проверка через VineJS

Файл можно проверять и в валидаторе вместе с остальными полями — правило vine.file({ size: '2mb', extnames: ['jpg', 'png'] }). Тогда ошибки покажутся рядом с полем, как в уроке о формах.

Частая ошибка

Сохранять файл под именем, которое прислал браузер (cover.clientName), опасно: два автора загрузят photo.jpg, и второй файл заменит первый, а имя может содержать ../ и путь в чужую папку. Всегда создавайте имя сами, например через cuid().

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.