File Uploads Testing – Persiapan

Pada modul ini kita akan melakukan persiapan untuk file uploads testing. File yang akan diupload adalah file image untuk profile picture. Proses upload hanya dilakukan setelah user login.

Menambahkan direktori image

Pada application root directory, tambahkan directory static/img.

Menambahkan UserImage Type

Saat ini model users belum terdapat field image, buka file pkg/data/users.go, lalu tambahkan field userimage.

package data

import (
	"errors"
	"time"

	"golang.org/x/crypto/bcrypt"
)

// User describes the data for the User type.
type User struct {
	ID         int       `json:"id"`
	FirstName  string    `json:"first_name"`
	LastName   string    `json:"last_name"`
	Email      string    `json:"email"`
	Password   string    `json:"-"`
	IsAdmin    int       `json:"is_admin"`
	CreatedAt  time.Time `json:"-"`
	UpdatedAt  time.Time `json:"-"`
	ProfilePic UserImage `json:"-"`
}

// PasswordMatches uses Go's bcrypt package to compare a user supplied password
// with the hash we have stored for a given user in the database. If the password
// and hash match, we return true; otherwise, we return false.
func (u *User) PasswordMatches(plainText string) (bool, error) {
	err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(plainText))
	if err != nil {
		switch {
		case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
			// invalid password
			return false, nil
		default:
			return false, err
		}
	}

	return true, nil
}

Menambahkan field userimage kedalam query

Berikutnya kita tambahkan field userimage kedalam setiap query pada file pkg/repository/users_postgres.go

package dbrepo

import (
	"context"
	"database/sql"
	"log"
	"time"
	"webapp/pkg/data"

	"golang.org/x/crypto/bcrypt"
)

const dbTimeout = time.Second * 3

type PostgresDBRepo struct {
	DB *sql.DB
}

func (m *PostgresDBRepo) Connection() *sql.DB {
	return m.DB
}

