Files
wehub-resource-sync e904b667c6
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:59:26 +08:00

268 lines
7.7 KiB
Go

// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package paddleocr
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
)
type SaveResourceOption func(*saveResourceOptions)
type saveResourceOptions struct {
overwrite bool
}
func WithOverwrite(overwrite bool) SaveResourceOption {
return func(o *saveResourceOptions) {
o.overwrite = overwrite
}
}
// SaveResource downloads one result resource URL.
func (c *Client) SaveResource(ctx context.Context, resourceURL, dest string, opts ...SaveResourceOption) (string, error) {
if resourceURL == "" {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "resource URL is required"}}
}
if dest == "" {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination path is required"}}
}
options := saveResourceOptions{}
for _, opt := range opts {
opt(&options)
}
savedPath, err := resolveSavePath(resourceURL, dest)
if err != nil {
return "", err
}
parent := filepath.Dir(savedPath)
if st, err := os.Stat(parent); err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", &FileNotFoundError{Path: parent, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination parent not found: " + parent, Cause: err}}
}
return "", err
} else if !st.IsDir() {
return "", &FileNotFoundError{Path: parent, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination parent is not a directory: " + parent}}
}
if !options.overwrite {
if _, err := os.Stat(savedPath); err == nil {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination already exists: " + savedPath}}
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
return "", err
}
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, resourceURL, nil)
if err != nil {
return "", &NetworkError{PaddleOCRAPIError{Message: err.Error(), Cause: err}}
}
resp, err := c.httpClient.Do(req)
if err != nil {
return "", classifyHTTPError(err)
}
defer resp.Body.Close()
if err := raiseForResourceResponse(resp); err != nil {
return "", err
}
tmp, err := os.CreateTemp(parent, "."+filepath.Base(savedPath)+".tmp-*")
if err != nil {
return "", err
}
tmpPath := tmp.Name()
cleanupTemp := true
defer func() {
if cleanupTemp {
_ = os.Remove(tmpPath)
}
}()
if _, err := io.Copy(tmp, resp.Body); err != nil {
_ = tmp.Close()
return "", err
}
if err := tmp.Close(); err != nil {
return "", err
}
if options.overwrite {
if err := os.Rename(tmpPath, savedPath); err != nil {
return "", err
}
} else {
if err := os.Link(tmpPath, savedPath); err != nil {
if errors.Is(err, os.ErrExist) {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination already exists: " + savedPath, Cause: err}}
}
return "", err
}
if err := os.Remove(tmpPath); err != nil {
return "", err
}
}
cleanupTemp = false
return savedPath, nil
}
func (c *Client) SaveOCRResultResources(ctx context.Context, result *OCRResult, destDir string, opts ...SaveResourceOption) ([]string, error) {
if result == nil {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "OCR result is required"}}
}
if err := requireExistingDirectory(destDir); err != nil {
return nil, err
}
saved := make([]string, 0, len(result.Pages))
for i, page := range result.Pages {
if page.OCRImageURL == "" {
continue
}
name := fmt.Sprintf("ocr-page-%d%s", i+1, safeResourceExtension(page.OCRImageURL))
if err := validateResultResourceFilename(name); err != nil {
return nil, err
}
path, err := c.SaveResource(ctx, page.OCRImageURL, filepath.Join(destDir, name), opts...)
if err != nil {
return nil, err
}
saved = append(saved, path)
}
return saved, nil
}
func (c *Client) SaveDocumentParsingResultResources(ctx context.Context, result *DocParsingResult, destDir string, opts ...SaveResourceOption) ([]string, error) {
if result == nil {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "document parsing result is required"}}
}
if err := requireExistingDirectory(destDir); err != nil {
return nil, err
}
var saved []string
for _, page := range result.Pages {
paths, err := c.saveNamedResourceMap(ctx, page.MarkdownImages, destDir, opts...)
if err != nil {
return nil, err
}
saved = append(saved, paths...)
paths, err = c.saveNamedResourceMap(ctx, page.OutputImages, destDir, opts...)
if err != nil {
return nil, err
}
saved = append(saved, paths...)
}
return saved, nil
}
func (c *Client) saveNamedResourceMap(ctx context.Context, resources map[string]string, destDir string, opts ...SaveResourceOption) ([]string, error) {
keys := make([]string, 0, len(resources))
for key := range resources {
keys = append(keys, key)
}
sort.Strings(keys)
saved := make([]string, 0, len(keys))
for _, key := range keys {
resourceURL := resources[key]
if resourceURL == "" {
continue
}
name, err := resultResourceFilename(key)
if err != nil {
return nil, err
}
path, err := c.SaveResource(ctx, resourceURL, filepath.Join(destDir, name), opts...)
if err != nil {
return nil, err
}
saved = append(saved, path)
}
return saved, nil
}
func resolveSavePath(resourceURL, dest string) (string, error) {
if st, err := os.Stat(dest); err == nil && st.IsDir() {
name := "resource"
u, parseErr := url.Parse(resourceURL)
if parseErr == nil {
if base := path.Base(u.Path); base != "." && base != "/" && base != "" {
name = base
}
}
return filepath.Join(dest, name), nil
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
return "", err
}
return dest, nil
}
func raiseForResourceResponse(resp *http.Response) error {
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
body, _ := io.ReadAll(resp.Body)
return &APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: string(body)}}
}
func requireExistingDirectory(destDir string) error {
if destDir == "" {
return &InvalidRequestError{PaddleOCRAPIError{Message: "destination directory is required"}}
}
st, err := os.Stat(destDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return &FileNotFoundError{Path: destDir, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination directory not found: " + destDir, Cause: err}}
}
return err
}
if !st.IsDir() {
return &FileNotFoundError{Path: destDir, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination is not a directory: " + destDir}}
}
return nil
}
func resultResourceFilename(key string) (string, error) {
if err := validateResultResourceFilename(key); err != nil {
return "", err
}
return key, nil
}
func validateResultResourceFilename(name string) error {
if name == "" || name == "." || name == ".." || filepath.IsAbs(name) || path.IsAbs(name) || strings.ContainsAny(name, `/\`) {
return &InvalidRequestError{PaddleOCRAPIError{Message: "unsafe resource filename: " + name}}
}
return nil
}
func safeResourceExtension(resourceURL string) string {
u, err := url.Parse(resourceURL)
if err != nil {
return ""
}
ext := path.Ext(path.Base(u.Path))
if len(ext) <= 1 {
return ""
}
if err := validateResultResourceFilename("resource" + ext); err != nil {
return ""
}
return ext
}