184 lines
5.5 KiB
Go
184 lines
5.5 KiB
Go
|
|
package main
|
|||
|
|
|
|||
|
|
// fileapi.go 调 nl-pms-api 的最小客户端:上传图片换取公开访问 URL,
|
|||
|
|
// 以及素材库的列表/删除透传(权限由服务端按 用户/团队角色 判定)。
|
|||
|
|
// 服务器地址与密钥来自管理员下发的全局文件存储配置(见 filestorage.go)。
|
|||
|
|
// 请求由 Go 端发起(不走前端 fetch),天然绕开 webview 的 CORS 限制。
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"encoding/json"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"mime/multipart"
|
|||
|
|
"net/http"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var fileAPIClient = &http.Client{Timeout: 30 * time.Second}
|
|||
|
|
|
|||
|
|
// currentTeamID 返回客户端当前所在团队 id(未加入团队为 0)。
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// fileAPIBase 返回已启用的服务器存储地址(实时读全局配置,离线回本地缓存);
|
|||
|
|
// 未启用返回 FILE_API_UNCONFIGURED。
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// fileAPIStatusErr 把 nl-pms-api 的非 200 响应映射为客户端错误码。
|
|||
|
|
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")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// uploadImageToServer 把编码好的图片字节上传到 nl-pms-api,返回可公开访问的 http URL。
|
|||
|
|
// 归属:头像记个人(teamId=0),内容图记当前团队,素材库据此划定管理范围。
|
|||
|
|
// 未启用服务器存储时返回 FILE_API_UNCONFIGURED;失败不静默降级,由调用方向用户报错。
|
|||
|
|
func (a *App) uploadImageToServer(data []byte, mime, kind string) (string, error) {
|
|||
|
|
cfg, e := a.fileAPIBase()
|
|||
|
|
if e != nil {
|
|||
|
|
return "", e
|
|||
|
|
}
|
|||
|
|
ext := ".jpg"
|
|||
|
|
switch mime {
|
|||
|
|
case "image/png":
|
|||
|
|
ext = ".png"
|
|||
|
|
case "image/gif":
|
|||
|
|
ext = ".gif"
|
|||
|
|
case "image/webp":
|
|||
|
|
ext = ".webp"
|
|||
|
|
}
|
|||
|
|
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 id := a.syncUserID(); id > 0 {
|
|||
|
|
_ = w.WriteField("userId", strconv.FormatInt(id, 10))
|
|||
|
|
}
|
|||
|
|
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())
|
|||
|
|
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
|||
|
|
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.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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ListServerFiles 素材库分页列表。scope=mine 看自己;scope=team 看指定团队
|
|||
|
|
// (需为该团队 owner/admin);scope=all 看全部(仅云端账号 id=1)。
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
uid := a.syncUserID()
|
|||
|
|
if uid <= 0 {
|
|||
|
|
return ServerFileList{}, errors.New("SYNC_NOT_LOGGED_IN")
|
|||
|
|
}
|
|||
|
|
if page < 1 {
|
|||
|
|
page = 1
|
|||
|
|
}
|
|||
|
|
u := fmt.Sprintf("%s/api/v1/files?scope=%s&userId=%d&teamId=%d&page=%d&pageSize=24",
|
|||
|
|
cfg.BaseURL, scope, uid, teamID, page)
|
|||
|
|
req, e := http.NewRequest(http.MethodGet, u, nil)
|
|||
|
|
if e != nil {
|
|||
|
|
return ServerFileList{}, errors.New("FILE_API_REQUEST_FAILED")
|
|||
|
|
}
|
|||
|
|
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
|||
|
|
resp, e := fileAPIClient.Do(req)
|
|||
|
|
if e != nil {
|
|||
|
|
return ServerFileList{}, errors.New("FILE_STORAGE_UNREACHABLE")
|
|||
|
|
}
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DeleteServerFile 删除素材(记录+服务器磁盘文件)。服务端校验:本人、
|
|||
|
|
// 超管 id=1、或该文件归属团队的 owner/admin;越权返回 FILE_PERMISSION_DENIED。
|
|||
|
|
func (a *App) DeleteServerFile(id int64) error {
|
|||
|
|
if e := a.ready(); e != nil {
|
|||
|
|
return e
|
|||
|
|
}
|
|||
|
|
cfg, e := a.fileAPIBase()
|
|||
|
|
if e != nil {
|
|||
|
|
return e
|
|||
|
|
}
|
|||
|
|
uid := a.syncUserID()
|
|||
|
|
if uid <= 0 {
|
|||
|
|
return errors.New("SYNC_NOT_LOGGED_IN")
|
|||
|
|
}
|
|||
|
|
u := fmt.Sprintf("%s/api/v1/files/%d?userId=%d", cfg.BaseURL, id, uid)
|
|||
|
|
req, e := http.NewRequest(http.MethodDelete, u, nil)
|
|||
|
|
if e != nil {
|
|||
|
|
return errors.New("FILE_API_REQUEST_FAILED")
|
|||
|
|
}
|
|||
|
|
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
|||
|
|
resp, e := fileAPIClient.Do(req)
|
|||
|
|
if e != nil {
|
|||
|
|
return errors.New("FILE_STORAGE_UNREACHABLE")
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
if resp.StatusCode != http.StatusOK {
|
|||
|
|
return fileAPIStatusErr(resp.StatusCode)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|