feat: add tenant entry registration APIs

This commit is contained in:
richjohnrambo
2026-08-24 15:08:47 +08:00
parent ed9413d923
commit e61058ec03
16 changed files with 1174 additions and 0 deletions
+45
View File
@@ -153,6 +153,29 @@ func (fh *fshandler) Upload(fdef *types.FileDef, file io.Reader) (string, int64,
return fh.ServeURL + fname, size, nil
}
func (fh *fshandler) UploadPublic(fdef *media.PublicFileDef, file io.Reader) (string, int64, error) {
location, err := publicFileLocation(fh.FileUploadDirectory, fdef.Purpose, fdef.ID, fdef.CreatedAt)
if err != nil {
return "", 0, err
}
if err = os.MkdirAll(filepath.Dir(location), 0777); err != nil {
return "", 0, err
}
outfile, err := os.Create(location)
if err != nil {
return "", 0, err
}
size, err := io.Copy(outfile, file)
outfile.Close()
if err != nil {
os.Remove(location)
return "", 0, err
}
fdef.Location = location
fdef.ETag = etagFromPath(location)
return location, size, nil
}
// Download processes request for file download.
// The returned ReadSeekCloser must be closed after use.
func (fh *fshandler) Download(tenantID types.TenantID, url string) (*types.FileDef, media.ReadSeekCloser, error) {
@@ -226,6 +249,28 @@ func tenantFileLocation(root string, tenantID types.TenantID, fileID string, cre
createdAt.Format("2006"), createdAt.Format("01"), fileID), nil
}
func publicFileLocation(root, purpose, fileID string, createdAt time.Time) (string, error) {
purpose = cleanPublicPurpose(purpose)
fileID = strings.Trim(fileID, `/\`)
if root == "" || purpose == "" || fileID == "" {
return "", types.ErrMalformed
}
if createdAt.IsZero() {
createdAt = time.Now().UTC()
}
return filepath.Join(root, "public", purpose, createdAt.Format("2006"), createdAt.Format("01"), fileID), nil
}
func cleanPublicPurpose(purpose string) string {
purpose = strings.Trim(strings.ToLower(strings.TrimSpace(purpose)), `/\`)
return strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' || r == '-' {
return r
}
return -1
}, purpose)
}
func init() {
store.RegisterMediaHandler(handlerName, &fshandler{})
}
+14
View File
@@ -9,6 +9,7 @@ import (
"path"
"regexp"
"strings"
"time"
"slices"
@@ -45,6 +46,19 @@ type Handler interface {
GetIdFromUrl(url string) types.Uid
}
type PublicFileDef struct {
ID string
Purpose string
MimeType string
CreatedAt time.Time
Location string
ETag string
}
type PublicUploader interface {
UploadPublic(fdef *PublicFileDef, file io.Reader) (string, int64, error)
}
type AllowedOrigin struct {
Origin string
URL url.URL
+44
View File
@@ -262,6 +262,29 @@ func (oh *ossHandler) Upload(fdef *types.FileDef, file io.Reader) (string, int64
return oh.conf.ServeURL + fname, rc.count, nil
}
func (oh *ossHandler) UploadPublic(fdef *media.PublicFileDef, file io.Reader) (string, int64, error) {
key, err := publicObjectKey(fdef.Purpose, fdef.ID, fdef.CreatedAt)
if err != nil {
return "", 0, err
}
rc := readerCounter{reader: file}
options := []aliyunoss.Option{aliyunoss.CacheControl(oh.conf.CacheControl)}
if fdef.MimeType != "" {
options = append(options, aliyunoss.ContentType(fdef.MimeType))
}
if err = oh.bucket.PutObject(key, &rc, options...); err != nil {
return "", 0, err
}
fdef.Location = key
fdef.ETag = etagFromLocation(key)
if meta, err := oh.bucket.GetObjectDetailedMeta(key); err == nil {
if etag := strings.Trim(meta.Get("ETag"), "\""); etag != "" {
fdef.ETag = etag
}
}
return key, rc.count, nil
}
// Download is unsupported because OSS objects are served through signed redirects.
func (oh *ossHandler) Download(tenantID types.TenantID, url string) (*types.FileDef, media.ReadSeekCloser, error) {
return nil, nil, types.ErrUnsupported
@@ -308,6 +331,27 @@ func tenantObjectKey(tenantID types.TenantID, fileID string, createdAt time.Time
createdAt.Format("2006"), createdAt.Format("01"), fileID), nil
}
func publicObjectKey(purpose, fileID string, createdAt time.Time) (string, error) {
purpose = cleanPublicPurpose(purpose)
if purpose == "" || fileID == "" {
return "", types.ErrMalformed
}
if createdAt.IsZero() {
createdAt = time.Now().UTC()
}
return path.Join("public", purpose, createdAt.Format("2006"), createdAt.Format("01"), strings.Trim(fileID, "/")), nil
}
func cleanPublicPurpose(purpose string) string {
purpose = strings.Trim(strings.ToLower(strings.TrimSpace(purpose)), "/")
return strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' || r == '-' {
return r
}
return -1
}, purpose)
}
func etagFromLocation(location string) string {
hasher := fnv.New128()
hasher.Write([]byte(location))