// AllUsers returns all users as a slice of *data.User
func (m *PostgresDBRepo) AllUsers() ([]*data.User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	query := `select id, email, first_name, last_name, password, is_admin, created_at, updated_at
	from users order by last_name`

	rows, err := m.DB.QueryContext(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var users []*data.User

	for rows.Next() {
		var user data.User
		err := rows.Scan(
			&user.ID,
			&user.Email,
			&user.FirstName,
			&user.LastName,
			&user.Password,
			&user.IsAdmin,
			&user.CreatedAt,
			&user.UpdatedAt,
		)
		if err != nil {
			log.Println("Error scanning", err)
			return nil, err
		}

		users = append(users, &user)
	}

	return users, nil
}

// GetUser returns one user by id
func (m *PostgresDBRepo) GetUser(id int) (*data.User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	query := `
		select 
			u.id, u.email, u.first_name, u.last_name, u.password, u.is_admin, u.created_at, u.updated_at,
			coalesce(ui.file_name, '')
		from 
			users u
			left join user_images ui on (ui.user_id = u.id)
		where 
		    u.id = $1`

	var user data.User
	row := m.DB.QueryRowContext(ctx, query, id)

	err := row.Scan(
		&user.ID,
		&user.Email,
		&user.FirstName,
		&user.LastName,
		&user.Password,
		&user.IsAdmin,
		&user.CreatedAt,
		&user.UpdatedAt,
		&user.ProfilePic.FileName,
	)

	if err != nil {
		return nil, err
	}

	return &user, nil
}

// GetUserByEmail returns one user by email address
func (m *PostgresDBRepo) GetUserByEmail(email string) (*data.User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	query := `
		select 
			u.id, u.email, u.first_name, u.last_name, u.password, u.is_admin, u.created_at, u.updated_at,
			coalesce(ui.file_name, '')
		from 
			users u
			left join user_images ui on (ui.user_id = u.id)
		where 
		    u.email = $1`

	var user data.User
	row := m.DB.QueryRowContext(ctx, query, email)

	err := row.Scan(
		&user.ID,
		&user.Email,
		&user.FirstName,
		&user.LastName,
		&user.Password,
		&user.IsAdmin,
		&user.CreatedAt,
		&user.UpdatedAt,
		&user.ProfilePic.FileName,
	)

	if err != nil {
		return nil, err
	}

	return &user, nil
}

// UpdateUser updates one user in the database
func (m *PostgresDBRepo) UpdateUser(u data.User) error {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	stmt := `update users set
		email = $1,
		first_name = $2,
		last_name = $3,
		is_admin = $4,
		updated_at = $5
		where id = $6
	`

	_, err := m.DB.ExecContext(ctx, stmt,
		u.Email,
		u.FirstName,
		u.LastName,
		u.IsAdmin,
		time.Now(),
		u.ID,
	)

	if err != nil {
		return err
	}

	return nil
}

// DeleteUser deletes one user from the database, by id
func (m *PostgresDBRepo) DeleteUser(id int) error {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	stmt := `delete from users where id = $1`

	_, err := m.DB.ExecContext(ctx, stmt, id)
	if err != nil {
		return err
	}

	return nil
}

// InsertUser inserts a new user into the database, and returns the ID of the newly inserted row
func (m *PostgresDBRepo) InsertUser(user data.User) (int, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
	if err != nil {
		return 0, err
	}

	var newID int
	stmt := `insert into users (email, first_name, last_name, password, is_admin, created_at, updated_at)
		values ($1, $2, $3, $4, $5, $6, $7) returning id`

	err = m.DB.QueryRowContext(ctx, stmt,
		user.Email,
		user.FirstName,
		user.LastName,
		hashedPassword,
		user.IsAdmin,
		time.Now(),
		time.Now(),
	).Scan(&newID)

	if err != nil {
		return 0, err
	}

	return newID, nil
}

// ResetPassword is the method we will use to change a user's password.
func (m *PostgresDBRepo) ResetPassword(id int, password string) error {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
	if err != nil {
		return err
	}

	stmt := `update users set password = $1 where id = $2`
	_, err = m.DB.ExecContext(ctx, stmt, hashedPassword, id)
	if err != nil {
		return err
	}

	return nil
}

// InsertUserImage inserts a user profile image into the database.
func (m *PostgresDBRepo) InsertUserImage(i data.UserImage) (int, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	stmt := `delete from user_images where user_id = $1`
	_, err := m.DB.ExecContext(ctx, stmt, i.UserID)
	if err != nil {
		return 0, err
	}

	var newID int
	stmt = `insert into user_images (user_id, file_name, created_at, updated_at)
		values ($1, $2, $3, $4) returning id`

	err = m.DB.QueryRowContext(ctx, stmt,
		i.UserID,
		i.FileName,
		time.Now(),
		time.Now(),
	).Scan(&newID)

	if err != nil {
		return 0, err
	}

	return newID, nil
}

Menambahkan Form upload dan menampilkan profile picture

Berikutnya kita ubah file templates/profile.page.gohtml, untuk menampilkan profile picture jika ada, dan form upload files.

{{template "base" .}}

{{define "content"}}
    <div class="container">
        <div class="row">
            <div class="col">
                <h1 class="mt-3">User Profile</h1>
                <hr>

                {{if ne .User.ProfilePic.FileName ""}}
                    <img class="img-fluid" style="max-width: 300px;" src="/static/img/{{.User.ProfilePic.FileName}}" alt="profile">
                {{else}}
                    <p>No profile image uploaded yet...</p>
                {{end}}

                <hr>

                <form action="/user/upload-profile-pic" method="post" enctype="multipart/form-data">

                    <label for="formFile" class="form-label">Choose an image</label>
                    <input class="form-control" type="file" name="image" id="formFile" accept="image/gif,image/jpeg,image/png">

                    <input class="btn btn-primary mt-3" type="submit" value="Upload">

                </form>

            </div>
        </div>
    </div>
{{end}}

Sampai disini mari kita coba, apakah aplikasi berjalan sesuai ekspektasi, yaitu pada profile page akan ditampilkan form upload dan profile pic bila ada, atau teks status profile belum ada.

Buka command prompt, pastikan dahulu docker image untuk webapp sudah berjalan. Bila belum gunakan perintah docker-compose up -d pada root aplikasi, lalu jalankan webapp dengan perintah go run ./cmd/web.

Sampai disini persiapan awal file uploads sudah dibuat. Pada modul berikutnya kita akan membuat handler untuk menangani proses handler.

Sharing is caring:

Leave a Comment