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	"encoding/xml"
19	"errors"
20	"fmt"
21	"io"
22	"net/http"
23	"net/url"
24	"time"
25)
26
27// GetPageRangesResponse contains the response fields from
28// Get Page Ranges call.
29//
30// See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx
31type GetPageRangesResponse struct {
32	XMLName  xml.Name    `xml:"PageList"`
33	PageList []PageRange `xml:"PageRange"`
34}
35
36// PageRange contains information about a page of a page blob from
37// Get Pages Range call.
38//
39// See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx
40type PageRange struct {
41	Start int64 `xml:"Start"`
42	End   int64 `xml:"End"`
43}
44
45var (
46	errBlobCopyAborted    = errors.New("storage: blob copy is aborted")
47	errBlobCopyIDMismatch = errors.New("storage: blob copy id is a mismatch")
48)
49
50// PutPageOptions includes the options for a put page operation
51type PutPageOptions struct {
52	Timeout                           uint
53	LeaseID                           string     `header:"x-ms-lease-id"`
54	IfSequenceNumberLessThanOrEqualTo *int       `header:"x-ms-if-sequence-number-le"`
55	IfSequenceNumberLessThan          *int       `header:"x-ms-if-sequence-number-lt"`
56	IfSequenceNumberEqualTo           *int       `header:"x-ms-if-sequence-number-eq"`
57	IfModifiedSince                   *time.Time `header:"If-Modified-Since"`
58	IfUnmodifiedSince                 *time.Time `header:"If-Unmodified-Since"`
59	IfMatch                           string     `header:"If-Match"`
60	IfNoneMatch                       string     `header:"If-None-Match"`
61	RequestID                         string     `header:"x-ms-client-request-id"`
62}
63
64// WriteRange writes a range of pages to a page blob.
65// Ranges must be aligned with 512-byte boundaries and chunk must be of size
66// multiplies by 512.
67//
68// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Page
69func (b *Blob) WriteRange(blobRange BlobRange, bytes io.Reader, options *PutPageOptions) error {
70	if bytes == nil {
71		return errors.New("bytes cannot be nil")
72	}
73	return b.modifyRange(blobRange, bytes, options)
74}
75
76// ClearRange clears the given range in a page blob.
77// Ranges must be aligned with 512-byte boundaries and chunk must be of size
78// multiplies by 512.
79//
80// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Page
81func (b *Blob) ClearRange(blobRange BlobRange, options *PutPageOptions) error {
82	return b.modifyRange(blobRange, nil, options)
83}
84
85func (b *Blob) modifyRange(blobRange BlobRange, bytes io.Reader, options *PutPageOptions) error {
86	if blobRange.End < blobRange.Start {
87		return errors.New("the value for rangeEnd must be greater than or equal to rangeStart")
88	}
89	if blobRange.Start%512 != 0 {
90		return errors.New("the value for rangeStart must be a multiple of 512")
91	}
92	if blobRange.End%512 != 511 {
93		return errors.New("the value for rangeEnd must be a multiple of 512 - 1")
94	}
95
96	params := url.Values{"comp": {"page"}}
97
98	// default to clear
99	write := "clear"
100	var cl uint64
101
102	// if bytes is not nil then this is an update operation
103	if bytes != nil {
104		write = "update"
105		cl = (blobRange.End - blobRange.Start) + 1
106	}
107
108	headers := b.Container.bsc.client.getStandardHeaders()
109	headers["x-ms-blob-type"] = string(BlobTypePage)
110	headers["x-ms-page-write"] = write
111	headers["x-ms-range"] = blobRange.String()
112	headers["Content-Length"] = fmt.Sprintf("%v", cl)
113
114	if options != nil {
115		params = addTimeout(params, options.Timeout)
116		headers = mergeHeaders(headers, headersFromStruct(*options))
117	}
118	uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
119
120	resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, bytes, b.Container.bsc.auth)
121	if err != nil {
122		return err
123	}
124	defer drainRespBody(resp)
125	return checkRespCode(resp, []int{http.StatusCreated})
126}
127
128// GetPageRangesOptions includes the options for a get page ranges operation
129type GetPageRangesOptions struct {
130	Timeout          uint
131	Snapshot         *time.Time
132	PreviousSnapshot *time.Time
133	Range            *BlobRange
134	LeaseID          string `header:"x-ms-lease-id"`
135	RequestID        string `header:"x-ms-client-request-id"`
136}
137
138// GetPageRanges returns the list of valid page ranges for a page blob.
139//
140// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Get-Page-Ranges
141func (b *Blob) GetPageRanges(options *GetPageRangesOptions) (GetPageRangesResponse, error) {
142	params := url.Values{"comp": {"pagelist"}}
143	headers := b.Container.bsc.client.getStandardHeaders()
144
145	if options != nil {
146		params = addTimeout(params, options.Timeout)
147		params = addSnapshot(params, options.Snapshot)
148		if options.PreviousSnapshot != nil {
149			params.Add("prevsnapshot", timeRFC3339Formatted(*options.PreviousSnapshot))
150		}
151		if options.Range != nil {
152			headers["Range"] = options.Range.String()
153		}
154		headers = mergeHeaders(headers, headersFromStruct(*options))
155	}
156	uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
157
158	var out GetPageRangesResponse
159	resp, err := b.Container.bsc.client.exec(http.MethodGet, uri, headers, nil, b.Container.bsc.auth)
160	if err != nil {
161		return out, err
162	}
163	defer drainRespBody(resp)
164
165	if err = checkRespCode(resp, []int{http.StatusOK}); err != nil {
166		return out, err
167	}
168	err = xmlUnmarshal(resp.Body, &out)
169	return out, err
170}
171
172// PutPageBlob initializes an empty page blob with specified name and maximum
173// size in bytes (size must be aligned to a 512-byte boundary). A page blob must
174// be created using this method before writing pages.
175//
176// See CreateBlockBlobFromReader for more info on creating blobs.
177//
178// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Blob
179func (b *Blob) PutPageBlob(options *PutBlobOptions) error {
180	if b.Properties.ContentLength%512 != 0 {
181		return errors.New("Content length must be aligned to a 512-byte boundary")
182	}
183
184	params := url.Values{}
185	headers := b.Container.bsc.client.getStandardHeaders()
186	headers["x-ms-blob-type"] = string(BlobTypePage)
187	headers["x-ms-blob-content-length"] = fmt.Sprintf("%v", b.Properties.ContentLength)
188	headers["x-ms-blob-sequence-number"] = fmt.Sprintf("%v", b.Properties.SequenceNumber)
189	headers = mergeHeaders(headers, headersFromStruct(b.Properties))
190	headers = b.Container.bsc.client.addMetadataToHeaders(headers, b.Metadata)
191
192	if options != nil {
193		params = addTimeout(params, options.Timeout)
194		headers = mergeHeaders(headers, headersFromStruct(*options))
195	}
196	uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
197
198	resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
199	if err != nil {
200		return err
201	}
202	return b.respondCreation(resp, BlobTypePage)
203}
204