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	"net/http"
20	"net/url"
21	"sync"
22)
23
24// Directory represents a directory on a share.
25type Directory struct {
26	fsc        *FileServiceClient
27	Metadata   map[string]string
28	Name       string `xml:"Name"`
29	parent     *Directory
30	Properties DirectoryProperties
31	share      *Share
32}
33
34// DirectoryProperties contains various properties of a directory.
35type DirectoryProperties struct {
36	LastModified string `xml:"Last-Modified"`
37	Etag         string `xml:"Etag"`
38}
39
40// ListDirsAndFilesParameters defines the set of customizable parameters to
41// make a List Files and Directories call.
42//
43// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Directories-and-Files
44type ListDirsAndFilesParameters struct {
45	Prefix     string
46	Marker     string
47	MaxResults uint
48	Timeout    uint
49}
50
51// DirsAndFilesListResponse contains the response fields from
52// a List Files and Directories call.
53//
54// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Directories-and-Files
55type DirsAndFilesListResponse struct {
56	XMLName     xml.Name    `xml:"EnumerationResults"`
57	Xmlns       string      `xml:"xmlns,attr"`
58	Marker      string      `xml:"Marker"`
59	MaxResults  int64       `xml:"MaxResults"`
60	Directories []Directory `xml:"Entries>Directory"`
61	Files       []File      `xml:"Entries>File"`
62	NextMarker  string      `xml:"NextMarker"`
63}
64
65// builds the complete directory path for this directory object.
66func (d *Directory) buildPath() string {
67	path := ""
68	current := d
69	for current.Name != "" {
70		path = "/" + current.Name + path
71		current = current.parent
72	}
73	return d.share.buildPath() + path
74}
75
76// Create this directory in the associated share.
77// If a directory with the same name already exists, the operation fails.
78//
79// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Directory
80func (d *Directory) Create(options *FileRequestOptions) error {
81	// if this is the root directory exit early
82	if d.parent == nil {
83		return nil
84	}
85
86	params := prepareOptions(options)
87	headers, err := d.fsc.createResource(d.buildPath(), resourceDirectory, params, mergeMDIntoExtraHeaders(d.Metadata, nil), []int{http.StatusCreated})
88	if err != nil {
89		return err
90	}
91
92	d.updateEtagAndLastModified(headers)
93	return nil
94}
95
96// CreateIfNotExists creates this directory under the associated share if the
97// directory does not exists. Returns true if the directory is newly created or
98// false if the directory already exists.
99//
100// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Directory
101func (d *Directory) CreateIfNotExists(options *FileRequestOptions) (bool, error) {
102	// if this is the root directory exit early
103	if d.parent == nil {
104		return false, nil
105	}
106
107	params := prepareOptions(options)
108	resp, err := d.fsc.createResourceNoClose(d.buildPath(), resourceDirectory, params, nil)
109	if resp != nil {
110		defer drainRespBody(resp)
111		if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusConflict {
112			if resp.StatusCode == http.StatusCreated {
113				d.updateEtagAndLastModified(resp.Header)
114				return true, nil
115			}
116
117			return false, d.FetchAttributes(nil)
118		}
119	}
120
121	return false, err
122}
123
124// Delete removes this directory.  It must be empty in order to be deleted.
125// If the directory does not exist the operation fails.
126//
127// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-Directory
128func (d *Directory) Delete(options *FileRequestOptions) error {
129	return d.fsc.deleteResource(d.buildPath(), resourceDirectory, options)
130}
131
132// DeleteIfExists removes this directory if it exists.
133//
134// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-Directory
135func (d *Directory) DeleteIfExists(options *FileRequestOptions) (bool, error) {
136	resp, err := d.fsc.deleteResourceNoClose(d.buildPath(), resourceDirectory, options)
137	if resp != nil {
138		defer drainRespBody(resp)
139		if resp.StatusCode == http.StatusAccepted || resp.StatusCode == http.StatusNotFound {
140			return resp.StatusCode == http.StatusAccepted, nil
141		}
142	}
143	return false, err
144}
145
146// Exists returns true if this directory exists.
147func (d *Directory) Exists() (bool, error) {
148	exists, headers, err := d.fsc.resourceExists(d.buildPath(), resourceDirectory)
149	if exists {
150		d.updateEtagAndLastModified(headers)
151	}
152	return exists, err
153}
154
155// FetchAttributes retrieves metadata for this directory.
156//  See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-directory-properties
157func (d *Directory) FetchAttributes(options *FileRequestOptions) error {
158	params := prepareOptions(options)
159	headers, err := d.fsc.getResourceHeaders(d.buildPath(), compNone, resourceDirectory, params, http.MethodHead)
160	if err != nil {
161		return err
162	}
163
164	d.updateEtagAndLastModified(headers)
165	d.Metadata = getMetadataFromHeaders(headers)
166
167	return nil
168}
169
170// GetDirectoryReference returns a child Directory object for this directory.
171func (d *Directory) GetDirectoryReference(name string) *Directory {
172	return &Directory{
173		fsc:    d.fsc,
174		Name:   name,
175		parent: d,
176		share:  d.share,
177	}
178}
179
180// GetFileReference returns a child File object for this directory.
181func (d *Directory) GetFileReference(name string) *File {
182	return &File{
183		fsc:    d.fsc,
184		Name:   name,
185		parent: d,
186		share:  d.share,
187		mutex:  &sync.Mutex{},
188	}
189}
190
191// ListDirsAndFiles returns a list of files and directories under this directory.
192// It also contains a pagination token and other response details.
193//
194// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Directories-and-Files
195func (d *Directory) ListDirsAndFiles(params ListDirsAndFilesParameters) (*DirsAndFilesListResponse, error) {
196	q := mergeParams(params.getParameters(), getURLInitValues(compList, resourceDirectory))
197
198	resp, err := d.fsc.listContent(d.buildPath(), q, nil)
199	if err != nil {
200		return nil, err
201	}
202
203	defer resp.Body.Close()
204	var out DirsAndFilesListResponse
205	err = xmlUnmarshal(resp.Body, &out)
206	return &out, err
207}
208
209// SetMetadata replaces the metadata for this directory.
210//
211// Some keys may be converted to Camel-Case before sending. All keys
212// are returned in lower case by GetDirectoryMetadata. HTTP header names
213// are case-insensitive so case munging should not matter to other
214// applications either.
215//
216// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-Directory-Metadata
217func (d *Directory) SetMetadata(options *FileRequestOptions) error {
218	headers, err := d.fsc.setResourceHeaders(d.buildPath(), compMetadata, resourceDirectory, mergeMDIntoExtraHeaders(d.Metadata, nil), options)
219	if err != nil {
220		return err
221	}
222
223	d.updateEtagAndLastModified(headers)
224	return nil
225}
226
227// updates Etag and last modified date
228func (d *Directory) updateEtagAndLastModified(headers http.Header) {
229	d.Properties.Etag = headers.Get("Etag")
230	d.Properties.LastModified = headers.Get("Last-Modified")
231}
232
233// URL gets the canonical URL to this directory.
234// This method does not create a publicly accessible URL if the directory
235// is private and this method does not check if the directory exists.
236func (d *Directory) URL() string {
237	return d.fsc.client.getEndpoint(fileServiceName, d.buildPath(), url.Values{})
238}
239