Post di aplikasi blog biasanya butuh gambar/thumbnail supaya lebih menarik saat ditampilkan. Di part ini kita akan menambahkan fitur upload gambar untuk post, menggunakan filesystem abstraction yang sudah disediakan Goravel.
Menambahkan Kolom Thumbnail
go run . artisan make:migration add_thumbnail_to_posts_table
package migrations
import (
"github.com/goravel/framework/contracts/database/schema"
"github.com/goravel/framework/facades"
)
type M20260821000001AddThumbnailToPostsTable struct{}
func (r *M20260821000001AddThumbnailToPostsTable) Signature() string {
return "20260821000001_add_thumbnail_to_posts_table"
}
func (r *M20260821000001AddThumbnailToPostsTable) Up() error {
if facades.Schema().HasTable("posts") && !facades.Schema().HasColumn("posts", "thumbnail") {
return facades.Schema().Table("posts", func(table schema.Blueprint) {
table.String("thumbnail", 255).Nullable()
})
}
return nil
}
func (r *M20260821000001AddThumbnailToPostsTable) Down() error {
return facades.Schema().Table("posts", func(table schema.Blueprint) {
table.DropColumn("thumbnail")
})
}
Tambahkan field Thumbnail di app/models/post.go:
Thumbnail string `json:"thumbnail"`
Disk "public" di Konfigurasi Filesystem
Goravel sudah menyiapkan disk public secara default di config/filesystems.go:
"public": map[string]any{
"driver": "local",
"root": path.Storage("app/public"),
"url": config.Env("APP_URL", "").(string) + "/storage",
},
Disk ini menyimpan file secara lokal di storage/app/public, dan setiap file yang disimpan di sana otomatis punya URL publik dengan prefix /storage.
Menangani Upload File di Controller
Sebelum menyimpan file, kita wajib validasi tipe dan ukurannya dulu. Kalau tidak, endpoint ini jadi celah upload sembarang file — termasuk file .svg atau .html yang bisa berisi JavaScript. Kalau file itu nanti dibuka langsung dari URL publiknya, browser bisa saja mengeksekusi script di dalamnya (stored XSS). Makanya validasinya tidak boleh hanya mengandalkan ekstensi nama file atau Content-Type yang diklaim oleh client — keduanya gampang dipalsukan. Kita cek isi file yang sebenarnya (magic bytes), bukan namanya.
Tambahkan fungsi attachThumbnail di app/http/controllers/post_controller.go:
var allowedThumbnailMimeTypes = map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/webp": true,
}
const maxThumbnailSize = 2 * 1024 * 1024 // 2MB
func attachThumbnail(ctx http.Context, post *models.Post) error {
file, err := ctx.Request().File("thumbnail")
if err != nil {
// Tidak ada file yang diupload, tidak masalah.
return nil
}
size, err := file.Size()
if err != nil {
return err
}
if size > maxThumbnailSize {
return errors.New("thumbnail exceeds the 2MB size limit")
}
mimeType, err := file.MimeType()
if err != nil {
return err
}
if !allowedThumbnailMimeTypes[mimeType] {
return fmt.Errorf("unsupported thumbnail type %q — only JPEG, PNG, and WEBP are allowed", mimeType)
}
path, err := facades.Storage().Disk("public").PutFile("thumbnails", file)
if err != nil {
return err
}
post.Thumbnail = facades.Storage().Disk("public").Url(path)
return facades.Orm().Query().Save(post)
}
ctx.Request().File("thumbnail") mengambil file yang diupload lewat form-data dengan nama field thumbnail. Kalau tidak ada file yang dikirim, kita anggap wajar (bukan error) — thumbnail memang bersifat opsional.
Bagian pentingnya ada di file.MimeType() — di balik layar, Goravel men-scan isi file (magic bytes) pakai library mimetype, bukan sekadar membaca ekstensi atau header Content-Type yang dikirim client. Jadi kalau ada yang coba upload file HTML berisi <script> lalu dinamai gambar.png dan diklaim Content-Type: image/png, tetap akan terdeteksi sebagai text/html dan ditolak. Baru setelah lolos pengecekan ukuran dan tipe, PutFile("thumbnails", file) menyimpan file tersebut ke folder thumbnails di disk public, dengan nama file yang di-hash otomatis supaya tidak ada tabrakan nama. Method ini mengembalikan path relatif file yang tersimpan, yang kita ubah menjadi URL publik lewat .Url(path).
Jangan lupa tambahkan "errors" dan "fmt" ke import di bagian atas file.
Panggil fungsi ini di method Store dan Update, setelah post berhasil disimpan:
if err := attachThumbnail(ctx, &post); err != nil {
return helpers.Error(ctx, http.StatusBadRequest, "Failed to upload thumbnail", err.Error())
}
Testing
Karena melibatkan file, testing endpoint ini tidak bisa pakai body JSON biasa — harus multipart/form-data. Contoh pakai curl:
curl -X POST http://127.0.0.1:3000/posts \
-H "Authorization: Bearer {token}" \
-F "title=Post dengan Thumbnail" \
-F "body=Testing upload gambar" \
-F "thumbnail=@/path/ke/gambar.png;type=image/png"
Kalau berhasil, response data.thumbnail akan berisi URL lengkap ke gambar yang baru diupload, misalnya:
http://localhost/storage/thumbnails/NB5g6Q6tX4rfVGOE7FBqqDEoAyMe2A5B7HfZinnD.png
Cek juga file fisiknya benar-benar tersimpan di storage/app/public/thumbnails/.
Sekarang coba juga skenario yang seharusnya ditolak — file SVG berisi payload, dan file HTML yang dinamai .png:
echo '<svg onload="alert(1)" xmlns="http://www.w3.org/2000/svg"></svg>' > evil.svg
curl -X POST http://127.0.0.1:3000/posts \
-H "Authorization: Bearer {token}" \
-F "title=Evil" -F "body=test" \
-F "thumbnail=@evil.svg;type=image/svg+xml"
# -> 400, "unsupported thumbnail type \"image/svg+xml\""
echo '<html><body><script>alert(1)</script></body></html>' > evil.png
curl -X POST http://127.0.0.1:3000/posts \
-H "Authorization: Bearer {token}" \
-F "title=Evil PNG" -F "body=test" \
-F "thumbnail=@evil.png;type=image/png"
# -> 400, "unsupported thumbnail type \"text/html; charset=utf-8\"" — meskipun namanya .png
# dan Content-Type yang dikirim mengaku image/png, isinya tetap terdeteksi HTML.
Penutup
Validasi tipe file lewat content-sniffing ini jauh lebih aman dibanding cuma percaya ekstensi nama file. Di part berikutnya kita akan membahas refresh token dan logout untuk melengkapi sistem autentikasi kita.
Bagian dari Series: Seri Tutorial Belajar Framework Goravel Rest API untuk Pemula
Overview Seri ini membahas cara membangun REST API menggunakan Goravel, framework web berbasis Golang yang terinspirasi dari Laravel. Materi mencakup...
Lihat Series Lengkap