bring in khatru and eventstore.
This commit is contained in:
45
khatru/blossom/authorization.go
Normal file
45
khatru/blossom/authorization.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func readAuthorization(r *http.Request) (*nostr.Event, error) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(token, "Nostr ") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
eventj, err := base64.StdEncoding.DecodeString(token[6:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base64 token")
|
||||
}
|
||||
var evt nostr.Event
|
||||
if err := easyjson.Unmarshal(eventj, &evt); err != nil {
|
||||
return nil, fmt.Errorf("broken event")
|
||||
}
|
||||
if evt.Kind != 24242 || !evt.CheckID() {
|
||||
return nil, fmt.Errorf("invalid event")
|
||||
}
|
||||
if ok, _ := evt.CheckSignature(); !ok {
|
||||
return nil, fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
expirationTag := evt.Tags.Find("expiration")
|
||||
if expirationTag == nil {
|
||||
return nil, fmt.Errorf("missing \"expiration\" tag")
|
||||
}
|
||||
expiration, _ := strconv.ParseInt(expirationTag[1], 10, 64)
|
||||
if nostr.Timestamp(expiration) < nostr.Now() {
|
||||
return nil, fmt.Errorf("event expired")
|
||||
}
|
||||
|
||||
return &evt, nil
|
||||
}
|
||||
26
khatru/blossom/blob.go
Normal file
26
khatru/blossom/blob.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type BlobDescriptor struct {
|
||||
URL string `json:"url"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Uploaded nostr.Timestamp `json:"uploaded"`
|
||||
|
||||
Owner string `json:"-"`
|
||||
}
|
||||
|
||||
type BlobIndex interface {
|
||||
Keep(ctx context.Context, blob BlobDescriptor, pubkey string) error
|
||||
List(ctx context.Context, pubkey string) (chan BlobDescriptor, error)
|
||||
Get(ctx context.Context, sha256 string) (*BlobDescriptor, error)
|
||||
Delete(ctx context.Context, sha256 string, pubkey string) error
|
||||
}
|
||||
|
||||
var _ BlobIndex = (*EventStoreBlobIndexWrapper)(nil)
|
||||
104
khatru/blossom/eventstorewrapper.go
Normal file
104
khatru/blossom/eventstorewrapper.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/fiatjaf/eventstore"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
// EventStoreBlobIndexWrapper uses fake events to keep track of what blobs we have stored and who owns them
|
||||
type EventStoreBlobIndexWrapper struct {
|
||||
eventstore.Store
|
||||
|
||||
ServiceURL string
|
||||
}
|
||||
|
||||
func (es EventStoreBlobIndexWrapper) Keep(ctx context.Context, blob BlobDescriptor, pubkey string) error {
|
||||
ch, err := es.Store.QueryEvents(ctx, nostr.Filter{Authors: []string{pubkey}, Kinds: []int{24242}, Tags: nostr.TagMap{"x": []string{blob.SHA256}}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if <-ch == nil {
|
||||
// doesn't exist, save
|
||||
evt := &nostr.Event{
|
||||
PubKey: pubkey,
|
||||
Kind: 24242,
|
||||
Tags: nostr.Tags{
|
||||
{"x", blob.SHA256},
|
||||
{"type", blob.Type},
|
||||
{"size", strconv.Itoa(blob.Size)},
|
||||
},
|
||||
CreatedAt: blob.Uploaded,
|
||||
}
|
||||
evt.ID = evt.GetID()
|
||||
es.Store.SaveEvent(ctx, evt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es EventStoreBlobIndexWrapper) List(ctx context.Context, pubkey string) (chan BlobDescriptor, error) {
|
||||
ech, err := es.Store.QueryEvents(ctx, nostr.Filter{Authors: []string{pubkey}, Kinds: []int{24242}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan BlobDescriptor)
|
||||
|
||||
go func() {
|
||||
for evt := range ech {
|
||||
ch <- es.parseEvent(evt)
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (es EventStoreBlobIndexWrapper) Get(ctx context.Context, sha256 string) (*BlobDescriptor, error) {
|
||||
ech, err := es.Store.QueryEvents(ctx, nostr.Filter{Tags: nostr.TagMap{"x": []string{sha256}}, Kinds: []int{24242}, Limit: 1})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
evt := <-ech
|
||||
if evt != nil {
|
||||
bd := es.parseEvent(evt)
|
||||
return &bd, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (es EventStoreBlobIndexWrapper) Delete(ctx context.Context, sha256 string, pubkey string) error {
|
||||
ech, err := es.Store.QueryEvents(ctx, nostr.Filter{Authors: []string{pubkey}, Tags: nostr.TagMap{"x": []string{sha256}}, Kinds: []int{24242}, Limit: 1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evt := <-ech
|
||||
if evt != nil {
|
||||
return es.Store.DeleteEvent(ctx, evt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es EventStoreBlobIndexWrapper) parseEvent(evt *nostr.Event) BlobDescriptor {
|
||||
hhash := evt.Tags[0][1]
|
||||
mimetype := evt.Tags[1][1]
|
||||
ext := getExtension(mimetype)
|
||||
size, _ := strconv.Atoi(evt.Tags[2][1])
|
||||
|
||||
return BlobDescriptor{
|
||||
Owner: evt.PubKey,
|
||||
Uploaded: evt.CreatedAt,
|
||||
URL: es.ServiceURL + "/" + hhash + ext,
|
||||
SHA256: hhash,
|
||||
Type: mimetype,
|
||||
Size: size,
|
||||
}
|
||||
}
|
||||
367
khatru/blossom/handlers.go
Normal file
367
khatru/blossom/handlers.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/liamg/magic"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request) {
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
if auth == nil {
|
||||
blossomError(w, "missing \"Authorization\" header", 401)
|
||||
return
|
||||
}
|
||||
if auth.Tags.FindWithValue("t", "upload") == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
|
||||
mimetype := r.Header.Get("X-Content-Type")
|
||||
exts, _ := mime.ExtensionsByType(mimetype)
|
||||
var ext string
|
||||
if len(exts) > 0 {
|
||||
ext = exts[0]
|
||||
}
|
||||
|
||||
// get the file size from the incoming header
|
||||
size, _ := strconv.Atoi(r.Header.Get("X-Content-Length"))
|
||||
|
||||
for _, rb := range bs.RejectUpload {
|
||||
reject, reason, code := rb(r.Context(), auth, size, ext)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, "invalid \"Authorization\": "+err.Error(), 404)
|
||||
return
|
||||
}
|
||||
if auth == nil {
|
||||
blossomError(w, "missing \"Authorization\" header", 401)
|
||||
return
|
||||
}
|
||||
if auth.Tags.FindWithValue("t", "upload") == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
|
||||
// get the file size from the incoming header
|
||||
size, _ := strconv.Atoi(r.Header.Get("Content-Length"))
|
||||
if size == 0 {
|
||||
blossomError(w, "missing \"Content-Length\" header", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// read first bytes of upload so we can find out the filetype
|
||||
b := make([]byte, min(50, size), size)
|
||||
if n, err := r.Body.Read(b); err != nil && n != size {
|
||||
blossomError(w, "failed to read initial bytes of upload body: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
var ext string
|
||||
if ft, _ := magic.Lookup(b); ft != nil {
|
||||
ext = "." + ft.Extension
|
||||
} else {
|
||||
// if we can't find, use the filetype given by the upload header
|
||||
mimetype := r.Header.Get("Content-Type")
|
||||
ext = getExtension(mimetype)
|
||||
}
|
||||
|
||||
// run the reject hooks
|
||||
for _, ru := range bs.RejectUpload {
|
||||
reject, reason, code := ru(r.Context(), auth, size, ext)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// if it passes then we have to read the entire thing into memory so we can compute the sha256
|
||||
for {
|
||||
var n int
|
||||
n, err = r.Body.Read(b[len(b):cap(b)])
|
||||
b = b[:len(b)+n]
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(b) == cap(b) {
|
||||
// add more capacity (let append pick how much)
|
||||
// if Content-Length was correct we shouldn't reach this
|
||||
b = append(b, 0)[:len(b)]
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
blossomError(w, "failed to read upload body: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(b)
|
||||
hhash := hex.EncodeToString(hash[:])
|
||||
|
||||
// keep track of the blob descriptor
|
||||
bd := BlobDescriptor{
|
||||
URL: bs.ServiceURL + "/" + hhash + ext,
|
||||
SHA256: hhash,
|
||||
Size: len(b),
|
||||
Type: mime.TypeByExtension(ext),
|
||||
Uploaded: nostr.Now(),
|
||||
}
|
||||
if err := bs.Store.Keep(r.Context(), bd, auth.PubKey); err != nil {
|
||||
blossomError(w, "failed to save event: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// save actual blob
|
||||
for _, sb := range bs.StoreBlob {
|
||||
if err := sb(r.Context(), hhash, b); err != nil {
|
||||
blossomError(w, "failed to save: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// return response
|
||||
json.NewEncoder(w).Encode(bd)
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
|
||||
spl := strings.SplitN(r.URL.Path, ".", 2)
|
||||
hhash := spl[0]
|
||||
if len(hhash) != 65 {
|
||||
blossomError(w, "invalid /<sha256>[.ext] path", 400)
|
||||
return
|
||||
}
|
||||
hhash = hhash[1:]
|
||||
|
||||
// check for an authorization tag, if any
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// if there is one, we check if it has the extra requirements
|
||||
if auth != nil {
|
||||
if auth.Tags.FindWithValue("t", "get") == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
|
||||
if auth.Tags.FindWithValue("x", hhash) == nil &&
|
||||
auth.Tags.FindWithValue("server", bs.ServiceURL) == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, rg := range bs.RejectGet {
|
||||
reject, reason, code := rg(r.Context(), auth, hhash)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var ext string
|
||||
if len(spl) == 2 {
|
||||
ext = "." + spl[1]
|
||||
}
|
||||
|
||||
for _, lb := range bs.LoadBlob {
|
||||
reader, _ := lb(r.Context(), hhash)
|
||||
if reader != nil {
|
||||
// use unix epoch as the time if we can't find the descriptor
|
||||
// as described in the http.ServeContent documentation
|
||||
t := time.Unix(0, 0)
|
||||
descriptor, err := bs.Store.Get(r.Context(), hhash)
|
||||
if err == nil && descriptor != nil {
|
||||
t = descriptor.Uploaded.Time()
|
||||
}
|
||||
w.Header().Set("ETag", hhash)
|
||||
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
|
||||
http.ServeContent(w, r, hhash+ext, t, reader)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
blossomError(w, "file not found", 404)
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleHasBlob(w http.ResponseWriter, r *http.Request) {
|
||||
spl := strings.SplitN(r.URL.Path, ".", 2)
|
||||
hhash := spl[0]
|
||||
if len(hhash) != 65 {
|
||||
blossomError(w, "invalid /<sha256>[.ext] path", 400)
|
||||
return
|
||||
}
|
||||
hhash = hhash[1:]
|
||||
|
||||
bd, err := bs.Store.Get(r.Context(), hhash)
|
||||
if err != nil {
|
||||
blossomError(w, "failed to query: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if bd == nil {
|
||||
blossomError(w, "file not found", 404)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
// check for an authorization tag, if any
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// if there is one, we check if it has the extra requirements
|
||||
if auth != nil {
|
||||
if auth.Tags.FindWithValue("t", "list") == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pubkey := r.URL.Path[6:]
|
||||
|
||||
for _, rl := range bs.RejectList {
|
||||
reject, reason, code := rl(r.Context(), auth, pubkey)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ch, err := bs.Store.List(r.Context(), pubkey)
|
||||
if err != nil {
|
||||
blossomError(w, "failed to query: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte{'['})
|
||||
enc := json.NewEncoder(w)
|
||||
first := true
|
||||
for bd := range ch {
|
||||
if !first {
|
||||
w.Write([]byte{','})
|
||||
} else {
|
||||
first = false
|
||||
}
|
||||
enc.Encode(bd)
|
||||
}
|
||||
w.Write([]byte{']'})
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
auth, err := readAuthorization(r)
|
||||
if err != nil {
|
||||
blossomError(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
if auth != nil {
|
||||
if auth.Tags.FindWithValue("t", "delete") == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
spl := strings.SplitN(r.URL.Path, ".", 2)
|
||||
hhash := spl[0]
|
||||
if len(hhash) != 65 {
|
||||
blossomError(w, "invalid /<sha256>[.ext] path", 400)
|
||||
return
|
||||
}
|
||||
hhash = hhash[1:]
|
||||
if auth.Tags.FindWithValue("x", hhash) == nil &&
|
||||
auth.Tags.FindWithValue("server", bs.ServiceURL) == nil {
|
||||
blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403)
|
||||
return
|
||||
}
|
||||
|
||||
// should we accept this delete?
|
||||
for _, rd := range bs.RejectDelete {
|
||||
reject, reason, code := rd(r.Context(), auth, hhash)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// delete the entry that links this blob to this author
|
||||
if err := bs.Store.Delete(r.Context(), hhash, auth.PubKey); err != nil {
|
||||
blossomError(w, "delete of blob entry failed: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// we will actually only delete the file if no one else owns it
|
||||
if bd, err := bs.Store.Get(r.Context(), hhash); err == nil && bd == nil {
|
||||
for _, del := range bs.DeleteBlob {
|
||||
if err := del(r.Context(), hhash); err != nil {
|
||||
blossomError(w, "failed to delete blob: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleReport(w http.ResponseWriter, r *http.Request) {
|
||||
var body []byte
|
||||
_, err := r.Body.Read(body)
|
||||
if err != nil {
|
||||
blossomError(w, "can't read request body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
var evt *nostr.Event
|
||||
if err := json.Unmarshal(body, evt); err != nil {
|
||||
blossomError(w, "can't parse event", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if isValid, _ := evt.CheckSignature(); !isValid {
|
||||
blossomError(w, "invalid report event is provided", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if evt.Kind != nostr.KindReporting {
|
||||
blossomError(w, "invalid report event is provided", 400)
|
||||
return
|
||||
}
|
||||
|
||||
for _, rr := range bs.ReceiveReport {
|
||||
if err := rr(r.Context(), evt); err != nil {
|
||||
blossomError(w, "failed to receive report: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleMirror(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleNegentropy(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
78
khatru/blossom/server.go
Normal file
78
khatru/blossom/server.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type BlossomServer struct {
|
||||
ServiceURL string
|
||||
Store BlobIndex
|
||||
|
||||
StoreBlob []func(ctx context.Context, sha256 string, body []byte) error
|
||||
LoadBlob []func(ctx context.Context, sha256 string) (io.ReadSeeker, error)
|
||||
DeleteBlob []func(ctx context.Context, sha256 string) error
|
||||
ReceiveReport []func(ctx context.Context, reportEvt *nostr.Event) error
|
||||
|
||||
RejectUpload []func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int)
|
||||
RejectGet []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
|
||||
RejectList []func(ctx context.Context, auth *nostr.Event, pubkey string) (bool, string, int)
|
||||
RejectDelete []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
|
||||
}
|
||||
|
||||
func New(rl *khatru.Relay, serviceURL string) *BlossomServer {
|
||||
bs := &BlossomServer{
|
||||
ServiceURL: serviceURL,
|
||||
}
|
||||
|
||||
base := rl.Router()
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/upload" {
|
||||
if r.Method == "PUT" {
|
||||
bs.handleUpload(w, r)
|
||||
return
|
||||
} else if r.Method == "HEAD" {
|
||||
bs.handleUploadCheck(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/list/") && r.Method == "GET" {
|
||||
bs.handleList(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if (len(r.URL.Path) == 65 || strings.Index(r.URL.Path, ".") == 65) && strings.Index(r.URL.Path[1:], "/") == -1 {
|
||||
if r.Method == "HEAD" {
|
||||
bs.handleHasBlob(w, r)
|
||||
return
|
||||
} else if r.Method == "GET" {
|
||||
bs.handleGetBlob(w, r)
|
||||
return
|
||||
} else if r.Method == "DELETE" {
|
||||
bs.handleDelete(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if r.URL.Path == "/report" {
|
||||
if r.Method == "PUT" {
|
||||
bs.handleReport(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
base.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
rl.SetRouter(mux)
|
||||
|
||||
return bs
|
||||
}
|
||||
37
khatru/blossom/utils.go
Normal file
37
khatru/blossom/utils.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package blossom
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func blossomError(w http.ResponseWriter, msg string, code int) {
|
||||
w.Header().Add("X-Reason", msg)
|
||||
w.WriteHeader(code)
|
||||
}
|
||||
|
||||
func getExtension(mimetype string) string {
|
||||
if mimetype == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch mimetype {
|
||||
case "image/jpeg":
|
||||
return ".jpg"
|
||||
case "image/gif":
|
||||
return ".gif"
|
||||
case "image/png":
|
||||
return ".png"
|
||||
case "image/webp":
|
||||
return ".webp"
|
||||
case "video/mp4":
|
||||
return ".mp4"
|
||||
}
|
||||
|
||||
exts, _ := mime.ExtensionsByType(mimetype)
|
||||
if len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user