chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
112
platform/internal/storage/local.go
Normal file
112
platform/internal/storage/local.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ObjectMeta struct {
|
||||
Key string `json:"key"`
|
||||
URL string `json:"url"`
|
||||
Filename string `json:"filename"`
|
||||
ContentType string `json:"content_type"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Put(ctx context.Context, tenantID int64, filename, contentType string, r io.Reader, size int64) (*ObjectMeta, error)
|
||||
Open(ctx context.Context, key string) (io.ReadCloser, *ObjectMeta, error)
|
||||
}
|
||||
|
||||
type LocalStore struct {
|
||||
Root string
|
||||
PublicBase string // e.g. http://127.0.0.1:8180/api/v1/storage
|
||||
}
|
||||
|
||||
func NewLocalStore(root, publicBase string) (*LocalStore, error) {
|
||||
if root == "" {
|
||||
root = "./data/uploads"
|
||||
}
|
||||
if err := os.MkdirAll(root, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LocalStore{Root: root, PublicBase: strings.TrimRight(publicBase, "/")}, nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) Put(_ context.Context, tenantID int64, filename, contentType string, r io.Reader, size int64) (*ObjectMeta, error) {
|
||||
ext := filepath.Ext(filename)
|
||||
if ext == "" {
|
||||
ext = guessExt(contentType)
|
||||
}
|
||||
id := make([]byte, 8)
|
||||
_, _ = rand.Read(id)
|
||||
key := fmt.Sprintf("t%d/%s/%s%s", tenantID, time.Now().UTC().Format("20060102"), hex.EncodeToString(id), ext)
|
||||
full := filepath.Join(s.Root, filepath.FromSlash(key))
|
||||
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := os.Create(full)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
n, err := io.Copy(f, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if size <= 0 {
|
||||
size = n
|
||||
}
|
||||
meta := &ObjectMeta{
|
||||
Key: key, Filename: filename, ContentType: contentType, Size: size, CreatedAt: time.Now().UTC(),
|
||||
URL: s.PublicBase + "/" + key,
|
||||
}
|
||||
// sidecar meta
|
||||
_ = os.WriteFile(full+".meta", []byte(fmt.Sprintf("%s\n%s\n%d\n%s", filename, contentType, size, meta.CreatedAt.Format(time.RFC3339))), 0o644)
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) Open(_ context.Context, key string) (io.ReadCloser, *ObjectMeta, error) {
|
||||
key = strings.TrimPrefix(key, "/")
|
||||
if strings.Contains(key, "..") {
|
||||
return nil, nil, fmt.Errorf("invalid key")
|
||||
}
|
||||
full := filepath.Join(s.Root, filepath.FromSlash(key))
|
||||
f, err := os.Open(full)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
st, _ := f.Stat()
|
||||
meta := &ObjectMeta{Key: key, Size: st.Size(), URL: s.PublicBase + "/" + key, CreatedAt: st.ModTime().UTC()}
|
||||
if b, err := os.ReadFile(full + ".meta"); err == nil {
|
||||
parts := strings.Split(string(b), "\n")
|
||||
if len(parts) >= 2 {
|
||||
meta.Filename = parts[0]
|
||||
meta.ContentType = parts[1]
|
||||
}
|
||||
}
|
||||
return f, meta, nil
|
||||
}
|
||||
|
||||
func guessExt(ct string) string {
|
||||
switch ct {
|
||||
case "image/png":
|
||||
return ".png"
|
||||
case "image/jpeg":
|
||||
return ".jpg"
|
||||
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
||||
return ".xlsx"
|
||||
case "text/csv":
|
||||
return ".csv"
|
||||
default:
|
||||
return ".bin"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user