Skip to content

Latest commit

 

History

History
230 lines (177 loc) · 5.97 KB

02.4-backend-images-upload.md

File metadata and controls

230 lines (177 loc) · 5.97 KB

Backend - Images Upload

File Upload

Handling Uploads

yarn add multer
yarn add -D @types/multer

Create src/config/upload.ts (note: it's going to be changed later):

import path from 'path';
import crypto from 'crypto';
import multer from 'multer';

export default {
  storage: multer.diskStorage({
    destination: path.resolve(__dirname, '..', '..', 'tmp'),

    filename(request, file, callback) {
      const fileHash = crypto.randomBytes(10).toString('HEX');
      const fileName = `${fileHash}-${file.originalname}`;

      return callback(null, fileName);
    },
  }),
};

Receiving the file in src/routes/users.routes.ts:

import multer from 'multer';
import uploadConfig from '../config/auth';

const uploadMiddleware = multer(uploadConfig);
// ...

usersRouter.patch(
  '/avatar',
  ensureAuthenticated,
  uploadMiddleware.single('avatar'),
  async (request, response) => {
    return response.json({ ok: true });
  },
);

Test with insomnia and check if the image files are being saved in the /tmp directory.

Create the avatar field for Users

Create a migration to add the avatar column in the users table:

yarn typeorm migration:create -n AddAvatarColumnToUsers

src/migrations/*AddAvatarColumnToUsers:

// ...
public async up(queryRunner: QueryRunner): Promise<void> {
  await queryRunner.addColumn(
    'users',
    new TableColumn({
      name: 'avatar',
      type: 'varchar', // although you can, do NOT save raw image in the DB
      isNullable: true,
    }),
  );
}

public async down(queryRunner: QueryRunner): Promise<void> {
  await queryRunner.dropColumn('users', 'avatar');
}

Add the avatar column in the model too (it's not mentioned in this lecture's video).

src/models/User.ts:

// ...
class User {
  // ...
  @Column()
  avatar: string
  // ...
}

Run the migration.

yarn typeorm migration:run

Updating the Avatar

Create src/services/UpdateUserAvatarService.ts to do this:

  • get the user data from the db (throw error if not found)
  • if user's avatar is not null, delete the old file
  • set the path to the new file as user.avatar

Note: the userRepository.findOne() accepts the user ID as argument.

Note: I noticed the method used in the video to check if a file exists is kinda buggy. Then I used this:

      if (fs.existsSync(userAvatarFilePath)) {
        await fs.promises.unlink(userAvatarFilePath);
      }

My complete source of src/services/UpdateUserAvatarService.ts:

import { getRepository } from 'typeorm';
import path from 'path';
import fs from 'fs';

import uploadConfig from '../config/upload';
import User from '../models/User';

interface Request {
  userId: string;
  avatarFilename: string;
}

class UpdateUserAvatarService {
  public async execute({ userId, avatarFilename }: Request): Promise<User> {
    const usersRepository = getRepository(User);

    const user = await usersRepository.findOne(userId);

    if (!user) {
      throw new Error('Only authenticated user can change avatar');
    }

    if (user.avatar) {
      const userAvatarFilePath = path.join(uploadConfig.directory, user.avatar);

      if (fs.existsSync(userAvatarFilePath)) {
        await fs.promises.unlink(userAvatarFilePath);
      }
    }

    user.avatar = avatarFilename;

    await usersRepository.save(user);

    return user;
  }
}

export default UpdateUserAvatarService;

In the src/routes/users.routes.ts:

usersRouter.patch(
  '/avatar',
  ensureAuthenticated,
  uploadMiddleware.single('avatar'),
  async (request, response) => {
    try {
      const updateUserAvatar = new UpdateUserAvatarService();

      const user = await updateUserAvatar.execute({
        user_id: request.user.id, // ensureAuthenticated middlware filled this
        avatarFilename: request.file.filename, // uploadMiddleware filled this
      });

      return response.json(user);
    } catch(err) {
      return response.status(400).json({ error: err.message });
    }
  },
);

Test with insomnia and check if:

  • the path to the image file was persisted in the database
  • the old file was deleted (if it's an update).

Serving Static Files

src/server.ts:

// ...
import uploadConfig from './config/upload';
// ...
app.use('/files', express.static(uploadConfig.directory));
// ...

Summary

  1. handle file uploads
  • a route for POST /users/avatar receiving an image file
  • install and use multer to create a middleware to get the file
    • save the file in the /tmp directory.
  1. add the column avatar to the users table and User model.
  • create a migration to add the avatar column in the users table
  • add the avatar column in the User model
  1. on upload:
  • delete the current file in user.avatar
  • save the file path to the recently uploaded file in user.avatar

My GoBarber codebase up to this point

https://github.com/meleu/gobarber/tree/fc1c76b7b16fdab909657320ff48c17ae7cc10c5