Files
code-utils/fileapi.go
2026-08-15 17:18:00 +08:00

227 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// fileapi.go 调 nl-pms-api 的最小客户端:上传图片换取公开访问 URL
// 以及素材库的列表/删除透传(权限由服务端按 用户/团队角色 判定)。
// 服务器地址来自管理员下发的全局文件存储配置(见 filestorage.go
// 鉴权使用同步登录的 JWTsync_access_token不再使用 apiKey / 表单 userId。
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
"strings"
"time"
)
var fileAPIClient = &http.Client{Timeout: 30 * time.Second}
func (a *App) currentTeamID() int64 {
n, _ := strconv.ParseInt(strings.TrimSpace(a.store.Meta("current_team_id")), 10, 64)
if n < 0 {
return 0
}
return n
}
func (a *App) fileAPIBase() (FileStorageConfig, error) {
cfg := a.currentFileStorage()
if cfg.Mode != "server" || cfg.BaseURL == "" {
return cfg, errors.New("FILE_API_UNCONFIGURED")
}
return cfg, nil
}
func (a *App) syncBearerToken() (string, error) {
tok := ""
if a.store != nil {
tok = strings.TrimSpace(a.store.Meta("sync_access_token"))
}
if tok == "" {
return "", errors.New("SYNC_NOT_LOGGED_IN")
}
return tok, nil
}
// fileAPIDo 对文件服务发请求401 时尝试 refresh 后重试一次。
func (a *App) fileAPIDo(req *http.Request) (*http.Response, error) {
tok, e := a.syncBearerToken()
if e != nil {
return nil, e
}
req.Header.Set("Authorization", "Bearer "+tok)
resp, e := fileAPIClient.Do(req)
if e != nil {
return nil, e
}
if resp.StatusCode == http.StatusUnauthorized {
resp.Body.Close()
if a.apiTryRefresh() != nil {
return nil, errors.New("UNAUTHORIZED")
}
tok, e = a.syncBearerToken()
if e != nil {
return nil, e
}
// 重建请求体不可行时由调用方重试;此处仅对无 body 的 GET/DELETE 重试。
req.Header.Set("Authorization", "Bearer "+tok)
return fileAPIClient.Do(req)
}
return resp, nil
}
func fileAPIStatusErr(status int) error {
switch status {
case http.StatusForbidden:
return errors.New("FILE_PERMISSION_DENIED")
case http.StatusNotFound:
return errors.New("FILE_NOT_FOUND")
default:
return errors.New("FILE_API_REQUEST_FAILED")
}
}
func (a *App) uploadImageToServer(data []byte, mime, kind string) (string, error) {
cfg, e := a.fileAPIBase()
if e != nil {
return "", e
}
if _, e := a.syncBearerToken(); e != nil {
return "", e
}
ext := ".jpg"
switch mime {
case "image/png":
ext = ".png"
case "image/gif":
ext = ".gif"
case "image/webp":
ext = ".webp"
}
doUpload := func() (string, error) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
fw, e := w.CreateFormFile("file", "img"+ext)
if e != nil {
return "", errors.New("IMAGE_UPLOAD_FAILED")
}
if _, e = fw.Write(data); e != nil {
return "", errors.New("IMAGE_UPLOAD_FAILED")
}
_ = w.WriteField("kind", kind)
if kind == "content" {
_ = w.WriteField("teamId", strconv.FormatInt(a.currentTeamID(), 10))
}
if e = w.Close(); e != nil {
return "", errors.New("IMAGE_UPLOAD_FAILED")
}
req, e := http.NewRequest(http.MethodPost, cfg.BaseURL+"/api/v1/files", &buf)
if e != nil {
return "", errors.New("IMAGE_UPLOAD_FAILED")
}
req.Header.Set("Content-Type", w.FormDataContentType())
tok, _ := a.syncBearerToken()
req.Header.Set("Authorization", "Bearer "+tok)
resp, e := fileAPIClient.Do(req)
if e != nil {
return "", errors.New("IMAGE_UPLOAD_FAILED")
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if resp.StatusCode == http.StatusUnauthorized {
if a.apiTryRefresh() == nil {
return "", errors.New("RETRY")
}
return "", errors.New("IMAGE_UPLOAD_FAILED")
}
if resp.StatusCode != http.StatusOK {
return "", errors.New("IMAGE_UPLOAD_FAILED")
}
var out struct {
URL string `json:"url"`
}
if json.Unmarshal(body, &out) != nil || out.URL == "" {
return "", errors.New("IMAGE_UPLOAD_FAILED")
}
return out.URL, nil
}
url, e := doUpload()
if e != nil && e.Error() == "RETRY" {
return doUpload()
}
return url, e
}
func (a *App) ListServerFiles(scope string, teamID int64, page int) (ServerFileList, error) {
if e := a.ready(); e != nil {
return ServerFileList{}, e
}
cfg, e := a.fileAPIBase()
if e != nil {
return ServerFileList{}, e
}
if a.syncUserID() <= 0 {
return ServerFileList{}, errors.New("SYNC_NOT_LOGGED_IN")
}
if page < 1 {
page = 1
}
u := fmt.Sprintf("%s/api/v1/files?scope=%s&teamId=%d&page=%d&pageSize=24",
cfg.BaseURL, scope, teamID, page)
req, e := http.NewRequest(http.MethodGet, u, nil)
if e != nil {
return ServerFileList{}, errors.New("FILE_API_REQUEST_FAILED")
}
resp, e := a.fileAPIDo(req)
if e != nil {
if e.Error() == "SYNC_OFFLINE" || e.Error() == "UNAUTHORIZED" {
return ServerFileList{}, errors.New("FILE_STORAGE_UNREACHABLE")
}
return ServerFileList{}, e
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
if resp.StatusCode != http.StatusOK {
return ServerFileList{}, fileAPIStatusErr(resp.StatusCode)
}
var out ServerFileList
if json.Unmarshal(body, &out) != nil {
return ServerFileList{}, errors.New("FILE_API_REQUEST_FAILED")
}
return out, nil
}
func (a *App) DeleteServerFile(id int64) error {
if e := a.ready(); e != nil {
return e
}
cfg, e := a.fileAPIBase()
if e != nil {
return e
}
if a.syncUserID() <= 0 {
return errors.New("SYNC_NOT_LOGGED_IN")
}
u := fmt.Sprintf("%s/api/v1/files/%d", cfg.BaseURL, id)
req, e := http.NewRequest(http.MethodDelete, u, nil)
if e != nil {
return errors.New("FILE_API_REQUEST_FAILED")
}
resp, e := a.fileAPIDo(req)
if e != nil {
if e.Error() == "SYNC_OFFLINE" || e.Error() == "UNAUTHORIZED" {
return errors.New("FILE_STORAGE_UNREACHABLE")
}
return e
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fileAPIStatusErr(resp.StatusCode)
}
return nil
}