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.