Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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/drive

The 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 driveConfig

Files 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.

// 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

The 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().

Thanks for reading!

Donate to the site — we work hard for you.