Загрузка файлов и 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().