merged main, dev, and i guess got accounts working??

i am so good at commit messages :3
This commit is contained in:
ari melody 2025-01-20 15:08:01 +00:00
commit 5566a795da
Signed by: ari
GPG key ID: CF99829C92678188
53 changed files with 1366 additions and 398 deletions

175
api/account.go Normal file
View file

@ -0,0 +1,175 @@
package api
import (
"arimelody-web/controller"
"arimelody-web/model"
"arimelody-web/global"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
)
func handleLogin() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
credentials := LoginRequest{}
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
account, err := controller.GetAccount(global.DB, credentials.Username)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.Error(w, "Invalid username or password", http.StatusBadRequest)
return
}
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = bcrypt.CompareHashAndPassword(account.Password, []byte(credentials.Password))
if err != nil {
http.Error(w, "Invalid username or password", http.StatusBadRequest)
return
}
// TODO: sessions and tokens
w.WriteHeader(http.StatusOK)
w.Write([]byte("Logged in successfully. TODO: Session tokens\n"))
})
}
func handleAccountRegistration() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
type RegisterRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
Code string `json:"code"`
}
credentials := RegisterRequest{}
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// make sure code exists in DB
invite := model.Invite{}
err = global.DB.Get(&invite, "SELECT * FROM invite WHERE code=$1", credentials.Code)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.Error(w, "Invalid invite code", http.StatusBadRequest)
return
}
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if time.Now().After(invite.ExpiresAt) {
http.Error(w, "Invalid invite code", http.StatusBadRequest)
_, err = global.DB.Exec("DELETE FROM invite WHERE code=$1", credentials.Code)
if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %s\n", err.Error()) }
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
account := model.Account{
Username: credentials.Username,
Password: hashedPassword,
Email: credentials.Email,
AvatarURL: "/img/default-avatar.png",
}
err = controller.CreateAccount(global.DB, &account)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
_, err = global.DB.Exec("DELETE FROM invite WHERE code=$1", credentials.Code)
if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %s\n", err.Error()) }
w.WriteHeader(http.StatusCreated)
w.Write([]byte("Account created successfully\n"))
})
}
func handleDeleteAccount() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
credentials := LoginRequest{}
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
account, err := controller.GetAccount(global.DB, credentials.Username)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
http.Error(w, "Invalid username or password", http.StatusBadRequest)
return
}
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = bcrypt.CompareHashAndPassword(account.Password, []byte(credentials.Password))
if err != nil {
http.Error(w, "Invalid username or password", http.StatusBadRequest)
return
}
err = controller.DeleteAccount(global.DB, account.ID)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to delete account: %s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Account deleted successfully\n"))
})
}

View file

