File uploads and Drive
Let us add a cover to blog posts — an image the author uploads through a form. The file has to be received, checked and saved. For storage AdonisJS has the Drive package: it saves files to the server disk or to cloud storage such as S3, and the code stays the same.
Installing Drive
node ace add @adonisjs/driveThe command asks which storage services to set up — choose fs (the local disk) to start. The file config/drive.ts appears:
// 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 driveConfigFiles go into the storage/ folder, and serveFiles makes them available at addresses like /uploads/....
A form with a file
{{-- 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">Publish</button>
</form>The enctype="multipart/form-data" attribute is required: without it the browser sends only the file name, not its content.
Receiving and saving
// 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 ?? 'No file selected')
}
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 returns the uploaded file and checks its size and extension. cuid() gives a unique name so that files of different authors do not overwrite each other. moveToDisk puts the file into storage under the key covers/..., and the key is saved in the post's cover_key column.
A link to the file
// 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 }}">
@endThe post page shows the cover at an address like /uploads/covers/...jpg.
Checking with VineJS
You can also check the file in a validator together with the other fields — the rule is vine.file({ size: '2mb', extnames: ['jpg', 'png'] }). Then errors appear next to the field, as in the forms lesson.
Common mistake
Saving a file under the name the browser sent (cover.clientName) is dangerous: two authors upload photo.jpg, and the second file replaces the first, and the name may contain ../ and a path into another folder. Always create the name yourself, for example with cuid().