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
|
// SPDX-FileCopyrightText: 2018 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0
package schwift
import (
"context"
"encoding/json"
"fmt"
"net/http"
"regexp"
"sync"
)
// Account represents a Swift account. Instances are usually obtained by
// connecting to a backend (see package-level documentation), or by traversing
// upwards from a container with Container.Account().
type Account struct {
backend Backend
// URL parts
baseURL string
name string
// cache
headers *AccountHeaders
headersMutex sync.Mutex
caps *Capabilities
modifyCaps func(*Capabilities)
capsMutex sync.Mutex
}
// IsEqualTo returns true if both Account instances refer to the same account.
func (a *Account) IsEqualTo(other *Account) bool {
return other.baseURL == a.baseURL && other.name == a.name
}
var endpointURLRegexp = regexp.MustCompile(`^(.*/)v1/(.*)/$`)
// InitializeAccount takes something that implements the Backend interface, and
// returns the Account instance corresponding to the account/project that this
// backend is connected to.
func InitializeAccount(backend Backend) (*Account, error) {
match := endpointURLRegexp.FindStringSubmatch(backend.EndpointURL())
if match == nil {
return nil, fmt.Errorf(`schwift.InitializeAccount(): invalid Swift endpoint URL: cannot find "/v1/" in %q`, backend.EndpointURL())
}
return &Account{
backend: backend,
baseURL: match[1],
name: match[2],
}, nil
}
// SwitchAccount returns a handle to a different account on the same server. Note
// that you need reseller permissions to access accounts other than that where
// you originally authenticated. This method does not check whether the account
// actually exists.
//
// The account name is usually the Keystone project ID with an additional "AUTH_"
// prefix.
func (a *Account) SwitchAccount(accountName string) *Account {
newEndpointURL := a.baseURL + "v1/" + accountName + "/"
return &Account{
backend: a.backend.Clone(newEndpointURL),
baseURL: a.baseURL,
name: accountName,
}
}
// Name returns the name of the account (usually the prefix "AUTH_" followed by
// the Keystone project ID).
func (a *Account) Name() string {
return a.name
}
// Backend returns the backend which is used to make requests against this
// account.
func (a *Account) Backend() Backend {
return a.backend
}
// Headers returns the AccountHeaders for this account. If the AccountHeaders
// has not been cached yet, a HEAD request is issued on the account.
//
// This operation fails with http.StatusNotFound if the account does not exist.
func (a *Account) Headers(ctx context.Context) (AccountHeaders, error) {
a.headersMutex.Lock()
defer a.headersMutex.Unlock()
if a.headers != nil {
return *a.headers, nil
}
resp, err := Request{
Method: http.MethodHead,
ExpectStatusCodes: []int{204},
}.Do(ctx, a.backend)
if err != nil {
return AccountHeaders{}, err
}
defer resp.Body.Close()
headers := AccountHeaders{headersFromHTTP(resp.Header)}
err = headers.Validate()
if err != nil {
return headers, err
}
a.headers = &headers
return *a.headers, nil
}
// Invalidate clears the internal cache of this Account instance. The next call
// to Headers() on this instance will issue a HEAD request on the account.
func (a *Account) Invalidate() {
a.headersMutex.Lock()
defer a.headersMutex.Unlock()
a.headers = nil
}
// Update updates the account using a POST request. The headers in the headers
// attribute take precedence over those in opts.Headers.
//
// A successful POST request implies Invalidate() since it may change metadata.
func (a *Account) Update(ctx context.Context, headers AccountHeaders, opts *RequestOptions) error {
resp, err := Request{
Method: http.MethodPost,
Options: cloneRequestOptions(opts, headers.Headers),
ExpectStatusCodes: []int{204},
}.Do(ctx, a.backend)
if err == nil {
a.Invalidate()
resp.Body.Close()
}
return err
}
// Create creates the account using a PUT request. This operation is only
// available to reseller admins, not to regular users.
//
// A successful PUT request implies Invalidate() since it may change metadata.
func (a *Account) Create(ctx context.Context, opts *RequestOptions) error {
resp, err := Request{
Method: http.MethodPut,
Options: opts,
ExpectStatusCodes: []int{201, 202},
DrainResponseBody: true,
}.Do(ctx, a.backend)
if err == nil {
a.Invalidate()
resp.Body.Close()
}
return err
}
// Containers returns a ContainerIterator that lists the containers in this
// account. The most common use case is:
//
// containers, err := account.Containers().Collect()
//
// You can extend this by configuring the iterator before collecting the results:
//
// iter := account.Containers()
// iter.Prefix = "test-"
// containers, err := iter.Collect()
//
// Or you can use a different iteration method:
//
// err := account.Containers().ForeachDetailed(func (ci ContainerInfo) error {
// log.Printf("container %s contains %d objects!\n",
// ci.Container.Name(), ci.ObjectCount)
// })
func (a *Account) Containers() *ContainerIterator {
return &ContainerIterator{Account: a}
}
// Capabilities queries the GET /info endpoint of the Swift server providing
// this account. Capabilities are cached, so the GET request will only be sent
// once during the first call to this method.
func (a *Account) Capabilities(ctx context.Context) (Capabilities, error) {
a.capsMutex.Lock()
defer a.capsMutex.Unlock()
if a.caps != nil {
return *a.caps, nil
}
buf, err := a.RawCapabilities(ctx)
if err != nil {
return Capabilities{}, err
}
var caps Capabilities
err = json.Unmarshal(buf, &caps)
if err != nil {
return caps, err
}
if a.modifyCaps != nil {
a.modifyCaps(&caps)
}
a.caps = &caps
return caps, nil
}
// ModifyReportedCapabilities allows the caller to tamper with the capabilities reported by [Account.Capabilities].
// This is necessary when dealing with non-compliant server implementations that do not report their capabilities correctly
// (e.g. Ceph does not report "tempurl.allowed_digests", which breaks [Object.TempURL]).
//
// This call impacts all calls to [Account.Capabilities] that are made after this call succeeds.
// Calls to [Account.RawCapabilities] are not affected by this modification.
// Calls to this method do not stack: If this method is called multiple times, only the last modification will take effect.
func (a *Account) ModifyReportedCapabilities(modify func(*Capabilities)) {
a.capsMutex.Lock()
defer a.capsMutex.Unlock()
a.caps = nil // to force refetch+modify during next Capabilities() call
a.modifyCaps = modify
}
// RawCapabilities queries the GET /info endpoint of the Swift server providing
// this account, and returns the response body. Unlike Account.Capabilities,
// this method does not employ any caching.
func (a *Account) RawCapabilities(ctx context.Context) ([]byte, error) {
// This method is the only one in Schwift that bypasses struct Request since
// the request URL is not below the endpoint URL.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"info", http.NoBody)
if err != nil {
return nil, err
}
resp, err := a.backend.Do(req)
if err != nil {
return nil, err
}
return collectResponseBody(resp)
}
|