@ -13,6 +13,12 @@ import (
func Handler() http.Handler {
mux := http.NewServeMux()
// ACCOUNT ENDPOINTS
mux.Handle("/v1/login", handleLogin())
mux.Handle("/v1/register", handleAccountRegistration())
mux.Handle("/v1/delete-account", handleDeleteAccount())
// ARTIST ENDPOINTS
mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -34,10 +40,10 @@ func Handler() http.Handler {
ServeArtist(artist).ServeHTTP(w, r)
case http.MethodPut:
// PUT /api/v1/artist/{id} (admin)
admin.MustAuthorise(UpdateArtist(artist)).ServeHTTP(w, r)
admin.RequireAccount(global.DB, UpdateArtist(artist)).ServeHTTP(w, r)
case http.MethodDelete:
// DELETE /api/v1/artist/{id} (admin)
admin.MustAuthorise(DeleteArtist(artist)).ServeHTTP(w, r)
admin.RequireAccount(global.DB, DeleteArtist(artist)).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@ -49,7 +55,7 @@ func Handler() http.Handler {
ServeAllArtists().ServeHTTP(w, r)
case http.MethodPost:
// POST /api/v1/artist (admin)
admin.MustAuthorise(CreateArtist()).ServeHTTP(w, r)
admin.RequireAccount(global.DB, CreateArtist()).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@ -76,10 +82,10 @@ func Handler() http.Handler {
ServeRelease(release).ServeHTTP(w, r)
case http.MethodPut:
// PUT /api/v1/music/{id} (admin)
admin.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r)
admin.RequireAccount(global.DB, UpdateRelease(release)).ServeHTTP(w, r)
case http.MethodDelete:
// DELETE /api/v1/music/{id} (admin)
admin.MustAuthorise(DeleteRelease(release)).ServeHTTP(w, r)
admin.RequireAccount(global.DB, DeleteRelease(release)).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@ -91,7 +97,7 @@ func Handler() http.Handler {
ServeCatalog().ServeHTTP(w, r)
case http.MethodPost:
// POST /api/v1/music (admin)
admin.MustAuthorise(CreateRelease()).ServeHTTP(w, r)
admin.RequireAccount(global.DB, CreateRelease()).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@ -115,13 +121,13 @@ func Handler() http.Handler {
switch r.Method {
case http.MethodGet:
// GET /api/v1/track/{id} (admin)
admin.MustAuthorise(ServeTrack(track)).ServeHTTP(w, r)
admin.RequireAccount(global.DB, ServeTrack(track)).ServeHTTP(w, r)
case http.MethodPut:
// PUT /api/v1/track/{id} (admin)
admin.MustAuthorise(UpdateTrack(track)).ServeHTTP(w, r)
admin.RequireAccount(global.DB, UpdateTrack(track)).ServeHTTP(w, r)
case http.MethodDelete:
// DELETE /api/v1/track/{id} (admin)
admin.MustAuthorise(DeleteTrack(track)).ServeHTTP(w, r)
admin.RequireAccount(global.DB, DeleteTrack(track)).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@ -130,10 +136,10 @@ func Handler() http.Handler {
switch r.Method {
case http.MethodGet:
// GET /api/v1/track (admin)
admin.MustAuthorise(ServeAllTracks()).ServeHTTP(w, r)
admin.RequireAccount(global.DB, ServeAllTracks()).ServeHTTP(w, r)
case http.MethodPost:
// POST /api/v1/track (admin)
admin.MustAuthorise(CreateTrack()).ServeHTTP(w, r)
admin.RequireAccount(global.DB, CreateTrack()).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}

View file

@ -10,7 +10,6 @@ import (
"strings"
"time"
"arimelody-web/admin"
"arimelody-web/global"
"arimelody-web/controller"
"arimelody-web/model"
@ -21,13 +20,15 @@ func ServeAllArtists() http.Handler {
var artists = []*model.Artist{}
artists, err := controller.GetAllArtists(global.DB)
if err != nil {
fmt.Printf("FATAL: Failed to serve all artists: %s\n", err)
fmt.Printf("WARN: Failed to serve all artists: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(artists)
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(artists)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
@ -51,12 +52,17 @@ func ServeArtist(artist *model.Artist) http.Handler {
}
)
show_hidden_releases := admin.GetSession(r) != nil
account, err := controller.GetAccountByRequest(global.DB, r)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
show_hidden_releases := account != nil
var dbCredits []*model.Credit
dbCredits, err := controller.GetArtistCredits(global.DB, artist.ID, show_hidden_releases)
if err != nil {
fmt.Printf("FATAL: Failed to retrieve artist credits for %s: %s\n", artist.ID, err)
fmt.Printf("WARN: Failed to retrieve artist credits for %s: %s\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -74,7 +80,9 @@ func ServeArtist(artist *model.Artist) http.Handler {
}
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(artistJSON{
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(artistJSON{
Artist: artist,
Credits: credits,
})
@ -105,7 +113,7 @@ func CreateArtist() http.Handler {
http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest)
return
}
fmt.Printf("FATAL: Failed to create artist %s: %s\n", artist.ID, err)
fmt.Printf("WARN: Failed to create artist %s: %s\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -118,7 +126,7 @@ func UpdateArtist(artist *model.Artist) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := json.NewDecoder(r.Body).Decode(&artist)
if err != nil {
fmt.Printf("FATAL: Failed to update artist: %s\n", err)
fmt.Printf("WARN: Failed to update artist: %s\n", err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
@ -153,7 +161,7 @@ func UpdateArtist(artist *model.Artist) http.Handler {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to update artist %s: %s\n", artist.ID, err)
fmt.Printf("WARN: Failed to update artist %s: %s\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
@ -167,7 +175,7 @@ func DeleteArtist(artist *model.Artist) http.Handler {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to delete artist %s: %s\n", artist.ID, err)
fmt.Printf("WARN: Failed to delete artist %s: %s\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})

View file

@ -10,7 +10,6 @@ import (
"strings"
"time"
"arimelody-web/admin"
"arimelody-web/global"
"arimelody-web/controller"
"arimelody-web/model"
@ -19,10 +18,23 @@ import (
func ServeRelease(release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// only allow authorised users to view hidden releases
authorised := admin.GetSession(r) != nil
if !authorised && !release.Visible {
http.NotFound(w, r)
return
privileged := false
if !release.Visible {
account, err := controller.GetAccountByRequest(global.DB, r)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if account != nil {
// TODO: check privilege on release
privileged = true
}
if !privileged {
http.NotFound(w, r)
return
}
}
type (
@ -53,18 +65,18 @@ func ServeRelease(release *model.Release) http.Handler {
Links: make(map[string]string),
}
if authorised || release.IsReleased() {
if release.IsReleased() || privileged {
// get credits
credits, err := controller.GetReleaseCredits(global.DB, release.ID)
if err != nil {
fmt.Printf("FATAL: Failed to serve release %s: Credits: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
for _, credit := range credits {
artist, err := controller.GetArtist(global.DB, credit.Artist.ID)
if err != nil {
fmt.Printf("FATAL: Failed to serve release %s: Artists: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -79,7 +91,7 @@ func ServeRelease(release *model.Release) http.Handler {
// get tracks
tracks, err := controller.GetReleaseTracks(global.DB, release.ID)
if err != nil {
fmt.Printf("FATAL: Failed to serve release %s: Tracks: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -94,7 +106,7 @@ func ServeRelease(release *model.Release) http.Handler {
// get links
links, err := controller.GetReleaseLinks(global.DB, release.ID)
if err != nil {
fmt.Printf("FATAL: Failed to serve release %s: Links: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -104,7 +116,9 @@ func ServeRelease(release *model.Release) http.Handler {
}
w.Header().Add("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(response)
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err := encoder.Encode(response)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@ -132,11 +146,24 @@ func ServeCatalog() http.Handler {
}
catalog := []Release{}
authorised := admin.GetSession(r) != nil
account, err := controller.GetAccountByRequest(global.DB, r)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err.Error())
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
for _, release := range releases {
if !release.Visible && !authorised {
continue
if !release.Visible {
privileged := false
if account != nil {
// TODO: check privilege on release
privileged = true
}
if !privileged {
continue
}
}
artists := []string{}
for _, credit := range release.Credits {
if !credit.Primary { continue }
@ -155,7 +182,9 @@ func ServeCatalog() http.Handler {
}
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(catalog)
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(catalog)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@ -197,14 +226,16 @@ func CreateRelease() http.Handler {
http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest)
return
}
fmt.Printf("FATAL: Failed to create release %s: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to create release %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
err = json.NewEncoder(w).Encode(release)
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(release)
if err != nil {
fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -275,7 +306,7 @@ func UpdateRelease(release *model.Release) http.Handler {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to update release %s: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
@ -296,7 +327,7 @@ func UpdateReleaseTracks(release *model.Release) http.Handler {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to update tracks for %s: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
@ -337,7 +368,7 @@ func UpdateReleaseCredits(release *model.Release) http.Handler {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
@ -363,7 +394,7 @@ func UpdateReleaseLinks(release *model.Release) http.Handler {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
@ -377,7 +408,7 @@ func DeleteRelease(release *model.Release) http.Handler {
http.NotFound(w, r)
return
}
fmt.Printf("FATAL: Failed to delete release %s: %s\n", release.ID, err)
fmt.Printf("WARN: Failed to delete release %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})

View file

@ -28,7 +28,7 @@ func ServeAllTracks() http.Handler {
var dbTracks = []*model.Track{}
dbTracks, err := controller.GetAllTracks(global.DB)
if err != nil {
fmt.Printf("FATAL: Failed to pull tracks from DB: %s\n", err)
fmt.Printf("WARN: Failed to pull tracks from DB: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
@ -40,9 +40,11 @@ func ServeAllTracks() http.Handler {
}
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(tracks)
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(tracks)
if err != nil {
fmt.Printf("FATAL: Failed to serve all tracks: %s\n", err)
fmt.Printf("WARN: Failed to serve all tracks: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
@ -52,7 +54,7 @@ func ServeTrack(track *model.Track) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dbReleases, err := controller.GetTrackReleases(global.DB, track.ID, false)
if err != nil {
fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
fmt.Printf("WARN: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
@ -62,9 +64,11 @@ func ServeTrack(track *model.Track) http.Handler {
}
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(Track{ track, releases })
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(Track{ track, releases })
if err != nil {
fmt.Printf("FATAL: Failed to serve track %s: %s\n", track.ID, err)
fmt.Printf("WARN: Failed to serve track %s: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
@ -91,7 +95,7 @@ func CreateTrack() http.Handler {
id, err := controller.CreateTrack(global.DB, &track)
if err != nil {
fmt.Printf("FATAL: Failed to create track: %s\n", err)
fmt.Printf("WARN: Failed to create track: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -128,7 +132,9 @@ func UpdateTrack(track *model.Track) http.Handler {
}
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(track)
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
err = encoder.Encode(track)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

View file

@ -1,6 +1,7 @@
package api
import (
"arimelody-web/global"
"bufio"
"encoding/base64"
"errors"
@ -15,6 +16,7 @@ func HandleImageUpload(data *string, directory string, filename string) (string,
header := split[0]
imageData, err := base64.StdEncoding.DecodeString(split[1])
ext, _ := strings.CutPrefix(header, "data:image/")
directory = filepath.Join(global.Config.DataDirectory, directory)
switch ext {
case "png":