Files
code-utils/fileapi.go
2026-08-14 07:52:01 +08:00

184 lines
5.5 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
// 请求由 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/adminscope=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
}