mirror of
https://github.com/octoleo/restic.git
synced 2024-11-14 01:04:05 +00:00
413 lines
11 KiB
Go
413 lines
11 KiB
Go
|
package storage
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
tablesURIPath = "/Tables"
|
||
|
nextTableQueryParameter = "NextTableName"
|
||
|
headerNextPartitionKey = "x-ms-continuation-NextPartitionKey"
|
||
|
headerNextRowKey = "x-ms-continuation-NextRowKey"
|
||
|
nextPartitionKeyQueryParameter = "NextPartitionKey"
|
||
|
nextRowKeyQueryParameter = "NextRowKey"
|
||
|
)
|
||
|
|
||
|
// TableAccessPolicy are used for SETTING table policies
|
||
|
type TableAccessPolicy struct {
|
||
|
ID string
|
||
|
StartTime time.Time
|
||
|
ExpiryTime time.Time
|
||
|
CanRead bool
|
||
|
CanAppend bool
|
||
|
CanUpdate bool
|
||
|
CanDelete bool
|
||
|
}
|
||
|
|
||
|
// Table represents an Azure table.
|
||
|
type Table struct {
|
||
|
tsc *TableServiceClient
|
||
|
Name string `json:"TableName"`
|
||
|
OdataEditLink string `json:"odata.editLink"`
|
||
|
OdataID string `json:"odata.id"`
|
||
|
OdataMetadata string `json:"odata.metadata"`
|
||
|
OdataType string `json:"odata.type"`
|
||
|
}
|
||
|
|
||
|
// EntityQueryResult contains the response from
|
||
|
// ExecuteQuery and ExecuteQueryNextResults functions.
|
||
|
type EntityQueryResult struct {
|
||
|
OdataMetadata string `json:"odata.metadata"`
|
||
|
Entities []*Entity `json:"value"`
|
||
|
QueryNextLink
|
||
|
table *Table
|
||
|
}
|
||
|
|
||
|
type continuationToken struct {
|
||
|
NextPartitionKey string
|
||
|
NextRowKey string
|
||
|
}
|
||
|
|
||
|
func (t *Table) buildPath() string {
|
||
|
return fmt.Sprintf("/%s", t.Name)
|
||
|
}
|
||
|
|
||
|
func (t *Table) buildSpecificPath() string {
|
||
|
return fmt.Sprintf("%s('%s')", tablesURIPath, t.Name)
|
||
|
}
|
||
|
|
||
|
// Get gets the referenced table.
|
||
|
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/querying-tables-and-entities
|
||
|
func (t *Table) Get(timeout uint, ml MetadataLevel) error {
|
||
|
if ml == EmptyPayload {
|
||
|
return errEmptyPayload
|
||
|
}
|
||
|
|
||
|
query := url.Values{
|
||
|
"timeout": {strconv.FormatUint(uint64(timeout), 10)},
|
||
|
}
|
||
|
headers := t.tsc.client.getStandardHeaders()
|
||
|
headers[headerAccept] = string(ml)
|
||
|
|
||
|
uri := t.tsc.client.getEndpoint(tableServiceName, t.buildSpecificPath(), query)
|
||
|
resp, err := t.tsc.client.exec(http.MethodGet, uri, headers, nil, t.tsc.auth)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer readAndCloseBody(resp.body)
|
||
|
|
||
|
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
respBody, err := ioutil.ReadAll(resp.body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
err = json.Unmarshal(respBody, t)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Create creates the referenced table.
|
||
|
// This function fails if the name is not compliant
|
||
|
// with the specification or the tables already exists.
|
||
|
// ml determines the level of detail of metadata in the operation response,
|
||
|
// or no data at all.
|
||
|
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/create-table
|
||
|
func (t *Table) Create(timeout uint, ml MetadataLevel, options *TableOptions) error {
|
||
|
uri := t.tsc.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{
|
||
|
"timeout": {strconv.FormatUint(uint64(timeout), 10)},
|
||
|
})
|
||
|
|
||
|
type createTableRequest struct {
|
||
|
TableName string `json:"TableName"`
|
||
|
}
|
||
|
req := createTableRequest{TableName: t.Name}
|
||
|
buf := new(bytes.Buffer)
|
||
|
if err := json.NewEncoder(buf).Encode(req); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
headers := t.tsc.client.getStandardHeaders()
|
||
|
headers = addReturnContentHeaders(headers, ml)
|
||
|
headers = addBodyRelatedHeaders(headers, buf.Len())
|
||
|
headers = options.addToHeaders(headers)
|
||
|
|
||
|
resp, err := t.tsc.client.exec(http.MethodPost, uri, headers, buf, t.tsc.auth)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer readAndCloseBody(resp.body)
|
||
|
|
||
|
if ml == EmptyPayload {
|
||
|
if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
} else {
|
||
|
if err := checkRespCode(resp.statusCode, []int{http.StatusCreated}); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ml != EmptyPayload {
|
||
|
data, err := ioutil.ReadAll(resp.body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
err = json.Unmarshal(data, t)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Delete deletes the referenced table.
|
||
|
// This function fails if the table is not present.
|
||
|
// Be advised: Delete deletes all the entries that may be present.
|
||
|
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/delete-table
|
||
|
func (t *Table) Delete(timeout uint, options *TableOptions) error {
|
||
|
uri := t.tsc.client.getEndpoint(tableServiceName, t.buildSpecificPath(), url.Values{
|
||
|
"timeout": {strconv.Itoa(int(timeout))},
|
||
|
})
|
||
|
|
||
|
headers := t.tsc.client.getStandardHeaders()
|
||
|
headers = addReturnContentHeaders(headers, EmptyPayload)
|
||
|
headers = options.addToHeaders(headers)
|
||
|
|
||
|
resp, err := t.tsc.client.exec(http.MethodDelete, uri, headers, nil, t.tsc.auth)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer readAndCloseBody(resp.body)
|
||
|
|
||
|
if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
||
|
return err
|
||
|
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// QueryOptions includes options for a query entities operation.
|
||
|
// Top, filter and select are OData query options.
|
||
|
type QueryOptions struct {
|
||
|
Top uint
|
||
|
Filter string
|
||
|
Select []string
|
||
|
RequestID string
|
||
|
}
|
||
|
|
||
|
func (options *QueryOptions) getParameters() (url.Values, map[string]string) {
|
||
|
query := url.Values{}
|
||
|
headers := map[string]string{}
|
||
|
if options != nil {
|
||
|
if options.Top > 0 {
|
||
|
query.Add(OdataTop, strconv.FormatUint(uint64(options.Top), 10))
|
||
|
}
|
||
|
if options.Filter != "" {
|
||
|
query.Add(OdataFilter, options.Filter)
|
||
|
}
|
||
|
if len(options.Select) > 0 {
|
||
|
query.Add(OdataSelect, strings.Join(options.Select, ","))
|
||
|
}
|
||
|
headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID)
|
||
|
}
|
||
|
return query, headers
|
||
|
}
|
||
|
|
||
|
// QueryEntities returns the entities in the table.
|
||
|
// You can use query options defined by the OData Protocol specification.
|
||
|
//
|
||
|
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities
|
||
|
func (t *Table) QueryEntities(timeout uint, ml MetadataLevel, options *QueryOptions) (*EntityQueryResult, error) {
|
||
|
if ml == EmptyPayload {
|
||
|
return nil, errEmptyPayload
|
||
|
}
|
||
|
query, headers := options.getParameters()
|
||
|
query = addTimeout(query, timeout)
|
||
|
uri := t.tsc.client.getEndpoint(tableServiceName, t.buildPath(), query)
|
||
|
return t.queryEntities(uri, headers, ml)
|
||
|
}
|
||
|
|
||
|
// NextResults returns the next page of results
|
||
|
// from a QueryEntities or NextResults operation.
|
||
|
//
|
||
|
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities
|
||
|
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-timeout-and-pagination
|
||
|
func (eqr *EntityQueryResult) NextResults(options *TableOptions) (*EntityQueryResult, error) {
|
||
|
if eqr == nil {
|
||
|
return nil, errNilPreviousResult
|
||
|
}
|
||
|
if eqr.NextLink == nil {
|
||
|
return nil, errNilNextLink
|
||
|
}
|
||
|
headers := options.addToHeaders(map[string]string{})
|
||
|
return eqr.table.queryEntities(*eqr.NextLink, headers, eqr.ml)
|
||
|
}
|
||
|
|
||
|
// SetPermissions sets up table ACL permissions
|
||
|
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/Set-Table-ACL
|
||
|
func (t *Table) SetPermissions(tap []TableAccessPolicy, timeout uint, options *TableOptions) error {
|
||
|
params := url.Values{"comp": {"acl"},
|
||
|
"timeout": {strconv.Itoa(int(timeout))},
|
||
|
}
|
||
|
|
||
|
uri := t.tsc.client.getEndpoint(tableServiceName, t.Name, params)
|
||
|
headers := t.tsc.client.getStandardHeaders()
|
||
|
headers = options.addToHeaders(headers)
|
||
|
|
||
|
body, length, err := generateTableACLPayload(tap)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
headers["Content-Length"] = strconv.Itoa(length)
|
||
|
|
||
|
resp, err := t.tsc.client.exec(http.MethodPut, uri, headers, body, t.tsc.auth)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer readAndCloseBody(resp.body)
|
||
|
|
||
|
if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func generateTableACLPayload(policies []TableAccessPolicy) (io.Reader, int, error) {
|
||
|
sil := SignedIdentifiers{
|
||
|
SignedIdentifiers: []SignedIdentifier{},
|
||
|
}
|
||
|
for _, tap := range policies {
|
||
|
permission := generateTablePermissions(&tap)
|
||
|
signedIdentifier := convertAccessPolicyToXMLStructs(tap.ID, tap.StartTime, tap.ExpiryTime, permission)
|
||
|
sil.SignedIdentifiers = append(sil.SignedIdentifiers, signedIdentifier)
|
||
|
}
|
||
|
return xmlMarshal(sil)
|
||
|
}
|
||
|
|
||
|
// GetPermissions gets the table ACL permissions
|
||
|
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/get-table-acl
|
||
|
func (t *Table) GetPermissions(timeout int, options *TableOptions) ([]TableAccessPolicy, error) {
|
||
|
params := url.Values{"comp": {"acl"},
|
||
|
"timeout": {strconv.Itoa(int(timeout))},
|
||
|
}
|
||
|
|
||
|
uri := t.tsc.client.getEndpoint(tableServiceName, t.Name, params)
|
||
|
headers := t.tsc.client.getStandardHeaders()
|
||
|
headers = options.addToHeaders(headers)
|
||
|
|
||
|
resp, err := t.tsc.client.exec(http.MethodGet, uri, headers, nil, t.tsc.auth)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.body.Close()
|
||
|
|
||
|
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
var ap AccessPolicy
|
||
|
err = xmlUnmarshal(resp.body, &ap.SignedIdentifiersList)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return updateTableAccessPolicy(ap), nil
|
||
|
}
|
||
|
|
||
|
func (t *Table) queryEntities(uri string, headers map[string]string, ml MetadataLevel) (*EntityQueryResult, error) {
|
||
|
headers = mergeHeaders(headers, t.tsc.client.getStandardHeaders())
|
||
|
if ml != EmptyPayload {
|
||
|
headers[headerAccept] = string(ml)
|
||
|
}
|
||
|
|
||
|
resp, err := t.tsc.client.exec(http.MethodGet, uri, headers, nil, t.tsc.auth)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.body.Close()
|
||
|
|
||
|
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
data, err := ioutil.ReadAll(resp.body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
var entities EntityQueryResult
|
||
|
err = json.Unmarshal(data, &entities)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
for i := range entities.Entities {
|
||
|
entities.Entities[i].Table = t
|
||
|
}
|
||
|
entities.table = t
|
||
|
|
||
|
contToken := extractContinuationTokenFromHeaders(resp.headers)
|
||
|
if contToken == nil {
|
||
|
entities.NextLink = nil
|
||
|
} else {
|
||
|
originalURI, err := url.Parse(uri)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
v := originalURI.Query()
|
||
|
v.Set(nextPartitionKeyQueryParameter, contToken.NextPartitionKey)
|
||
|
v.Set(nextRowKeyQueryParameter, contToken.NextRowKey)
|
||
|
newURI := t.tsc.client.getEndpoint(tableServiceName, t.buildPath(), v)
|
||
|
entities.NextLink = &newURI
|
||
|
entities.ml = ml
|
||
|
}
|
||
|
|
||
|
return &entities, nil
|
||
|
}
|
||
|
|
||
|
func extractContinuationTokenFromHeaders(h http.Header) *continuationToken {
|
||
|
ct := continuationToken{
|
||
|
NextPartitionKey: h.Get(headerNextPartitionKey),
|
||
|
NextRowKey: h.Get(headerNextRowKey),
|
||
|
}
|
||
|
|
||
|
if ct.NextPartitionKey != "" && ct.NextRowKey != "" {
|
||
|
return &ct
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func updateTableAccessPolicy(ap AccessPolicy) []TableAccessPolicy {
|
||
|
taps := []TableAccessPolicy{}
|
||
|
for _, policy := range ap.SignedIdentifiersList.SignedIdentifiers {
|
||
|
tap := TableAccessPolicy{
|
||
|
ID: policy.ID,
|
||
|
StartTime: policy.AccessPolicy.StartTime,
|
||
|
ExpiryTime: policy.AccessPolicy.ExpiryTime,
|
||
|
}
|
||
|
tap.CanRead = updatePermissions(policy.AccessPolicy.Permission, "r")
|
||
|
tap.CanAppend = updatePermissions(policy.AccessPolicy.Permission, "a")
|
||
|
tap.CanUpdate = updatePermissions(policy.AccessPolicy.Permission, "u")
|
||
|
tap.CanDelete = updatePermissions(policy.AccessPolicy.Permission, "d")
|
||
|
|
||
|
taps = append(taps, tap)
|
||
|
}
|
||
|
return taps
|
||
|
}
|
||
|
|
||
|
func generateTablePermissions(tap *TableAccessPolicy) (permissions string) {
|
||
|
// generate the permissions string (raud).
|
||
|
// still want the end user API to have bool flags.
|
||
|
permissions = ""
|
||
|
|
||
|
if tap.CanRead {
|
||
|
permissions += "r"
|
||
|
}
|
||
|
|
||
|
if tap.CanAppend {
|
||
|
permissions += "a"
|
||
|
}
|
||
|
|
||
|
if tap.CanUpdate {
|
||
|
permissions += "u"
|
||
|
}
|
||
|
|
||
|
if tap.CanDelete {
|
||
|
permissions += "d"
|
||
|
}
|
||
|
return permissions
|
||
|
}
|