1package storage
2
3// Copyright 2017 Microsoft Corporation
4//
5//  Licensed under the Apache License, Version 2.0 (the "License");
6//  you may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at
8//
9//      http://www.apache.org/licenses/LICENSE-2.0
10//
11//  Unless required by applicable law or agreed to in writing, software
12//  distributed under the License is distributed on an "AS IS" BASIS,
13//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14//  See the License for the specific language governing permissions and
15//  limitations under the License.
16
17import (
18	"errors"
19	"net/http"
20	"net/url"
21	"strconv"
22	"time"
23)
24
25// lease constants.
26const (
27	leaseHeaderPrefix = "x-ms-lease-"
28	headerLeaseID     = "x-ms-lease-id"
29	leaseAction       = "x-ms-lease-action"
30	leaseBreakPeriod  = "x-ms-lease-break-period"
31	leaseDuration     = "x-ms-lease-duration"
32	leaseProposedID   = "x-ms-proposed-lease-id"
33	leaseTime         = "x-ms-lease-time"
34
35	acquireLease = "acquire"
36	renewLease   = "renew"
37	changeLease  = "change"
38	releaseLease = "release"
39	breakLease   = "break"
40)
41
42// leasePut is common PUT code for the various acquire/release/break etc functions.
43func (b *Blob) leaseCommonPut(headers map[string]string, expectedStatus int, options *LeaseOptions) (http.Header, error) {
44	params := url.Values{"comp": {"lease"}}
45
46	if options != nil {
47		params = addTimeout(params, options.Timeout)
48		headers = mergeHeaders(headers, headersFromStruct(*options))
49	}
50	uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
51
52	resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
53	if err != nil {
54		return nil, err
55	}
56	defer drainRespBody(resp)
57
58	if err := checkRespCode(resp, []int{expectedStatus}); err != nil {
59		return nil, err
60	}
61
62	return resp.Header, nil
63}
64
65// LeaseOptions includes options for all operations regarding leasing blobs
66type LeaseOptions struct {
67	Timeout           uint
68	Origin            string     `header:"Origin"`
69	IfMatch           string     `header:"If-Match"`
70	IfNoneMatch       string     `header:"If-None-Match"`
71	IfModifiedSince   *time.Time `header:"If-Modified-Since"`
72	IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
73	RequestID         string     `header:"x-ms-client-request-id"`
74}
75
76// AcquireLease creates a lease for a blob
77// returns leaseID acquired
78// In API Versions starting on 2012-02-12, the minimum leaseTimeInSeconds is 15, the maximum
79// non-infinite leaseTimeInSeconds is 60. To specify an infinite lease, provide the value -1.
80// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
81func (b *Blob) AcquireLease(leaseTimeInSeconds int, proposedLeaseID string, options *LeaseOptions) (returnedLeaseID string, err error) {
82	headers := b.Container.bsc.client.getStandardHeaders()
83	headers[leaseAction] = acquireLease
84
85	if leaseTimeInSeconds == -1 {
86		// Do nothing, but don't trigger the following clauses.
87	} else if leaseTimeInSeconds > 60 || b.Container.bsc.client.apiVersion < "2012-02-12" {
88		leaseTimeInSeconds = 60
89	} else if leaseTimeInSeconds < 15 {
90		leaseTimeInSeconds = 15
91	}
92
93	headers[leaseDuration] = strconv.Itoa(leaseTimeInSeconds)
94
95	if proposedLeaseID != "" {
96		headers[leaseProposedID] = proposedLeaseID
97	}
98
99	respHeaders, err := b.leaseCommonPut(headers, http.StatusCreated, options)
100	if err != nil {
101		return "", err
102	}
103
104	returnedLeaseID = respHeaders.Get(http.CanonicalHeaderKey(headerLeaseID))
105
106	if returnedLeaseID != "" {
107		return returnedLeaseID, nil
108	}
109
110	return "", errors.New("LeaseID not returned")
111}
112
113// BreakLease breaks the lease for a blob
114// Returns the timeout remaining in the lease in seconds
115// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
116func (b *Blob) BreakLease(options *LeaseOptions) (breakTimeout int, err error) {
117	headers := b.Container.bsc.client.getStandardHeaders()
118	headers[leaseAction] = breakLease
119	return b.breakLeaseCommon(headers, options)
120}
121
122// BreakLeaseWithBreakPeriod breaks the lease for a blob
123// breakPeriodInSeconds is used to determine how long until new lease can be created.
124// Returns the timeout remaining in the lease in seconds
125// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
126func (b *Blob) BreakLeaseWithBreakPeriod(breakPeriodInSeconds int, options *LeaseOptions) (breakTimeout int, err error) {
127	headers := b.Container.bsc.client.getStandardHeaders()
128	headers[leaseAction] = breakLease
129	headers[leaseBreakPeriod] = strconv.Itoa(breakPeriodInSeconds)
130	return b.breakLeaseCommon(headers, options)
131}
132
133// breakLeaseCommon is common code for both version of BreakLease (with and without break period)
134func (b *Blob) breakLeaseCommon(headers map[string]string, options *LeaseOptions) (breakTimeout int, err error) {
135
136	respHeaders, err := b.leaseCommonPut(headers, http.StatusAccepted, options)
137	if err != nil {
138		return 0, err
139	}
140
141	breakTimeoutStr := respHeaders.Get(http.CanonicalHeaderKey(leaseTime))
142	if breakTimeoutStr != "" {
143		breakTimeout, err = strconv.Atoi(breakTimeoutStr)
144		if err != nil {
145			return 0, err
146		}
147	}
148
149	return breakTimeout, nil
150}
151
152// ChangeLease changes a lease ID for a blob
153// Returns the new LeaseID acquired
154// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
155func (b *Blob) ChangeLease(currentLeaseID string, proposedLeaseID string, options *LeaseOptions) (newLeaseID string, err error) {
156	headers := b.Container.bsc.client.getStandardHeaders()
157	headers[leaseAction] = changeLease
158	headers[headerLeaseID] = currentLeaseID
159	headers[leaseProposedID] = proposedLeaseID
160
161	respHeaders, err := b.leaseCommonPut(headers, http.StatusOK, options)
162	if err != nil {
163		return "", err
164	}
165
166	newLeaseID = respHeaders.Get(http.CanonicalHeaderKey(headerLeaseID))
167	if newLeaseID != "" {
168		return newLeaseID, nil
169	}
170
171	return "", errors.New("LeaseID not returned")
172}
173
174// ReleaseLease releases the lease for a blob
175// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
176func (b *Blob) ReleaseLease(currentLeaseID string, options *LeaseOptions) error {
177	headers := b.Container.bsc.client.getStandardHeaders()
178	headers[leaseAction] = releaseLease
179	headers[headerLeaseID] = currentLeaseID
180
181	_, err := b.leaseCommonPut(headers, http.StatusOK, options)
182	if err != nil {
183		return err
184	}
185
186	return nil
187}
188
189// RenewLease renews the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx
190func (b *Blob) RenewLease(currentLeaseID string, options *LeaseOptions) error {
191	headers := b.Container.bsc.client.getStandardHeaders()
192	headers[leaseAction] = renewLease
193	headers[headerLeaseID] = currentLeaseID
194
195	_, err := b.leaseCommonPut(headers, http.StatusOK, options)
196	if err != nil {
197		return err
198	}
199
200	return nil
201}
202