İçindekiler
GoUI'de ikili dosya verisi WebSocket üzerinden asla yolculuk etmez.
Yüklemeler, küçük bir upload paketi tarafından işlenen normal bir
multipart HTTP POST'tur; sadece ortaya çıkan metadata (bir ID, ad,
URL, boyut, içerik türü), normal bir olay aracılığıyla WS üzerinden bir
bileşene geri akar. Bu, WS protokolünü sadece-JSON tutar ve herhangi bir
bileşen koduna dokunmadan depolama arka ucunu değiştirmenize olanak
tanır.
Bu belge boyunca kullanılan modül yolu: github.com/zatrano/goui.
1. upload.Storage
// upload/storage.go
type Storage interface {
Save(originalName, contentType string, r io.Reader, size int64) (Meta, error)
Open(id string) (io.ReadCloser, Meta, error)
Delete(id string) error
}
type Meta struct {
ID string `json:"id"`
Name string `json:"name"`
ContentType string `json:"contentType"`
Size int64 `json:"size"`
URL string `json:"url"`
StoredAt time.Time `json:"-"`
}
GoUI'de yüklenen dosyalarla ilgilenen her şey — HTTP route'ları,
forms.DragDropUpload bileşeni, istemci upload.js modülü — dosyalarla
yalnızca bu arayüz ve Meta aracılığıyla konuşur. Adapter'ınızın Store
seçeneğine (veya upload.NewHandler / upload.Mount'a) geçirdiğiniz
geçirilen uygulamayı değiştirin, diğer her katman etkilenmez.
2. LocalStore — yerleşik uygulama
// upload/storage.go
type LocalStore struct {
Dir string
BaseURL string // örn. /goui/files
MaxBytes int64
mu sync.RWMutex
index map[string]Meta
}
func NewLocalStore(dir, baseURL string, maxBytes int64) (*LocalStore, error)
Sıfır değerler geçirdiğinizde NewLocalStore tarafından uygulanan
varsayılanlar:
| Alan | Boş/sıfır olduğunda varsayılan |
|---|---|
BaseURL |
/goui/files |
MaxBytes |
8 << 20 (8 MiB) |
Dir'in varsayılanı yoktur — her zaman gerçek bir dizin sağlamanız
gerekir; NewLocalStore, önceden var olması gerekmediği için
os.MkdirAll(dir, 0o755)'i çağırır.
store, err := upload.NewLocalStore("./data/uploads", "/goui/files", 8<<20)
if err != nil {
log.Fatal(err)
}
2.1 Save
- Rastgele 16 baytlık bir hex ID (
newID) üretir, orijinal dosya adını sanitize eder (sanitizeName—filepath.Base+ literal bir".."değiştirme aracılığıyla dizin bileşenlerini ve..dizilerini soyar, dolayısıyla dosya adı aracılığıyla yol geçişi (path traversal) mümkün değildir), ve dosyayı<ext>'in sanitize edilmiş addan alındığı<Dir>/<id><ext>'e yazar. MaxBytes'ı iki kez zorlar: çağıransize'ı zaten önceden biliyorsa hızlı-yol reddi, ve kopyalarken sıkı bir dur (io.LimitReader(r, MaxBytes+1)— kopyaMaxBytes'tan fazla okursa, kısmi dosya kaldırılır ve"file too large"döndürülür). Bu, çağıransizehakkında yalan söylese (veya önceden bilmese) bileMaxBytes'ın zorlanması anlamına gelir.- Hiçbir
Content-Typesağlanmadıysamime.TypeByExtension'a, ve nihayetindeapplication/octet-stream'e geri döner. - Ortaya çıkan
Meta'yı (URL: BaseURL + "/" + idile) bir bellek içi indekste (sync.RWMutexarkasında birmap[string]Meta) kaydeder. Bu indeks kalıcı değildir — process'i yeniden başlatmak, dosya hâlâ diskte olsa bile, önceden yüklenmiş dosyaları ID ileOpen/Deletearacılığıyla sunma yeteneğini kaybeder. Yerel geliştirmenin ötesindeki her şey için buna göre planlayın (üretim şeklindeki alternatif için §5'e bakın).
2.2 Open / Delete
Open(id), ID'yi bellek içi indekste arar, sonra diskte dosyayı bulmak
için <Dir>/<id>.*'i glob'lar (bir uzantısız yola geri dönerek).
Delete(id), indeks girdisini kaldırır ve eşleşen dosyayı/dosyaları
glob'layıp kaldırır. sync.RWMutex sayesinde her ikisi de Save ile
eşzamanlı olarak çağrılmak için güvenlidir.
3. HTTP route'ları
Çekirdek modül framework-agnostic bir net/http handler sunar:
// upload/handler.go
const (
UploadPath = "/goui/upload"
FilesPrefix = "/goui/files"
)
func NewHandler(store Storage) *Handler // POST upload + GET download
func Mount(mux *http.ServeMux, store Storage)
Yüklemeleri adapter'ınızın Store seçeneğiyle bağlayın (önerilen), veya
handler'ı doğrudan net/http üzerinde mount edin:
store, _ := upload.NewLocalStore(filepath.Join(root, "data", "uploads"), "/goui/files", 8<<20)
// Fiber (WebSocket + upload tek çağrıda)
gouifiber.Register(app, gouifiber.Options{
Server: ws.NewServer(hub, registry, tr),
Store: store,
})
// net/http ServeMux
gouistdlib.Register(mux, gouistdlib.Options{Server: server, Store: store})
// Veya yalnızca handler, herhangi bir http.Handler mux üzerinde:
upload.Mount(mux, store)
POST /goui/upload, birfilealanı olan bir multipart form bekler (c.FormFile("file")). Başarıda200veMetaJSON gövdesiyle yanıt verir ({"id":..., "name":..., "contentType":..., "size":..., "url":...}). Eksik bir dosyada veya birStorage.Savehatasında (dosya çok büyük dahil){"error": "..."}ile400yanıtı verir.GET /goui/files/:id, dosyayı saklananContent-Type'ı ve birContent-Disposition: inline; filename="..."başlığıyla geri akıtır, veya başarısızlıkta404/500. Mevcut handler'ın, yanıtı yazmadan önce dosyanın tamamını belleğe tampomladığını (io.ReadAll) unutmayın — bu paketin gönderdiği 8 MiB sınıfı varsayılanlar için sorun değil, amaMaxBytes'ı önemli ölçüde yükseltirseniz veya bu yol üzerinden büyük medya sunuyorsanız gözden geçirilmesi gereken bir şey.
Her iki route da, GoUI'ye özgü hiçbir auth'u olmayan düz HTTP
handler'larıdır — yüklemeler/indirmeler kısıtlanması gerekiyorsa, kendi
middleware'inizi /goui/upload ve /goui/files'ın önüne koyun.
4. İstemci tarafı akış, uçtan uca
forms.DragDropUpload (forms/upload.go), bir bırakma bölgesi artı
gizli bir "taşıyıcı" (carrier) düğme render eder:
type DragDropUpload struct {
core.BaseComponent
CommonAttrs
FieldValidation
Files []UploadedRef
Accept string
Multiple bool
ShowThumbs bool
UploadURL string // varsayılan /goui/upload
EventName string
OnChange func(files []UploadedRef)
}
Tam gidiş-dönüş:
-
Render edilen markup,
data-goui-upload,data-upload-url(varsayılan/goui/upload),data-upload-event, vedata-accept/data-multipleözniteliklerini taşır; bunlar istemci tarafından okunur. -
client/modules/upload.js(enhanceUpload), herhangi bir[data-goui-upload]bölgesindedragover/drop/change'i dinler,accept'e göre filtreler, ve her dosya için şunu çağırır:export async function postFile(url, file) { const fd = new FormData(); fd.append('file', file, file.name); const res = await fetch(url, { method: 'POST', body: fd }); const data = await res.json(); if (!res.ok) throw new Error(data.error || res.statusText); return data; // sunucudan Meta JSON'u } -
Başarıda,
notifyUploaded(zone, meta), döndürülenMeta'yı bölgenin gizli.goui-upload-carrierdüğmesindekidata-goui-*özniteliklerine tıkıştırır ve sentetik olarak tıklar:carrier.setAttribute('data-goui-value', meta.id || ''); carrier.setAttribute('data-goui-id', meta.id || ''); carrier.setAttribute('data-goui-name', meta.name || ''); carrier.setAttribute('data-goui-url', meta.url || ''); carrier.setAttribute('data-goui-size', String(meta.size || 0)); carrier.setAttribute('data-goui-content-type', meta.contentType || ''); carrier.click(); -
Bu tıklama,
goui.jstarafından delege edilen normal birg-clickolayıdır (data-upload-event, ki bu varsayılan olarak<name>.uploaded'dır); bu, aynıdata-goui-*özniteliklerini olay payload'ına okur (collectPayload,data-goui-id,-name,-url,-size,-content-type,-valuegibi diğerlerini tanır) ve onu normal bir WS olayı olarak gönderir. -
DragDropUpload.HandleEvent,"uploaded"eylemini alır, payload'dan birUploadedRefinşa eder,d.Files'ı ekler/değiştirir, ayarlıysaOnChange'i çağırır, veMarkDirty()'yi çağırır — normal bir render, yeni dosyayı (veShowThumbs'sa ve o bir resimse küçük resmi) göstererek takip eder.
Diğer bir deyişle: ikili yükleme, WS render döngüsünün tamamen
dışında, düz HTTP'dir; sadece ortaya çıkan metadata GoUI'ye yeniden
girer, sıradan bir olay payload'ı olarak, türü bir checkbox
değiştirmeden veya bir metin değişikliğinden ayırt edilemez.
forms.NewImageUpload(name, event), yaygın avatar/galeri durumu için
uygun bir preset'tir (Accept: "image/*", ShowThumbs: true).
Kaldırma aynı şekli takip eder: listelenen her dosyadaki "×" düğmesi,
"remove" eylemine bağlı düz bir g-click'tir (data-goui-value="<id>")
— dosyayı gerçekten silmek için herhangi bir HTTP çağrısı yapılmaz;
listeden kaldırmanın saklanan nesneyi de silmesi gerekiyorsa, bunu
HandleEvent'in "remove" dalında (store.Delete(id)'yi çağırarak)
kendiniz ekleyin.
5. Kendi Storage'ınızı yazma (S3/MinIO-şekilli iskelet)
Yukarıdaki her katman sadece Storage arayüzüne bağlı olduğundan, bir
bulut arka ucu eklemek üç metodu uygulama meselesidir. Aşağıda
sadece-imza (signature-only) bir iskelet vardır — kullandığınız
herhangi bir nesne deposu için SDK çağrılarını doldurun (AWS S3, MinIO,
Cloudflare R2, S3-uyumlu bir uç nokta aracılığıyla GCS vb.):
package myupload
import (
"context"
"io"
"time"
"github.com/zatrano/goui/upload"
)
// S3Store, upload.Storage'ı bir S3-uyumlu bucket'a karşı uygular.
type S3Store struct {
Client S3API // ihtiyacınız olan minimal client arayüzü (put/get/delete/head)
Bucket string
BaseURL string // örn. "https://cdn.example.com" veya presigned-URL tabanı
MaxBytes int64
}
// S3API, S3Store'un ihtiyaç duyduğu minimal yüzeydir — bunu kendi SDK'nıza göre darlaştırın.
type S3API interface {
PutObject(ctx context.Context, bucket, key string, body io.Reader, size int64, contentType string) error
GetObject(ctx context.Context, bucket, key string) (io.ReadCloser, error)
HeadObject(ctx context.Context, bucket, key string) (contentType string, size int64, err error)
DeleteObject(ctx context.Context, bucket, key string) error
}
func NewS3Store(client S3API, bucket, baseURL string, maxBytes int64) *S3Store {
// LocalStore'un aynı varsayılan konvansiyonunu uygulayın: baseURL "" -> CDN/tabanınız,
// maxBytes <= 0 -> 8<<20, vb.
return &S3Store{Client: client, Bucket: bucket, BaseURL: baseURL, MaxBytes: maxBytes}
}
func (s *S3Store) Save(originalName, contentType string, r io.Reader, size int64) (upload.Meta, error) {
// 1. size/MaxBytes koruması (size güvenilmezse LocalStore'un LimitReader hilesini yansıt)
// 2. bir key üret, örn. zamana bölünmüş önek + rastgele id + sanitize edilmiş uzantı
// 3. s.Client.PutObject(ctx, s.Bucket, key, r, size, contentType)
// 4. {key -> orijinal ad / içerik türü / boyut}'u kalıcı bir yerde sakla —
// LocalStore'un bellek içi map'inin aksine, bu üretimde bir yeniden başlatmaya
// HAYATTA KALMALIDIR; paket seviyesi bir map değil, kendi uygulamanızın
// kendi veritabanı tablosunu kullanın.
// 5. döndür upload.Meta{ID: key, Name: ..., ContentType: ..., Size: ..., URL: s.BaseURL + "/" + key, StoredAt: time.Now().UTC()}
panic("not implemented")
}
func (s *S3Store) Open(id string) (io.ReadCloser, upload.Meta, error) {
// 1. id için saklanan metadata'yı kalıcı deponuzdan arayın
// 2. s.Client.GetObject(ctx, s.Bucket, id) (veya byte'ları uygulamanız üzerinden
// proxy'lemek yerine çağıranları presigned bir URL'ye yönlendirin — genellikle
// daha iyi üretim seçimi)
panic("not implemented")
}
func (s *S3Store) Delete(id string) error {
// 1. kalıcı metadata satırını kaldır
// 2. s.Client.DeleteObject(ctx, s.Bucket, id)
panic("not implemented")
}
Ardından tam olarak LocalStore gibi bağlayın:
store := myupload.NewS3Store(s3Client, "my-bucket", "https://cdn.example.com", 8<<20)
gouifiber.Register(app, gouifiber.Options{Server: server, Store: store})
// GET /goui/files/:id, özel bir Storage'da presigned URL'ye 302 yönlendirebilir
forms.DragDropUpload, WS katmanı, veya istemci JS'sinin hiçbiri
değişmesi gerekmez — sadece Meta'yı (id, name, contentType,
size, url) ve iki HTTP route'unun JSON sözleşmesini görürler.
6. Pratik notlar
MaxBytes, sadece bir UI ipucu değil, sunucu tarafı sert bir sınırdır. UX'i şekillendirmek içinDragDropUpload'daaccept/Multiple'ı ayarlayın, ama el yapımı bir istek herhangi bir istemci tarafı kontrolünü atlayabildiğinden, her zamanMaxBytes'ı da gerçekten zorlamak istediğiniz sınıra yapılandırın./goui/uploadve/goui/files/:id, varsayılan olarak kimlik doğrulamasızdır. Yüklemeler/indirmeler herkese açık olması amaçlanmadıysa, kendi auth middleware'inizi bunların önüne uygulayın.- Bellek içi
LocalStore.index, bir yeniden başlatmaya hayatta kalmaz. Yerel geliştirme ve paketlenmiş örnekler için tasarlanmıştır. Kalıcı olan herhangi bir şey için, gerçek bir veritabanı + disk/nesne deposuyla desteklenen kendiStorage'ınızı yazın, veya en azından bellek içi indeksi, başlangıçta diskten (veya gerçek bir tablodan) yeniden inşa edilen biriyle değiştirin. - Bir
DragDropUploadlistesinden bir dosyayı silmek, sizHandleEvent'in"remove"dalında kendiniz birstore.Delete(id)çağrısı eklemediğiniz sürece sadece-UI bir işlemdir — bkz. §4.