227 lines
5.8 KiB
Go
227 lines
5.8 KiB
Go
package main
|
||
|
||
// fileapi.go 调 nl-pms-api 的最小客户端:上传图片换取公开访问 URL,
|
||
// 以及素材库的列表/删除透传(权限由服务端按 用户/团队角色 判定)。
|
||
// 服务器地址来自管理员下发的全局文件存储配置(见 filestorage.go);
|
||
// 鉴权使用同步登录的 JWT(sync_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
|
||
}
|