Post yang punya gambar sampul jauh lebih menarik ditampilkan di halaman blog. Di part ini kita tambahkan endpoint untuk upload thumbnail, lengkap dengan validasi tipe file dan ukuran maksimal.
Menambahkan Kolom Thumbnail
Update models/post.go:
type Post struct {
gorm.Model
Title string `gorm:"size:255;not null" json:"title"`
Slug string `gorm:"size:255;uniqueIndex;not null" json:"slug"`
Body string `gorm:"type:text" json:"body"`
Thumbnail string `gorm:"size:255" json:"thumbnail,omitempty"`
UserID uint `gorm:"index" json:"user_id"`
Author *User `gorm:"foreignKey:UserID" json:"author,omitempty"`
Categories []Category `gorm:"many2many:post_categories;" json:"categories,omitempty"`
}
Thumbnail cuma menyimpan path filenya, bukan file itu sendiri — file gambar aslinya disimpan di filesystem, bukan di database.
Menyiapkan Folder Upload & Static File Server
Buat folder public/uploads/ di root project (tambahkan file .gitkeep supaya foldernya ikut ter-track git walau kosong). Tambahkan ke .gitignore supaya file yang di-upload user tidak ikut ter-commit, tapi struktur foldernya tetap ada:
/public/uploads/*
!/public/uploads/.gitkeep
Fiber punya app.Static() untuk menyajikan file statis. Tambahkan di main.go:
app.Static("/uploads", "./public/uploads")
Dengan ini, file yang tersimpan di public/uploads/nama-file.png otomatis bisa diakses lewat http://localhost:3000/uploads/nama-file.png.
Endpoint Upload
Tambahkan ke handlers/post_handler.go:
const (
thumbnailMaxSizeBytes = 2 << 20 // 2MB
thumbnailUploadDir = "./public/uploads"
)
var allowedThumbnailExt = map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
}
func (h *PostHandler) UploadThumbnail(c *fiber.Ctx) error {
var post models.Post
id := c.Params("id")
if err := h.DB.First(&post, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return response.Error(c, fiber.StatusNotFound, "post tidak ditemukan")
}
return response.Error(c, fiber.StatusInternalServerError, "gagal mengambil data post")
}
if post.UserID != c.Locals("user_id").(uint) {
return response.Error(c, fiber.StatusForbidden, "anda tidak berhak mengubah post ini")
}
file, err := c.FormFile("thumbnail")
if err != nil {
return response.Error(c, fiber.StatusBadRequest, "file thumbnail wajib diisi")
}
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedThumbnailExt[ext] {
return response.Error(c, fiber.StatusUnprocessableEntity, "format file harus jpg, jpeg, atau png")
}
if file.Size > thumbnailMaxSizeBytes {
return response.Error(c, fiber.StatusUnprocessableEntity, "ukuran file maksimal 2MB")
}
filename := fmt.Sprintf("%s%s", uuid.NewString(), ext)
if err := c.SaveFile(file, filepath.Join(thumbnailUploadDir, filename)); err != nil {
return response.Error(c, fiber.StatusInternalServerError, "gagal menyimpan file")
}
post.Thumbnail = "/uploads/" + filename
if err := h.DB.Save(&post).Error; err != nil {
return response.Error(c, fiber.StatusInternalServerError, "gagal memperbarui data post")
}
return response.Success(c, fiber.StatusOK, "thumbnail berhasil diupload", post)
}
Beberapa hal penting:
- Sama seperti Update/Delete, endpoint ini memvalidasi kepemilikan post — hanya penulis yang boleh mengganti thumbnail-nya.
c.FormFile("thumbnail")membaca file darimultipart/form-data, bukan JSON body biasa — cara request-nya beda dari endpoint lain.- Ekstensi file divalidasi lewat whitelist (
allowedThumbnailExt), bukan blacklist — lebih aman karena kita eksplisit menentukan apa yang boleh, bukan mencoba menebak apa yang harus diblokir. - Ukuran file dibatasi 2MB lewat pengecekan
file.Sizesebelum disimpan. - Nama file di-generate ulang pakai
uuid.NewString()— bukan memakai nama asli dari user. Ini penting: nama file asli bisa berisi karakter aneh, bentrok dengan file lain, atau bahkan disusupi path traversal (../../etc/passwd) kalau dipakai mentah-mentah.
Menambahkan Route
posts.Post("/:id/thumbnail", middleware.AuthRequired, postHandler.UploadThumbnail)
Testing
# Upload thumbnail ke post milik sendiri
curl -X POST http://127.0.0.1:3000/api/v1/posts/30/thumbnail \
-H "Authorization: Bearer $TOKEN" \
-F "thumbnail=@/path/ke/gambar.png"
Response akan berisi path thumbnail yang baru:
{
"message": "thumbnail berhasil diupload",
"data": {
"ID": 30,
"thumbnail": "/uploads/9335d89d-9b8d-4141-8b9c-899aad5b13ff.png",
...
}
}
Akses langsung filenya lewat browser atau curl:
curl -I http://127.0.0.1:3000/uploads/9335d89d-9b8d-4141-8b9c-899aad5b13ff.png
Coba juga upload file yang bukan gambar (misalnya .txt) — akan ditolak dengan 422 dan pesan "format file harus jpg, jpeg, atau png". Dan tanpa token sama sekali, request akan ditolak 401 seperti endpoint terproteksi lainnya.
Post sekarang bisa punya thumbnail. Di part berikutnya kita tambahkan sistem role — membedakan user biasa dengan admin, supaya beberapa aksi bisa dibatasi khusus admin.
Bagian dari Series: GoFiber Dasar - Belajar Fundamental Lewat REST API Blog
Belajar konsep-konsep dasar GoFiber (routing, handler, GORM, MySQL, JWT auth) dengan membangun REST API blog sederhana dari nol - lengkap dengan auten...
Lihat Series Lengkap