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 }