-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.go
More file actions
306 lines (262 loc) · 8.38 KB
/
client.go
File metadata and controls
306 lines (262 loc) · 8.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
package disk
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// Context management and timeout handling implemented
const API_URL = "https://cloud-api.yandex.net/v1/disk/"
type HttpMethod string
const (
GET HttpMethod = "GET"
POST HttpMethod = "POST"
PUT HttpMethod = "PUT"
PATCH HttpMethod = "PATCH"
DELETE HttpMethod = "DELETE"
)
// ClientConfig holds configuration options for the Client
type ClientConfig struct {
DefaultTimeout time.Duration // Default timeout for requests
MaxRetries int // Maximum number of retries (future use)
EnableDebugLogging bool // Enable debug logging (future use)
Logger *LoggerConfig // Logger configuration
}
// DefaultClientConfig returns a ClientConfig with sensible defaults
func DefaultClientConfig() *ClientConfig {
return &ClientConfig{
DefaultTimeout: 30 * time.Second,
MaxRetries: 3,
EnableDebugLogging: false,
Logger: DefaultLoggerConfig(),
}
}
type Client struct {
AccessToken string
HTTPClient *http.Client
Logger *DiskLogger
Config *ClientConfig
}
// NewWithConfig creates a new Client with custom configuration
func NewWithConfig(config *ClientConfig, token ...string) (*Client, error) {
if len(token) == 0 {
envToken := os.Getenv("YANDEX_DISK_ACCESS_TOKEN")
if envToken == "" {
return nil, errors.New("provide yandex disk access token")
}
token = append(token, envToken)
}
if config == nil {
config = DefaultClientConfig()
}
// Validate and sanitize token
sanitizedToken := strings.TrimSpace(token[0])
if sanitizedToken == "" {
return nil, errors.New("access token cannot be empty")
}
// Initialize logger
logger := NewLogger(config.Logger)
// Create HTTP client with secure TLS configuration
transport := &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
},
},
MaxIdleConns: 10,
MaxIdleConnsPerHost: 2,
IdleConnTimeout: 90 * time.Second,
}
return &Client{
AccessToken: sanitizedToken,
HTTPClient: &http.Client{
Timeout: config.DefaultTimeout,
Transport: transport,
},
Config: config,
Logger: logger,
}, nil
}
// New(token ...string) fetch token from OS env var if has not direct defined
// Uses default configuration for backward compatibility
func New(token ...string) (*Client, error) {
return NewWithConfig(nil, token...)
}
func (c *Client) doRequest(ctx context.Context, method HttpMethod, resource string, data io.Reader) (*http.Response, error) {
startTime := time.Now()
// Ensure we have a proper context
if ctx == nil {
ctx = context.Background()
}
var resp *http.Response
var err error
var body io.Reader
body = data
// Use configurable timeout from client config if no deadline is set
// This respects any existing context deadline while providing a fallback
if _, hasDeadline := ctx.Deadline(); !hasDeadline && c.Config != nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, c.Config.DefaultTimeout)
defer cancel()
} else if _, hasDeadline := ctx.Deadline(); !hasDeadline {
// Fallback to HTTP client timeout if no config is available
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, c.HTTPClient.Timeout)
defer cancel()
}
// Check if context is already cancelled before making the request
select {
case <-ctx.Done():
c.Logger.LogError("doRequest", ctx.Err())
return nil, fmt.Errorf("request cancelled: %w", ctx.Err())
default:
// Continue with request
}
if method == GET || method == DELETE {
body = nil
} else if data != nil {
// Limit request body size to prevent memory exhaustion
body = io.LimitReader(data, 100*1024*1024) // 100MB limit
}
requestURL := API_URL + resource
req, err := http.NewRequestWithContext(ctx, string(method), requestURL, body)
if err != nil {
c.Logger.LogError("create request", err)
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "OAuth "+c.AccessToken)
// Log request details
if c.Logger != nil {
headers := make(map[string]string)
for key, values := range req.Header {
if len(values) > 0 {
headers[key] = values[0]
}
}
c.Logger.LogRequest(string(method), requestURL, headers)
}
if resp, err = c.HTTPClient.Do(req); err != nil {
c.Logger.LogError("execute request", err)
// Provide more context about the error
if ctx.Err() != nil {
return nil, fmt.Errorf("request failed due to context: %w", ctx.Err())
}
return nil, fmt.Errorf("failed to execute request: %w", err)
}
// Log response details
if c.Logger != nil {
duration := time.Since(startTime)
contentLength := resp.ContentLength
if contentLength == -1 {
contentLength = 0
}
c.Logger.LogResponse(resp.StatusCode, contentLength, duration)
}
return resp, err
}
// WithTimeout creates a context with the specified timeout duration
func WithTimeout(timeout time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), timeout)
}
// WithDeadline creates a context with the specified deadline
func WithDeadline(deadline time.Time) (context.Context, context.CancelFunc) {
return context.WithDeadline(context.Background(), deadline)
}
// WithCancel creates a cancellable context
func WithCancel() (context.Context, context.CancelFunc) {
return context.WithCancel(context.Background())
}
// SetTimeout updates the default timeout for the client
func (c *Client) SetTimeout(timeout time.Duration) {
if c.Config == nil {
c.Config = DefaultClientConfig()
}
c.Config.DefaultTimeout = timeout
c.HTTPClient.Timeout = timeout
}
// GetTimeout returns the current default timeout for the client
func (c *Client) GetTimeout() time.Duration {
if c.Config != nil {
return c.Config.DefaultTimeout
}
return c.HTTPClient.Timeout
}
// SetLogLevel sets the minimum log level for the client
func (c *Client) SetLogLevel(level LogLevel) {
if c.Logger != nil {
c.Logger.SetLevel(level)
}
}
// SetVerbose enables or disables verbose logging
func (c *Client) SetVerbose(verbose bool) {
if c.Logger != nil {
c.Logger.SetVerbose(verbose)
}
if c.Config != nil {
c.Config.EnableDebugLogging = verbose
}
}
// SetLogOutput changes the log output destination
func (c *Client) SetLogOutput(output io.Writer) {
if c.Logger != nil {
c.Logger.SetOutput(output)
}
}
// handleResponse provides centralized response handling with consistent error management
func (c *Client) handleResponse(resp *http.Response, expectedCodes []int) (*ErrorResponse, error) {
if len(expectedCodes) == 0 {
expectedCodes = []int{200}
}
// Check if status code is expected
for _, code := range expectedCodes {
if resp.StatusCode == code {
return nil, nil // Success
}
}
// Handle error response
var errorResponse ErrorResponse
if resp.Body != nil {
decoder := json.NewDecoder(resp.Body)
if decodeErr := decoder.Decode(&errorResponse); decodeErr != nil {
// If we can't decode the error response, create a generic one
errorResponse = ErrorResponse{
Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, http.StatusText(resp.StatusCode)),
Description: fmt.Sprintf("Failed to decode error response: %v", decodeErr),
}
}
} else {
errorResponse = ErrorResponse{
Error: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, http.StatusText(resp.StatusCode)),
}
}
return &errorResponse, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, errorResponse.Error)
}
// safeDecodeJSON safely decodes JSON response with proper error handling for partial responses
func (c *Client) safeDecodeJSON(resp *http.Response, target interface{}) error {
if resp.Body == nil {
return fmt.Errorf("response body is nil")
}
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(target); err != nil {
// Check if this is a partial response or connection error
if err.Error() == "EOF" {
return fmt.Errorf("partial response received: connection may have been interrupted")
}
if err.Error() == "unexpected EOF" {
return fmt.Errorf("incomplete response received: connection interrupted during transfer")
}
return fmt.Errorf("failed to decode response: %w", err)
}
return nil
}